diff --git a/jest.config.js b/jest.config.js
index f114ef2c3..ac8208423 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -27,7 +27,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
- statements: 77,
+ statements: 75,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit
diff --git a/src/assets/img/icons/Google.svg b/src/assets/img/icons/Google.svg
new file mode 100644
index 000000000..606351ec9
--- /dev/null
+++ b/src/assets/img/icons/Google.svg
@@ -0,0 +1,14 @@
+
diff --git a/src/assets/img/icons/Outlook.com.svg b/src/assets/img/icons/Outlook.com.svg
new file mode 100644
index 000000000..467b585ac
--- /dev/null
+++ b/src/assets/img/icons/Outlook.com.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/assets/img/icons/Outlook.svg b/src/assets/img/icons/Outlook.svg
new file mode 100644
index 000000000..f974fb13a
--- /dev/null
+++ b/src/assets/img/icons/Outlook.svg
@@ -0,0 +1,16 @@
+
diff --git a/src/assets/img/icons/Yahoo.svg b/src/assets/img/icons/Yahoo.svg
new file mode 100644
index 000000000..0206eccbb
--- /dev/null
+++ b/src/assets/img/icons/Yahoo.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/assets/img/icons/iCal.svg b/src/assets/img/icons/iCal.svg
new file mode 100644
index 000000000..399bcdea0
--- /dev/null
+++ b/src/assets/img/icons/iCal.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/constants/calendar-options.js b/src/constants/calendar-options.js
new file mode 100644
index 000000000..c87a85b18
--- /dev/null
+++ b/src/constants/calendar-options.js
@@ -0,0 +1,8 @@
+const calendarOptions = {
+ ICAL: "iCal",
+ GOOGLE: "Google",
+ OUTLOOK: "Outlook",
+ OUTLOOKCOM: "Outlook.com",
+ YAHOO: "Yahoo",
+};
+export { calendarOptions };
diff --git a/src/constants/calendar-status.js b/src/constants/calendar-status.js
new file mode 100644
index 000000000..38cf23467
--- /dev/null
+++ b/src/constants/calendar-status.js
@@ -0,0 +1,7 @@
+const calendarStatus = {
+ FREE: "Free",
+ BUSY: "Busy",
+ TENTATIVE: "Tentative",
+ OUT_OF_THE_OFFICE: "OutOfTheOffice",
+};
+export { calendarStatus };
diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js
index 8f4116c83..320952b4d 100644
--- a/src/constants/endpoints.js
+++ b/src/constants/endpoints.js
@@ -138,6 +138,18 @@ 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",
+ },
LogExperimentExposureIfAssigned: {
url: "/experiments/api/v1/experiments/log-exposure",
method: "POST",
diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js
index b37f0c249..cc1c90d61 100644
--- a/src/constants/store-actions.js
+++ b/src/constants/store-actions.js
@@ -41,6 +41,7 @@ const storeActions = {
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
VALIDATE_ZIP: "validateZip",
PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA: "priceOrderItemsAndSaveServerData",
+ TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA: "taxOrderItemsAndSaveServerData",
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
LOG_PAGE_VIEW: "logPageView",
LOG_CUSTOM_EVENT: "logCustomEvent",
@@ -50,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",
@@ -83,6 +86,7 @@ const storeActions = {
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
"saveSupportingItemsSuppressingStateResetting",
SAVE_VAPS: "saveVaps",
+ SAVE_PROMOS: "savePromos",
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes",
SAVE_WORK_ORDER_FLAG: "saveWorkOrderFlag",
diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js
index 0ace57a7e..f4a43e91f 100644
--- a/src/constants/store-mutations.js
+++ b/src/constants/store-mutations.js
@@ -26,6 +26,8 @@ const storeMutations = {
UPDATE_VAPS: "updateVaps",
UPDATE_SUPPORTING_ITEMS: "updateSupportingItems",
UPDATE_LINE_ITEMS_SERVER_DATA: "updateLineItemsServerData",
+ UPDATE_INACTIVE_PROMOS: "updateInactivePromos",
+ UPDATE_PROMOS: "updatePromos",
UPDATE_REGISTRATION: "updateRegistration",
diff --git a/src/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons.vue b/src/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons.vue
index 0a6faa72f..d894f2ee8 100644
--- a/src/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons.vue
+++ b/src/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons.vue
@@ -21,9 +21,8 @@ export default {
name: "add-vaps-modal-buttons",
props: {
cartItems: Object,
- rainDefenseItem: Object,
- wiperItems: Object,
vapsTilesCmsName: String,
+ availableLineItems: Object,
},
data() {
return {
@@ -55,13 +54,19 @@ export default {
});
},
rainDefensePrice() {
- const lineItem = this.rainDefenseItem;
+ // const lineItem = this.rainDefenseItem;
+ const lineItem = this.availableLineItems.find((item) => {
+ return item.partType === "RAIN DEFENSE";
+ });
+
const price = lineItem.laborAmount + lineItem.sellingPrice + lineItem.kitPrice;
return parseFloat(price).toFixed(2);
},
wipersPrice() {
let price = 0;
- const lineItems = this.wiperItems;
+ const lineItems = this.availableLineItems.filter((item) => {
+ return item.partType.includes(" WIPER");
+ });
lineItems?.forEach((item) => {
price += item.laborAmount + item.sellingPrice + item.kitPrice;
});
diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue
index 67558c323..7107e64e8 100644
--- a/src/fmg-components/cart/cart.vue
+++ b/src/fmg-components/cart/cart.vue
@@ -54,36 +54,28 @@
+
diff --git a/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-list-button/calendar-modal-list-button.vue b/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-list-button/calendar-modal-list-button.vue
new file mode 100644
index 000000000..7f4fe2801
--- /dev/null
+++ b/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-list-button/calendar-modal-list-button.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+ {{ value }}
+
+
+
+
+
+
diff --git a/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.spec.js b/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.spec.js
new file mode 100644
index 000000000..d28c2442f
--- /dev/null
+++ b/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.spec.js
@@ -0,0 +1,53 @@
+import { shallowMount } from "@vue/test-utils";
+import { getMountOptions } from "@/helpers/unit-test-helper.js";
+import calendarModalQuestion from "@/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.vue";
+
+describe("calendar-modal-question.vue", () => {
+ test("When selected calendar option is changed, a correctly selectedCalendarOption should be emitted", async () => {
+ //Arrange
+ const url = "iCal url";
+ const name = "iCal";
+ const icon = "iCal.svg";
+ const expectedEmit = [
+ [
+ {
+ url: url,
+ name: name,
+ icon: icon,
+ },
+ ],
+ ];
+
+ const { wrapper } = setupMocks({
+ customMountOptions: {
+ propsData: {
+ calendarOptionsData: [
+ {
+ url: url,
+ name: name,
+ icon: icon,
+ },
+ ],
+ },
+ },
+ });
+
+ wrapper.vm.$refs.calendarOptions.closeModal = jest.fn();
+ wrapper.vm.selectedCalendarOption = name;
+
+ //Act
+ wrapper.vm.setSelectedCalendarOption();
+
+ //Assert
+ expect(wrapper.emitted()["update:modelValue"]).toEqual(expectedEmit);
+ });
+});
+
+function setupMocks({ customMountOptions }) {
+ const mountOptions = getMountOptions({
+ ...customMountOptions,
+ });
+
+ const wrapper = shallowMount(calendarModalQuestion, mountOptions);
+ return { wrapper };
+}
diff --git a/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.vue b/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.vue
new file mode 100644
index 000000000..dacda07f2
--- /dev/null
+++ b/src/layouts/add-to-calendar/calendar-modal-question/calendar-modal-question.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/layouts/confirmation/confirmation.spec.js b/src/layouts/confirmation/confirmation.spec.js
index c60b19c26..eb77afcb1 100644
--- a/src/layouts/confirmation/confirmation.spec.js
+++ b/src/layouts/confirmation/confirmation.spec.js
@@ -1,5 +1,7 @@
// Components
import confirmation from "@/layouts/confirmation/confirmation.vue";
+import { AppointmentTypeStrings } from "@/constants/schedule-constants";
+import { applicationConfig } from "@/constants/application-config.js";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
@@ -12,14 +14,14 @@ import router from "@/router";
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
-
+var wordingText = "wording Text {custom:ADDRESS}";
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
store.getters = {
order: {
schedule: {
- date: "2019-01-01",
+ date: "2023-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: "000",
@@ -33,9 +35,9 @@ beforeEach(() => {
supportingItems: [],
},
serviceLocation: {
- address: "",
- address2: null,
- city: "",
+ address: "test",
+ address2: "123",
+ city: "test",
state: "AZ",
appointmentType: "Inshop",
zipCode: "12345",
@@ -112,8 +114,188 @@ describe("confirmation.vue", () => {
expect(window.location.assign).toHaveBeenCalled();
});
});
+describe("computed properties...", () => {
+ // test("ScheduleDateFormatted should return date in expected format.", () => {
+ // //Arrange
+ // const { wrapper } = setupMocks({});
+
+ // // Act
+ // const testValue = wrapper.vm.ScheduleDateFormatted;
+
+ // // Assert
+ // expect(testValue).toEqual("Sunday, January 1");
+ // });
+ test("ScheduleTimeFormatted should return mobile time in expected format.", () => {
+ //Arrange
+ store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE;
+ store.getters.order.schedule.startTime = "09:00";
+ store.getters.order.schedule.endTime = "11:00";
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ScheduleTimeFormatted;
+
+ // Assert
+ expect(testValue).toEqual("Between 9 AM - 11 AM");
+ });
+ test("ScheduleTimeFormatted should return DROP_OFF time in expected format.", () => {
+ //Arrange
+ store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
+ store.getters.order.schedule.startTime = "09:00";
+ store.getters.order.schedule.endTime = "11:00";
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ScheduleTimeFormatted;
+
+ // Assert
+ expect(testValue).toEqual("Drop off before 9:30 AM");
+ });
+ test("ScheduleTimeFormatted should return Inshop time in expected format.", () => {
+ //Arrange
+ store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
+ store.getters.order.schedule.startTime = "09:00";
+ store.getters.order.schedule.endTime = "11:00";
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ScheduleTimeFormatted;
+
+ // Assert
+ expect(testValue).toEqual("at 9:00 AM");
+ });
+ test("AppointmentWordingText should return mobile text in expected format.", () => {
+ //Arrange
+ store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE;
+
+ const { wrapper } = setupMocks({});
+ // Act
+ const testValue = wrapper.vm.AppointmentWordingText;
+
+ // Assert
+ expect(testValue).toEqual("wording Text 123, test,
test, AZ 12345");
+ });
+ test("AppointmentWordingText should return DROP_OFF text in expected format.", () => {
+ //Arrange
+ store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
+
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.AppointmentWordingText;
+
+ // Assert
+ expect(testValue).toEqual("wording Text test1,
test, AZ 12345");
+ });
+ test("AppointmentWordingText should return IN_SHOP text in expected format.", () => {
+ //Arrange
+ store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
+
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.AppointmentWordingText;
+
+ // Assert
+ expect(testValue).toEqual("wording Text test1,
test, AZ 12345");
+ });
+ test("ConfirmationEmailText should return text in expected format.", () => {
+ //Arrange
+ wordingText = "{custom:MY_ACCOUNT_URL}";
+ const { wrapper } = setupMocks({});
+ // Act
+ const testValue = wrapper.vm.ConfirmationEmailText;
+
+ // Assert
+ expect(testValue).toEqual(applicationConfig.MY_ACCOUNT);
+ });
+ test("ScheduleDate should return date in expected format.", () => {
+ //Arrange
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ScheduleDate;
+
+ // Assert
+ expect(testValue).toEqual("2023-01-01");
+ });
+ test("ScheduleStartTime should return time in expected format.", () => {
+ //Arrange
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ScheduleStartTime;
+
+ // Assert
+ expect(testValue).toEqual("09:00");
+ });
+ test("ScheduleEndTime should return time in expected format.", () => {
+ //Arrange
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ScheduleEndTime;
+
+ // Assert
+ expect(testValue).toEqual("10:00");
+ });
+ test("InShopWordingText should return text in expected format.", () => {
+ //Arrange
+ wordingText = "InShopWordingText format";
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.InShopWordingText;
+
+ // Assert
+ expect(testValue).toEqual(wordingText);
+ });
+ test("MobileWordingText should return text in expected format.", () => {
+ //Arrange
+ wordingText = "MobileWordingText format";
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.MobileWordingText;
+
+ // Assert
+ expect(testValue).toEqual(wordingText);
+ });
+ test("DropOffWordingText should return text in expected format.", () => {
+ //Arrange
+ wordingText = "DropOffWordingText format";
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.DropOffWordingText;
+
+ // Assert
+ expect(testValue).toEqual(wordingText);
+ });
+ test("ServiceLocationFullAddress should return text in expected format.", () => {
+ //Arrange
+
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ServiceLocationFullAddress;
+
+ // Assert
+ expect(testValue).toEqual("123, test,
test, AZ 12345");
+ });
+ test("ProviderFullAddress should return text in expected format.", () => {
+ //Arrange
+
+ const { wrapper } = setupMocks({});
+
+ // Act
+ const testValue = wrapper.vm.ProviderFullAddress;
+
+ // Assert
+ expect(testValue).toEqual("test1,
test, AZ 12345");
+ });
+});
-const wordingText = "wording Text";
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue
index 3691cf496..cb7ca43c1 100644
--- a/src/layouts/confirmation/confirmation.vue
+++ b/src/layouts/confirmation/confirmation.vue
@@ -10,6 +10,10 @@
+
{{ this.ScheduleDateFormatted }}
{{ this.ScheduleTimeFormatted }}
+
-
-
-
-
-
@@ -58,6 +49,7 @@
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import navbar from "@/fmg-components/nav-bar/nav-bar";
+import addToCalendar from "@/layouts/add-to-calendar/add-to-calendar";
//Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@@ -243,6 +235,7 @@ export default {
Form,
navbar,
vehicleBanner,
+ addToCalendar,
},
};
diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue
index 26e8c52a5..2a0fe3c93 100644
--- a/src/layouts/payment-method/payment-method.vue
+++ b/src/layouts/payment-method/payment-method.vue
@@ -17,8 +17,8 @@
@@ -34,10 +34,12 @@
+ :availableLineItems="availableLineItems"
+ vapsTilesCmsName="VapsProductTiles"
+ v-on="{ 'buttonEvent.openModal': openModal }" />
+
+
@@ -64,6 +66,7 @@ import paymentMethodQuestion from "@/layouts/payment-method/payment-method-quest
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import cart from "@/fmg-components/cart/cart";
import addVapsModalButtons from "@/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons";
+import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import alert from "@/ux-components/alert/alert";
@@ -88,11 +91,14 @@ export default {
null,
"payment-method"
);
+
const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_DEFENSE,
null,
"payment-method"
);
+ console.log("rainDefensePromise: ", rainDefensePromise);
+
const supportingItemsPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS,
null,
@@ -119,16 +125,17 @@ export default {
];
const resultMap = await settleAllPromises(promiseResultMap);
- const clonedGlassParts = store.getters.order.lineItems.glassParts
- ? JSON.parse(JSON.stringify(store.getters.order.lineItems.glassParts))
- : [];
+
+ const glassParts = (await store.getters.order.lineItems.glassParts) ?? [];
+
const availableLineItems = [
resultMap.rainDefense,
...resultMap.supportingItems,
...resultMap.wipers,
- ...clonedGlassParts,
+ ...glassParts,
];
- const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
+
+ const pricedAvailableLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: availableLineItems,
@@ -136,30 +143,25 @@ export default {
"payment-method",
false
);
- const storeLineItems = store.getters.lineItems;
- const cartItems = [
- ...clonedGlassParts,
- ...storeLineItems.supportingItems,
- ...storeLineItems.vaps,
- ];
+ const lineItems = store.getters.lineItems;
+
+ const cartItems = [...glassParts, ...lineItems.supportingItems, ...lineItems.vaps]; // TOBEREMOVED BY AJC IN CSR-1384
// Call the "next" function to complete the transition to this page.
next((vm) => {
+ vm.cartItems = cartItems; // TOBEREMOVED BY AJC IN CSR-1384
+
vm.setCmsContent(resultMap.cmsContent);
- vm.availableLineItems = pricingResults;
- vm.availableWipers = resultMap.wipers;
- vm.availableRainDefense = resultMap.rainDefense;
- vm.cartItems = cartItems;
+ vm.availableLineItems = pricedAvailableLineItems;
+ vm.lineItems = lineItems;
});
},
data() {
return {
- cartItems: [],
- availableWipers: null,
- availableRainDefense: null,
- availableLineItems: null,
- supportingItems: null,
- pricedGlassParts: null,
+ cartItems: [], // TOBEREMOVED BY AJC IN CSR-1384
+
+ lineItems: [],
+ availableLineItems: [],
displayPaypalAlert: this.getPaypalAlert(),
paymentMethodInternalModel: this.getPaymentMethodFromStore(),
};
@@ -217,6 +219,9 @@ export default {
packageReqs && serviceLocationReqs && isInsuranceSet && scheduleReqs && customerReqs
);
},
+ openModal(modalName) {
+ this.$refs[modalName].openModal();
+ },
getPaypalAlert() {
return store.getters.order.payment.piaErrorCode ? true : false;
},
@@ -240,7 +245,7 @@ export default {
const matchedIndices = [];
this.cartItems.forEach((cartItem, i) => {
if (cartItem.partType === item.partType) {
- console.log("adding this to matchedIndices: ", i);
+ // console.log("adding this to matchedIndices: ", i);
matchedIndices.push(i);
}
});
@@ -253,18 +258,25 @@ export default {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
- this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
- this.dispatchStoreAction(this.storeActions.SAVE_WORK_ORDER_FLAG, true, false);
- this.dispatchStoreAction(
- this.storeActions.SAVE_PAYMENT_METHODS_CHOICE,
- this.paymentMethod,
- false
- );
- await submitWorkOrder({ pageNameToLog: "payment-method" });
- this.$router.navigateWithoutSaving(
- this.navigationScenarios.CLICKED_FORWARD,
- this.$route
- );
+ switch (this.paymentMethod) {
+ case paymentMethods.CREDIT_CARD:
+ this.piaSetup("cc");
+ break;
+ case paymentMethods.PAYPAL:
+ this.piaSetup("pp");
+ break;
+ case paymentMethods.AFTERPAY:
+ this.piaSetup("ap");
+ break;
+ default:
+ this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
+ this.dispatchStoreAction(this.storeActions.SAVE_WORK_ORDER_FLAG, true, false);
+ await submitWorkOrder({ pageNameToLog: "payment-method" });
+ this.$router.navigateWithoutSaving(
+ this.navigationScenarios.CLICKED_FORWARD,
+ this.$route
+ );
+ }
},
async ccdPayment() {
this.piaSetup("cc");
@@ -278,16 +290,18 @@ export default {
async piaSetup(payNowType) {
this.$refs.loadingModal.showModal();
- //leaving site, clear any save session promises that we will not be able to resolve when we return
+ // since we're leaving the site for pia, clear any save session promises that we will not be able to resolve when we return
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
await this.dispatchStoreAction(storeActions.SAVE_PAY_NOW_TYPE, payNowType, false);
- // make sure pia error codes are reset, and get a pia work order in delete status
+ // make sure pia error codes are reset
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
- this.dispatchStoreAction(storeActions.SAVE_PIA_WORK_ORDER, true);
+ // if we don't have a work order in delete substatus, set the flag and submit
if (!store.getters.order.workOrderNumber) {
+ this.dispatchStoreAction(storeActions.SAVE_PIA_WORK_ORDER, true);
await submitWorkOrder({ pageNameToLog: "payment-method" });
+ this.dispatchStoreAction(storeActions.SAVE_PIA_WORK_ORDER, false);
}
this.$router.navigateWithoutSaving(
@@ -298,12 +312,9 @@ export default {
},
computed: {
damageInfo() {
+ // console.log("this.$store.getters.damage ", this.$store.getters.damage)
return this.$store.getters.damage;
},
- storeLineItems() {
- // console.log("storeLineItems, aka lineItems from store: ", this.$store.getters.lineItems)
- return this.$store.getters.lineItems;
- },
customCtaCopy() {
switch (this.paymentMethod) {
case paymentMethods.CREDIT_CARD:
@@ -322,7 +333,8 @@ export default {
const enablePIA =
this.getSettingValue(experimentSettings.SUBMIT_ORDER_ENABLE_PIA) === "true";
- return !(piaExperience && enablePIA);
+ return false;
+ //return !(piaExperience && enablePIA);
},
paymentMethod() {
if (this.isPiaDisabled) {
@@ -346,6 +358,7 @@ export default {
cart,
alert,
addVapsModalButtons,
+ contentGroupModal,
paymentMethodQuestion,
},
};
diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js
index 60948bd52..96b7bf092 100644
--- a/src/layouts/quote/quote.spec.js
+++ b/src/layouts/quote/quote.spec.js
@@ -21,6 +21,12 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn(),
}));
+jest.mock("@/helpers/promotions-helper", () => ({
+ revalidatePromosAndValidateNewPromo: jest.fn(() => {
+ return { validatePromoResponse: null, revalidatePromoResponse: null };
+ }),
+}));
+
jest.mock(
"@/store",
() => {
@@ -43,11 +49,20 @@ jest.mock("@/mixins/base-mixin", () => ({
getTierOnePackagePrice() {
return mockTierOnePrice;
},
- filterOutFees() {
+ filterOutFees(items) {
return null;
},
},
}));
+
+const mockMixin = {
+ methods: {
+ filterOutFees: jest.fn().mockImplementation(() => {
+ return null;
+ }),
+ },
+};
+
let mockTierOnePrice = 501;
const mockPriceOrderStoreAction = storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
@@ -93,6 +108,16 @@ afterEach(() => {
describe("quote.vue", () => {
test("IsInsurance false should navigateWithSaving", async () => {
//Arrange
+ store.getters.payment = {
+ insuranceCoverage: {},
+ isInsurance: false,
+ };
+ store.getters.order = {
+ lineItems: [],
+ payment: {
+ parentAccountNumber: 167132,
+ },
+ };
const { wrapper } = setupMocks({
customMountOptions: {
router: {
@@ -102,11 +127,6 @@ describe("quote.vue", () => {
},
});
- store.getters.payment = {
- insuranceCoverage: {},
- isInsurance: false,
- };
-
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
@@ -124,20 +144,28 @@ describe("quote.vue", () => {
test("IsInsurance true should navigateToHeritageFunnel", async () => {
//Arrange
+ store.getters = {
+ payment: {
+ insuranceCoverage: {},
+ isInsurance: true,
+ },
+ order: {
+ lineItems: [],
+ payment: {
+ parentAccountNumber: null,
+ },
+ },
+ };
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithSaving: jest.fn(),
},
route: { quote },
+ mixins: [mockMixin],
},
});
- store.getters.payment = {
- insuranceCoverage: {},
- isInsurance: true,
- };
-
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
@@ -154,9 +182,9 @@ describe("quote.vue", () => {
});
test("should pass arePagePrerequisitesValid with a repair order", () => {
//Arrange
- const { wrapper } = setupMocks({});
store.getters = {
order: {
+ lineItems: [],
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
@@ -172,6 +200,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
@@ -179,7 +208,6 @@ describe("quote.vue", () => {
});
test("should pass arePagePrerequisitesValid with a replace order", () => {
//Arrange
- const { wrapper } = setupMocks({});
store.getters = {
order: {
serviceLocation: {
@@ -200,6 +228,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
@@ -207,9 +236,9 @@ describe("quote.vue", () => {
});
test("should fail arePagePrerequisitesValid without glass parts or flagged as repair", () => {
//Arrange
- const { wrapper } = setupMocks({});
store.getters = {
order: {
+ lineItems: [],
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
@@ -220,6 +249,7 @@ describe("quote.vue", () => {
referralNumber: "1234567",
},
};
+ const { wrapper } = setupMocks({});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
@@ -227,8 +257,6 @@ describe("quote.vue", () => {
});
test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => {
//Arrange
- const { wrapper } = setupMocks({});
-
store.getters = {
order: {
lineItems: {
@@ -236,7 +264,7 @@ describe("quote.vue", () => {
},
},
};
-
+ const { wrapper } = setupMocks({});
//mock this to avoid needing to populate this.$route in an unrelated test
wrapper.vm.getDefaultIsInsuranceSelectedValue = jest.fn();
@@ -257,8 +285,6 @@ describe("quote.vue", () => {
});
test("should default to insurance if query param 'isInsurance' is true", async () => {
//Arrange
- const { wrapper } = setupMocks({});
-
store.getters = {
order: {
lineItems: {
@@ -272,6 +298,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "true" } };
//Act
@@ -287,8 +314,6 @@ describe("quote.vue", () => {
});
test("should default to cash if query param 'isInsurance' is false", async () => {
//Arrange
- const { wrapper } = setupMocks({});
-
store.getters = {
order: {
lineItems: {
@@ -302,6 +327,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "false" } };
//Act
@@ -317,7 +343,6 @@ describe("quote.vue", () => {
});
test("should default to insurance if insurance selection is saved to store", async () => {
//Arrange
- const { wrapper } = setupMocks({});
store.getters = {
order: {
@@ -332,6 +357,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
@@ -348,8 +374,6 @@ describe("quote.vue", () => {
});
test("should default to cash if cash selection is saved to store", async () => {
//Arrange
- const { wrapper } = setupMocks({});
-
store.getters = {
order: {
lineItems: {
@@ -363,6 +387,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
@@ -380,8 +405,6 @@ describe("quote.vue", () => {
test("should default to cash if total economy package price is under $500", async () => {
// Also needs no query parameter or previous selection in store to be present
//Arrange
- const { wrapper } = setupMocks({});
-
store.getters = {
order: {
lineItems: {
@@ -396,6 +419,7 @@ describe("quote.vue", () => {
},
};
mockTierOnePrice = 200;
+ const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
@@ -413,7 +437,6 @@ describe("quote.vue", () => {
test("should default to insurance if total economy package price is over $500", async () => {
// Also needs no query parameter or previous selection in store to be present
//Arrange
- const { wrapper } = setupMocks({});
store.getters = {
order: {
@@ -429,6 +452,7 @@ describe("quote.vue", () => {
},
};
mockTierOnePrice = 505;
+ const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection
wrapper.vm.$route = { query: null };
@@ -445,7 +469,7 @@ describe("quote.vue", () => {
});
test("Should default to insurence if user Service zip is from certain States", async () => {
//Arrange
- const { wrapper } = setupMocks({});
+
store.getters = {
order: {
lineItems: {
@@ -459,6 +483,7 @@ describe("quote.vue", () => {
},
},
};
+ const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: null };
//Act
await quote.beforeRouteEnter.call(
diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue
index e2836ed9e..4773938d0 100644
--- a/src/layouts/quote/quote.vue
+++ b/src/layouts/quote/quote.vue
@@ -29,6 +29,7 @@
:availableLineItems="availableLineItems"
:isInsuranceSelected="isInsuranceSelected"
@vapsItemsSelected="vapsItemsSelectedAction"
+ :activePromos="allActivePromos"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
isRequired />
@@ -79,6 +80,10 @@ import { Form, defineRule } from "vee-validate";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { applicationConfig } from "@/constants/application-config";
import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states";
+import { revalidatePromosAndValidateNewPromo } from "@/helpers/promotions-helper";
+import { queryStrings } from "@/constants/query-strings";
+import { getQuerystringParameter } from "@/helpers/querystring-helper";
+import { setTransitionHooks } from "vue";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
export default {
@@ -122,14 +127,12 @@ export default {
];
const resultMap = await settleAllPromises(promiseResultMap);
- const clonedGlassParts = store.getters.order.lineItems.glassParts
- ? JSON.parse(JSON.stringify(store.getters.order.lineItems.glassParts))
- : [];
+ const nullSafeGlassParts = store.getters.order.lineItems.glassParts ?? [];
const availableLineItems = [
resultMap.rainDefense,
...resultMap.supportingItems,
...resultMap.wipers,
- ...clonedGlassParts,
+ ...nullSafeGlassParts,
];
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
@@ -146,13 +149,26 @@ export default {
resultMap.supportingItems,
false
);
+
+ // Promo logic
+ const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
+
+ const { validatePromoResponse, revalidatePromoResponse } =
+ await revalidatePromosAndValidateNewPromo(
+ promoCodeFromQueryString,
+ pricingResults,
+ "quote"
+ );
+ // End of promo logic
+
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
- vm.pricedGlassParts = clonedGlassParts;
+ vm.pricedGlassParts = nullSafeGlassParts;
vm.supportingItems = resultMap.supportingItems;
vm.availableLineItems = pricingResults;
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems);
+ vm.newValidatedPromos = validatePromoResponse?.data.orderPromos;
});
},
data() {
@@ -162,8 +178,18 @@ export default {
availableLineItems: null,
supportingItems: null,
pricedGlassParts: null,
+ newValidatedPromos: null,
};
},
+ computed: {
+ allActivePromos() {
+ const allActivePromos = [];
+ if (this.newValidatedPromos) allActivePromos.push(...this.newValidatedPromos);
+ if (this.$store.getters.order.lineItems.promos)
+ allActivePromos.push(...this.$store.getters.order.lineItems.promos);
+ return allActivePromos;
+ },
+ },
methods: {
openModalAction(modalName) {
this.$refs[modalName].openModal();
@@ -239,6 +265,7 @@ export default {
}
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
+ this.dispatchStoreAction(this.storeActions.SAVE_PROMOS, this.newValidatedPromos, false);
const payment = this.$store.getters.payment;
if (payment.isInsurance) {
diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue
index e3a4a499a..70872153a 100644
--- a/src/layouts/quote/service-package-question/service-package-question.vue
+++ b/src/layouts/quote/service-package-question/service-package-question.vue
@@ -29,6 +29,10 @@ import {
containsLineItemWithPartType,
findLineItemsWithPartType,
} from "@/helpers/service-package-helper";
+import {
+ getPromosThatMatchLineItemsOnOrder,
+ removeVapsPromosFromPromoArray,
+} from "@/helpers/promotions-helper";
export default {
name: "servicePackageQuestion",
@@ -40,6 +44,7 @@ export default {
isRequired: Boolean,
isInsuranceSelected: Boolean,
availableLineItems: null,
+ activePromos: null,
},
data() {
return {
@@ -86,8 +91,11 @@ export default {
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName),
- buttonAuxillaryCopy: this.getPackagePriceString(answer.Name),
+ buttonAuxillaryCopy: this.getDiscountedPackagePriceString(answer.Name),
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
+ additionalButtonData: {
+ strikeThroughPrice: this.getPackagePriceString(answer.Name),
+ },
}));
return modifiedAnswers;
},
@@ -161,21 +169,33 @@ export default {
return this.processIfStatements(footerText, "custom", this.getCustomValueFromString);
},
getPackagePriceString(packageName) {
- const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2);
+ const formattedPriceFloat = parseFloat(
+ this.getPackagePrice(packageName, { discountedPrice: false })
+ ).toFixed(2);
+ return "$" + formattedPriceFloat;
+ },
+ getDiscountedPackagePriceString(packageName) {
+ const formattedPriceFloat = parseFloat(
+ this.getPackagePrice(packageName, { discountedPrice: true })
+ ).toFixed(2);
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
},
- getPackagePrice(packageName) {
+ getPackagePrice(packageName, { discountedPrice = false }) {
+ const lineItemsToPrice = [...this.nullSafeAvailableLineItems];
+ if (discountedPrice) {
+ lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos));
+ }
let priceFloat = this.isInsuranceSelected
? 0
: baseMixin.methods.getTierOnePackagePrice(
- baseMixin.methods.filterOutFees(this.nullSafeAvailableLineItems)
+ baseMixin.methods.filterOutFees(lineItemsToPrice)
);
- priceFloat += this.getVapsPrice(packageName);
+ priceFloat += this.getVapsPrice(packageName, discountedPrice);
return priceFloat;
},
- getVapsPrice(packageName) {
+ getVapsPrice(packageName, applyPromoDiscounts = false) {
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
let price = 0;
@@ -184,6 +204,16 @@ export default {
price += baseMixin.methods.getTotalLineItemPrice(item);
});
+ if (applyPromoDiscounts && this.activePromos) {
+ const relevantPromos = getPromosThatMatchLineItemsOnOrder(
+ this.activePromos,
+ vapsItems
+ );
+ relevantPromos.forEach((promo) => {
+ price += baseMixin.methods.getTotalLineItemPrice(promo);
+ });
+ }
+
return price;
},
selectDefaultPackage() {
diff --git a/src/layouts/schedule/schedule.spec.js1 b/src/layouts/schedule/schedule.spec.js1
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/router/index.js b/src/router/index.js
index 7401bcc3b..0903ccac6 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -28,6 +28,7 @@ import store from "@/store";
import analyticsMixin from "@/mixins/analytics-mixin";
import { experimentTriggers } from "../constants/experiments";
import { applicationConfig } from "../constants/application-config";
+import { shouldStripPromoQueryString } from "@/helpers/promotions-helper";
const routes = [
{
@@ -283,7 +284,7 @@ async function navigate(
queryStringsObject[queryStrings.ZIP_CODE] = zip;
}
- if (promo) {
+ if (promo && !shouldStripPromoQueryString(currentRoute.query.fmgPage)) {
queryStringsObject[queryStrings.PROMO] = promo;
}
diff --git a/src/store/index.js b/src/store/index.js
index d15c89209..1454c8282 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -14,6 +14,7 @@ import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper
import { deepEqual } from "@/helpers/object-helper";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
+import { deepClone } from "@/helpers/object-helper";
import { queryStrings } from "@/constants/query-strings";
import { partTypeStrings } from "@/constants/part-type-strings";
import {
@@ -85,6 +86,7 @@ const getDefaultState = () => {
supportingItems: null,
vaps: null,
serverData: null,
+ promos: null,
},
payment: {
isInsurance: null,
@@ -96,6 +98,7 @@ const getDefaultState = () => {
isPia: null,
piaType: null,
piaErrorCode: null,
+ inactivePromos: null,
},
schedule: {
date: null,
@@ -196,6 +199,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;
},
@@ -1239,10 +1248,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}`,
@@ -1253,15 +1262,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 }) {
@@ -1541,6 +1555,7 @@ export const actions = {
endpoint: endpoints.SaveSession.url,
payload: {
submitAfterSave: context.state.submitAfterSave,
+ userAgent: navigator.userAgent,
applicationUser: {
crmCustomerId: applicationUser.crmCustomerId,
experiments: applicationUser.experiments,
@@ -1586,6 +1601,7 @@ export const actions = {
supportingItems: lineItems.supportingItems,
vaps: lineItems.vaps,
serverData: lineItems.serverData,
+ promos: lineItems.promos,
},
payment: {
InsuranceCoverage: {
@@ -1593,6 +1609,7 @@ export const actions = {
},
isInsurance: order.payment.isInsurance,
parentAccountNumber: order.payment.parentAccountNumber,
+ inactivePromos: order.payment.inactivePromos,
},
serviceLocation: {
streetAddress: order.serviceLocation.address,
@@ -2015,17 +2032,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(
@@ -2035,14 +2056,18 @@ export const actions = {
const zipCodeToUse = serviceZipCode
? serviceZipCode
: context.getters.order.serviceLocation.zipCode;
+
const ctuToUse = serviceZipCodeCtu
? serviceZipCodeCtu
: context.getters.order.serviceLocation.zipCodeCtu;
+
const flattenedLineItemsWithChildParts =
getFlattenedArrayOfLineItemsWithChildParts(availableLineItems);
+
const lineItemsWithOnlyPartNumbers = flattenedLineItemsWithChildParts.map((lineItem) => ({
partNumber: lineItem.partNumber,
}));
+
const availableLineItemsFormattedForRequest =
buildQueryStringParameterFromArrayOfComplexObjects(
lineItemsWithOnlyPartNumbers,
@@ -2076,9 +2101,160 @@ export const actions = {
context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
- availableLineItems = addPricesToLineItems(availableLineItems, response.data.lineItems);
+ const pricedLineItems = addPricesToLineItems(availableLineItems, response.data.lineItems);
- return availableLineItems;
+ return pricedLineItems;
+ },
+
+ // Tax order actions
+ async taxOrderItemsAndSaveServerData(
+ context,
+ {
+ payload: {
+ billToAccountNumber,
+ providerNumber,
+ appointmentType,
+ serviceLocationState,
+ serviceLocationZipCode,
+ pricedLineItems,
+ },
+ pageNameToLog,
+ }
+ ) {
+ const flattenedLineItemsWithChildParts =
+ getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems);
+
+ const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({
+ partNumber: lineItem.partNumber,
+ laborAmount: lineItem.laborAmount,
+ kitPrice: lineItem.kitPrice,
+ sellingPrice: lineItem.sellingPrice,
+ }));
+
+ const pricedLineItemsFormattedForRequest =
+ buildQueryStringParameterFromArrayOfComplexObjects(
+ lineItemsWithOnlyPriceInfo,
+ "lineItems"
+ );
+
+ let queryString =
+ `ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` +
+ `&BillToAccountNumber=${billToAccountNumber}` +
+ `&ProviderNumber=${providerNumber}` +
+ `&AppointmentType=${appointmentType}` +
+ `&ServiceLocation.State=${serviceLocationState}` +
+ `&ServiceLocation.ZipCode=${serviceLocationZipCode}` +
+ `&${pricedLineItemsFormattedForRequest}`;
+
+ const lineItemServerData = context.getters.order.lineItems.serverData;
+ if (lineItemServerData) {
+ queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
+ }
+
+ const response = await globalMethods.callHttpClient({
+ method: endpoints.TaxOrderItems.method,
+ endpoint: `${endpoints.TaxOrderItems.url}?${queryString}`,
+ logApiCall: true,
+ pageNameToLog: pageNameToLog,
+ });
+
+ context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
+
+ const taxedLineItems = addTaxesToPricedLineItems(
+ pricedLineItems,
+ response.data.taxedLineItems
+ );
+
+ 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
@@ -2145,7 +2321,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);
},
@@ -2286,21 +2462,55 @@ function convertGlassPieceNamingFromApi(glassArray) {
}
function addPricesToLineItems(lineItems, pricingLineItems) {
- lineItems.forEach((lineItem) => {
+ const pricedLineItems = deepClone(lineItems);
+ pricedLineItems.forEach((lineItem) => {
let lineItemIndex = pricingLineItems.findIndex(
(pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber
);
+
if (lineItem.childParts) {
addPricesToLineItems(lineItem.childParts, pricingLineItems);
}
+
let pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0];
lineItem.laborAmount = pricedLineItem.laborAmount;
lineItem.sellingPrice = pricedLineItem.sellingPrice;
lineItem.kitPrice = pricedLineItem.kitPrice;
});
+
+ return pricedLineItems;
+}
+
+function addTaxesToPricedLineItems(lineItems, taxingLineItems) {
+ lineItems.forEach((lineItem) => {
+ let lineItemIndex = taxingLineItems.findIndex(
+ (taxingLineItem) => taxingLineItem.partNumber === lineItem.partNumber
+ );
+
+ if (lineItem.childParts) {
+ addTaxesToPricedLineItems(lineItem.childParts, taxingLineItems);
+ }
+
+ let taxedLineItem = taxingLineItems.splice(lineItemIndex, 1)[0];
+ lineItem.SalesTax = taxedLineItem.SalesTax;
+ });
+
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) => {
@@ -2335,6 +2545,37 @@ function convertGlassPieceToBackEndCompatibleFormat(glassPieces) {
});
}
+function renameGlassToReplaceAttributes(glassToReplace) {
+ let newGlassToReplace = [];
+ if (glassToReplace) {
+ glassToReplace.forEach((item) => {
+ newGlassToReplace.push({ location: item.glassLocation, name: item.glassName });
+ });
+ }
+ return newGlassToReplace;
+}
+
+function addGuidToLineItemsIfNotAlreadyThere(lineItems) {
+ lineItems.forEach((lineItem) => {
+ if (!lineItem.id) {
+ lineItem.id = crypto.randomUUID();
+ }
+ });
+}
+
+function syncLineItemIds(lineItemsWithoutIds, lineItemsWithIds) {
+ if (!lineItemsWithIds || !lineItemsWithoutIds) {
+ return;
+ }
+ lineItemsWithoutIds.forEach((noId) => {
+ lineItemsWithIds.forEach((withId) => {
+ if (noId.partNumber === withId.partNumber && noId.partType === withId.partType) {
+ noId.id = withId.id;
+ }
+ });
+ });
+}
+
function providersEqual(providerA, providerB) {
return (
providerA.providerNumber === providerB.providerNumber &&
diff --git a/src/store/store.spec.js b/src/store/store.spec.js
index 9ae8065ae..1c9a5e92b 100644
--- a/src/store/store.spec.js
+++ b/src/store/store.spec.js
@@ -9,6 +9,8 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants";
// Mock global method
globalMethods.callHttpClient = jest.fn();
+global.crypto = { randomUUID: jest.fn() };
+
describe("Mutations", () => {
it("Updates vehicle year in state", () => {
// Arrange
@@ -1378,6 +1380,8 @@ describe("Actions", () => {
state: state,
};
+ context.state.order.lineItems = [];
+
const commit = jest.fn();
const dispatch = jest.fn();
@@ -1385,10 +1389,10 @@ describe("Actions", () => {
context.dispatch = dispatch;
// Act
- actions.saveGlassParts(context, { glassParts: {} });
+ actions.saveGlassParts(context, []);
// Assert
- expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, { glassParts: {} });
+ expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, []);
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
});
@@ -1417,7 +1421,7 @@ describe("Actions", () => {
state: {
order: {
lineItems: {
- supportingItems: ["TestValue1", "TestValue2"],
+ supportingItems: [{ partNum: "TestValue1" }, { partNum: "TestValue2" }],
},
},
},
@@ -1430,7 +1434,11 @@ describe("Actions", () => {
context.dispatch = dispatch;
// Act
- actions.saveSupportingItems(context, ["TestValue3", "TestValue4", "TestValue5"]);
+ actions.saveSupportingItems(context, [
+ { partNum: "TestValue3" },
+ { partNum: "TestValue4" },
+ { partNum: "TestValue5" },
+ ]);
// Assert
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
@@ -1442,7 +1450,7 @@ describe("Actions", () => {
state: {
order: {
lineItems: {
- supportingItems: ["TestValue1", "TestValue2"],
+ supportingItems: [{ partNum: "TestValue1" }, { partNum: "TestValue2" }],
},
},
},
@@ -1455,7 +1463,10 @@ describe("Actions", () => {
context.dispatch = dispatch;
// Act
- actions.saveSupportingItems(context, ["TestValue1", "TestValue2"]);
+ actions.saveSupportingItems(context, [
+ { partNum: "TestValue1" },
+ { partNum: "TestValue2" },
+ ]);
// Assert
expect(dispatch).not.toBeCalledWith(