diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 29d36d091..bfc02d034 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -72,6 +72,10 @@ const endpoints = { url: "/parts/api/v1/parts/mobile-fee", method: "GET", }, + GetPricingByDayUpchargePart: { + url: "/parts/api/v1/parts/get-pricing-by-day-part-number", + method: "GET", + }, GetServicePackageDiscountPart: { url: "/parts/api/v1/parts/service-package-discount", method: "POST", diff --git a/src/constants/part-number-strings.js b/src/constants/part-number-strings.js index d21ab83ac..d373ba016 100644 --- a/src/constants/part-number-strings.js +++ b/src/constants/part-number-strings.js @@ -3,6 +3,8 @@ const partNumberStrings = { MOBILE_STATIC_RECAL_FEE: "RECAL MOBILE", MOBILE_DUAL_RECAL_FEE: "RECAL MOBILEDUAL", DONATION: "DONATION", + // Fees + RECYCLE_FEE: "RECYCLE FEE", }; export { partNumberStrings }; diff --git a/src/constants/schedule-constants.js b/src/constants/schedule-constants.js index 5bb784efb..b7eaa4882 100644 --- a/src/constants/schedule-constants.js +++ b/src/constants/schedule-constants.js @@ -8,9 +8,17 @@ const PREMIUM_TIME_SLOT_ID_FLAG = "-PREMIUM"; const PREMIUM_FEE_PART_TYPE = "EARLY BIRD"; +const PRICING_BY_DAY_UPCHARGE_FEE_PART_TYPE = "DISC CASHSAVE20"; + const RouteCodeFlags = { ALL_DAY_DROP_OFF: "ALL DAY DROP OFF", OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF", }; -export { AppointmentTypeStrings, PREMIUM_TIME_SLOT_ID_FLAG, PREMIUM_FEE_PART_TYPE, RouteCodeFlags }; +export { + AppointmentTypeStrings, + PREMIUM_TIME_SLOT_ID_FLAG, + PREMIUM_FEE_PART_TYPE, + PRICING_BY_DAY_UPCHARGE_FEE_PART_TYPE, + RouteCodeFlags, +}; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 6c6ba62b7..af68c8997 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -30,6 +30,7 @@ const storeActions = { GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer", GET_MOLDING_QUESTIONS: "getMoldingQuestions", GET_MOBILE_FEE_PART: "getMobileFeePart", + GET_PRICING_BY_DAY_UPCHARGE_PART: "getPricingByDayUpchargePart", GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SHOP_TIME_SLOTS: "getShopTimeSlots", diff --git a/src/digital-components/date-picker/mixins/constants.js b/src/digital-components/date-picker/mixins/constants.js index f37e312f5..f9c9f6c9f 100644 --- a/src/digital-components/date-picker/mixins/constants.js +++ b/src/digital-components/date-picker/mixins/constants.js @@ -23,4 +23,6 @@ const MONTHS_OF_YEAR = [ const DAYS_OF_WEEK = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; -export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK }; +const PREMIUM_DAY_INDEXES = [1, 5, 6]; // assign Monday, Friday, Saturday to be premium days + +export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK, PREMIUM_DAY_INDEXES }; diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index 72ed56035..74cfc0964 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -135,7 +135,7 @@ export default { }, closeModal() { const modal = Modal.getInstance(document.getElementById(this.modalId)); - modal.hide(); + modal?.hide(); this.$emit("isModalOpened", false); }, }, diff --git a/src/experiment-components/date-picker-for-pricing-by-day.vue b/src/experiment-components/date-picker-for-pricing-by-day.vue index df4c9b140..7d63499ef 100644 --- a/src/experiment-components/date-picker-for-pricing-by-day.vue +++ b/src/experiment-components/date-picker-for-pricing-by-day.vue @@ -66,12 +66,15 @@ type="radio" name="day-of-month" v-model="selectedDate" - @click="fireDateSelectedEvent" - @keypress.enter="fireDateSelectedEvent" + @click="fireDateSelectedEvent($event, date)" + @keypress.enter="fireDateSelectedEvent($event, date)" :value="date.inputValue" :id="`${month.monthLabel}-${date.dateNum.toString()}`" /> @@ -103,6 +106,7 @@ import { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, + PREMIUM_DAY_INDEXES, } from "@/digital-components/date-picker/mixins/constants"; import { selectableDaysOptions, @@ -156,6 +160,9 @@ export default { type: String, default: "", }, + showPricingByDay: Boolean, + baseDayPrice: Number, + pricingByDayUpcharge: Number, }, setup(props) { const uuid = uuidv4(); @@ -220,17 +227,12 @@ export default { initializeComponent(initialData) { this.setCalendarData(initialData); }, - fireDateSelectedEvent(event) { + fireDateSelectedEvent(event, date) { // Ignore if arrow key selected radioButton if (event.screenX === 0 && event.screenY === 0) { return; } - this.$emit("date-clicked"); - }, - fireDateSelectedEvent2(date) { - console.log("date: ", date); - this.selectedDate = date; - this.$emit("date-clicked"); + this.$emit("date-clicked", date); }, getWeekStartDate(dateString) { const date = convertDateStringToDate(dateString); @@ -429,6 +431,8 @@ export default { hideSomeDaysForInitialView: hideSomeDaysForInitialView, hideSecondMonth: hideSecondMonth, preSelectedDate: config.preSelectedDate, + baseDayPrice: config.baseDayPrice, + pricingByDayUpcharge: config.pricingByDayUpcharge, }; return initialData; }); @@ -453,6 +457,8 @@ export default { initialViewEndDate: config.initialViewEndDate, hideSecondMonth: hideSecondMonth, preSelectedDate: config.preSelectedDate, + baseDayPrice: config.baseDayPrice, + pricingByDayUpcharge: config.pricingByDayUpcharge, }; if (direction === "future") { // first 0, then 1 @@ -586,6 +592,17 @@ export default { ("0" + monthNum).slice(-2) + "-" + ("0" + i).slice(-2); + const dayIndex = convertDateStringToDate(dateString).getDay(); + const isSelectable = + this.selectableDatesData.findIndex((date) => date.date === dateString) > -1 + ? true + : false; + + const isPricingByDayUpchargeDay = PREMIUM_DAY_INDEXES.includes(dayIndex); + let displayPrice = isPricingByDayUpchargeDay + ? options.baseDayPrice + options.pricingByDayUpcharge + : options.baseDayPrice; + const priceString = "$" + displayPrice; if (offset === 0 && i === this.todayDateNum) { dayClasses += " current-day"; @@ -596,7 +613,7 @@ export default { if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") { dayClasses += " unavailable-day"; } - if (convertDateStringToDate(dateString).getDay() === 0) { + if (dayIndex === 0) { dayClasses += " sunday"; } if ( @@ -611,10 +628,9 @@ export default { dateNum: i, dayClasses: dayClasses, inputValue: dateString, - isSelectable: - this.selectableDatesData.findIndex((date) => date.date === dateString) > -1 - ? true - : false, + priceString: priceString, + isSelectable: isSelectable, + isPricingByDayUpchargeDay: isPricingByDayUpchargeDay, }; dates.push(dateObject); } diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index b53dd30d7..f569c11d4 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -169,10 +169,16 @@ import { deepClone } from "@/helpers/object-helper"; import { getHighestFullySatisfiedTier, getPackageContents, - getDiscountPackageName, + getDiscountedPackageName, } from "@/helpers/service-package-helper"; import { getPromoCodeWithoutBundleIdentifier } from "@/helpers/promotions-helper"; import { storeActions } from "@/constants/store-actions"; +import { + getDisplayAmountDue, + getAmountDue, + getSubTotal, + getSalesTax, +} from "@/helpers/pricing-helper.js"; // Constants import { partTypeStrings } from "@/constants/part-type-strings"; @@ -467,7 +473,7 @@ export default { const discountServicePackage = experimentMixin.methods.getSettingValue( experimentSettings.PROMO_ON_PACKAGE ); - return getDiscountPackageName(discountServicePackage); + return getDiscountedPackageName(discountServicePackage); }, servicePackageTitleWidget() { const servicePackageNames = this.getCmsContent( @@ -1082,8 +1088,8 @@ export default { subTotal() { if (!this.lineItems || this.lineItems.length < 1) return; return this.isInsurance || !this.shouldHideRecalibration - ? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order) - : baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculate without recal + ? getSubTotal(this.lineItems) // calculate with recal (if on order) + : getSubTotal(this.lineItemsWithoutRecal); // calculate without recal }, salesTax() { if (!this.lineItems || this.lineItems.length < 1) return; @@ -1098,15 +1104,15 @@ export default { } return this.isInsurance || !this.shouldHideRecalibration - ? baseMixin.methods.getSalesTax(this.lineItems) // calculate with recal (if on order) - : baseMixin.methods.getSalesTax(this.lineItemsWithoutRecal); // calculated without recal + ? getSalesTax(this.lineItems) // calculate with recal (if on order) + : getSalesTax(this.lineItemsWithoutRecal); // calculated without recal }, amountDue() { if (!this.lineItems || this.lineItems.length < 1) return; if (this.showAsPaid) return 0; return this.isInsurance || !this.shouldHideRecalibration - ? baseMixin.methods.getAmountDue(this.lineItems) // calculate with recal (if on order) - : baseMixin.methods.getAmountDue(this.lineItemsWithoutRecal); // calculated without recal + ? getAmountDue(this.lineItems) // calculate with recal (if on order) + : getAmountDue(this.lineItemsWithoutRecal); // calculated without recal }, amountPaid() { if (!this.showAsPaid) { diff --git a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue index ebaa5a8be..76ee7c6a2 100644 --- a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue +++ b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.vue @@ -56,7 +56,7 @@ export default { }, unmounted() { // remove any modal effects before leaving page (e.g. user hits browser back button) - this.modal.closeModal(); + this.modal?.closeModal(); }, computed: { buttonText() { diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js new file mode 100644 index 000000000..c470aaf31 --- /dev/null +++ b/src/helpers/pricing-helper.js @@ -0,0 +1,93 @@ +import store from "@/store"; +import { storeActions } from "@/constants/store-actions"; +import baseMixin from "@/mixins/base-mixin.js"; + +export function getDisplayAmountDue(lineItemsObject, includeTax = true) { + return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", { + style: "currency", + currency: "USD", + }); +} + +export function getAmountDue(lineItemsObject, includeTax = true) { + let amountDue = 0; + const order = baseMixin?.methods?.hasSubmittedOrder() + ? baseMixin?.methods?.getSubmittedOrder() + : store.getters.order; + + if (lineItemsObject?.glassParts) { + amountDue += baseMixin.methods.getTotalPriceOfAllLineItemsAndChildParts( + lineItemsObject.glassParts, + includeTax + ); + } + + if (lineItemsObject?.supportingItems) { + amountDue += baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts( + lineItemsObject.supportingItems, + includeTax + ); + } + + if (store.getters.coverageIsVerified && !order.policy.isNoComp && !order.policy.isItac) { + amountDue = order.policy.currentDeductible; + } + + if (lineItemsObject?.vaps) { + amountDue += baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts( + lineItemsObject.vaps, + includeTax + ); + } + + if (lineItemsObject?.promos) { + amountDue += baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts( + lineItemsObject.promos, + includeTax + ); + } + + return ((amountDue * 100) / 100).toFixed(2); +} + +export function getSubTotal(lineItemsObject) { + // this is amountDue without sales tax + return getAmountDue(lineItemsObject, false); +} + +export function getSalesTax(lineItemsObject) { + // this is amountDue minus subTotal + return (((getAmountDue(lineItemsObject) - getSubTotal(lineItemsObject)) * 100) / 100).toFixed( + 2 + ); +} + +export async function getPriceUpchargeByDayPart(pageNameToLog) { + // Get the Pricing By Day Part + const basePriceByDayPart = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.GET_PRICING_BY_DAY_UPCHARGE_PART, + null, + pageNameToLog, + false + ); + + if ( + basePriceByDayPart?.data === null || + basePriceByDayPart?.data === undefined || + basePriceByDayPart?.data === "" + ) { + return null; + } + + // Get the Pricing By Day Base Part price + const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, + { + availableLineItems: [basePriceByDayPart?.data], + }, + pageNameToLog, + false + ); + + return pricingResults[0]; +} diff --git a/src/helpers/pricing-helper.spec.js b/src/helpers/pricing-helper.spec.js new file mode 100644 index 000000000..8a49bb5ae --- /dev/null +++ b/src/helpers/pricing-helper.spec.js @@ -0,0 +1,64 @@ +import { + getDisplayAmountDue, + getAmountDue, + getSubTotal, + getSalesTax, +} from "@/helpers/pricing-helper.js"; + +const lineItems = { + glassParts: [], + supportingItems: [], + vaps: [ + { + cartItemType: "FRONT WIPERS", + description: 'WIPER BLADE STANDARD 18"', + kitPrice: 0, + laborAmount: 0, + partNumber: "WB18", + partType: "FRONT WIPER", + salesTax: 2.0, + sellingPrice: 30.0, + }, + { + cartItemType: "FRONT WIPERS", + description: 'WIPER BLADE STANDARD 26"', + kitPrice: 0, + laborAmount: 0, + partNumber: "WB26", + partType: "FRONT WIPER", + salesTax: 1.0, + sellingPrice: 20.0, + }, + ], + promos: [], +}; + +describe("pricing-helper", () => { + describe("getDisplayAmountDue", () => { + it("should return the correct display amount due", () => { + const result = getDisplayAmountDue(lineItems); + expect(result).toBe("53.00"); + }); + }); + + describe("getAmountDue", () => { + it("should return the correct amount due", () => { + const result = getAmountDue(lineItems, true); + expect(result).toBe("53.00"); + }); + }); + + describe("getSubTotal", () => { + it("should return the correct subtotal", () => { + const result = getSubTotal(lineItems); + expect(result).toBe("50.00"); + }); + }); + + describe("getSalesTax", () => { + it("should return the correct sales tax", () => { + const result = getSalesTax(lineItems); + expect(result).toBe("3.00"); + }); + }); +}); diff --git a/src/helpers/recal-helper.js b/src/helpers/recal-helper.js index d43da5d34..2794bbcf6 100644 --- a/src/helpers/recal-helper.js +++ b/src/helpers/recal-helper.js @@ -35,10 +35,9 @@ export function containsRecalParts(lineItems) { } } -export function getItemsWithoutRecalParts(lineItems) { - const copy = deepClone(lineItems); - - const firstLevelFiltered = copy.filter((li) => !isRecalPart(li)); +export function getItemsWithoutRecalParts(lineItemsArray) { + if (!lineItemsArray || !Array.isArray(lineItemsArray)) return null; + const firstLevelFiltered = deepClone(lineItemsArray).filter((li) => !isRecalPart(li)); const childrenFiltered = firstLevelFiltered.map((li) => { if (li.childParts && li.childParts.length > 0) { diff --git a/src/helpers/service-package-helper.js b/src/helpers/service-package-helper.js index 6ff937564..fcbe4aee3 100644 --- a/src/helpers/service-package-helper.js +++ b/src/helpers/service-package-helper.js @@ -1,6 +1,12 @@ import { partTypeStrings } from "@/constants/part-type-strings"; import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected"; import { packageNames } from "@/constants/package-names"; +import baseMixin from "@/mixins/base-mixin.js"; +import { getItemsWithoutRecalParts } from "@/helpers/recal-helper"; +import { + getPromosThatMatchLineItemsOnOrder, + removeVapsPromosFromPromoArray, +} from "@/helpers/promotions-helper"; export function containsLineItemWithPartType(typeToFind, itemsToSearch) { const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch); @@ -239,7 +245,7 @@ export function getPackageNameByType(packageType) { return package_names[packageType] || null; } -export function getDiscountPackageName(discountPackage) { +export function getDiscountedPackageName(discountPackage) { const package_names = { ECON: packageNames.TIER_ONE, STANDARD: packageNames.TIER_TWO, diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index bf1157f2c..013916af0 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -16,6 +16,10 @@ jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({ submitWorkOrder: jest.fn(), })); +jest.mock("@/helpers/pricing-helper.js", () => ({ + getAmountDue: jest.fn(), +})); + let piaDisabledFlag = false; describe("payment-method.vue", () => { diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 47a6e2a14..33e070a93 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -158,6 +158,12 @@ import { coverageStatus } from "@/constants/insurance"; import { containsLineItemWithPartType } from "@/helpers/service-package-helper"; import { containsRecalParts } from "@/helpers/recal-helper"; import { getBoolFromString } from "@/helpers/boolean-helper"; +import { + getDisplayAmountDue, + getAmountDue, + getSubTotal, + getSalesTax, +} from "@/helpers/pricing-helper.js"; defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); @@ -767,7 +773,7 @@ export default { ); }, totalAmountDue() { - return baseMixin.methods.getAmountDue(this.lineItems); + return getAmountDue(this.lineItems); }, isPiaEnabled() { const piaExperience = this.getSettingValue(experimentSettings.PIA_EXPERIENCE); diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index 6bb99f3c8..b9f19f64a 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -10,11 +10,17 @@ import { queryStrings } from "@/constants/query-strings"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { storeActions } from "@/constants/store-actions"; import store from "@/store"; -import baseMixin from "@/mixins/base-mixin.js"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import { Form } from "vee-validate"; import { paymentMethods } from "@/constants/payment-method-constants"; import { routerParams } from "@/router/router-constants/router-params"; +import baseMixin from "@/mixins/base-mixin.js"; +import { + getDisplayAmountDue, + getAmountDue, + getSubTotal, + getSalesTax, +} from "@/helpers/pricing-helper.js"; // iframeResizer IS loaded into the page and necessary for the package to // to auto scale the iFrame this page is loaded in // Do not remove despite showing as "unused" CASH-309 @@ -163,7 +169,7 @@ export default { } }, getAmountDue() { - return baseMixin.methods.getAmountDue(store.getters.order.lineItems); + return getAmountDue(store.getters.order.lineItems); }, async saveAndSubmitWorkOrder() { // Final work order submit after returning from PIA. diff --git a/src/layouts/payment/payment.spec.js b/src/layouts/payment/payment.spec.js index 7bc14f229..05ad6dbc9 100644 --- a/src/layouts/payment/payment.spec.js +++ b/src/layouts/payment/payment.spec.js @@ -5,10 +5,14 @@ import payment from "@/layouts/payment/payment"; import { shallowMount } from "@vue/test-utils"; import { getMountOptions } from "@/helpers/unit-test-helper"; import store from "@/store"; -import baseMixin from "@/mixins/base-mixin"; import { storeActions } from "@/constants/store-actions"; import { paymentMethods } from "@/constants/payment-method-constants"; +jest.mock("@/helpers/pricing-helper.js", () => ({ + getAmountDue: jest.fn(), + getDisplayAmountDue: jest.fn(), +})); + // Constants const parts = { windshield: { diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index ecd2489c8..abf06927b 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -234,6 +234,12 @@ import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer. import { routerParams } from "@/router/router-constants/router-params"; import { coverageStatus } from "@/constants/insurance"; import { getBoolFromString } from "@/helpers/boolean-helper.js"; +import { + getDisplayAmountDue, + getAmountDue, + getSubTotal, + getSalesTax, +} from "@/helpers/pricing-helper.js"; export default { name: "payment", @@ -656,10 +662,10 @@ export default { this.piaLineItems = lineItems.join("||"); }, getAmountDue() { - return baseMixin.methods.getAmountDue(store.getters.order.lineItems); + return getAmountDue(store.getters.order.lineItems); }, getDisplayAmountDue() { - return baseMixin.methods.getDisplayAmountDue(store.getters.order.lineItems); + return getDisplayAmountDue(store.getters.order.lineItems); }, async paymentFailedPayLater() { this.$refs.loadingModal.isModalVisible = true; diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index d32054108..615662013 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -266,6 +266,7 @@ export default { false ); + // This will remove any servicePackageDiscount item from lineItems.supportingItems baseMixin.methods.dispatchStoreAction( storeActions.SAVE_SUPPORTING_ITEMS, resultMap.supportingItems, @@ -484,7 +485,7 @@ export default { }; }, mounted() { - this.attachCustomEvents(); + this.attachCustomEventsForAnalytics(); }, computed: { lineItemsCloneForWatcher() { @@ -670,7 +671,7 @@ export default { ); } }, - attachCustomEvents() { + attachCustomEventsForAnalytics() { this.prependActionToMethod(this, this.forwardButtonAction, () => { if (!this.isInsuranceSelected && this.lineItems.vaps?.length == 0) { const tierOnePrice = 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 ea40b2780..ab4772107 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -30,7 +30,7 @@ import { containsLineItemWithPartType, findLineItemsWithPartType, getPackageNameByType, - getDiscountPackageName, + getDiscountedPackageName, } from "@/helpers/service-package-helper"; import { getPromosThatMatchLineItemsOnOrder, @@ -166,7 +166,7 @@ export default { const discountServicePackage = experimentMixin.methods.getSettingValue( experimentSettings.PROMO_ON_PACKAGE ); - return getDiscountPackageName(discountServicePackage); + return getDiscountedPackageName(discountServicePackage); }, frontWipersApplicableForTierTwo() { return shouldFrontWipersBeAvailable( diff --git a/src/layouts/schedule/schedule.spec.js b/src/layouts/schedule/schedule.spec.js index 48e6e8afa..1c615a961 100644 --- a/src/layouts/schedule/schedule.spec.js +++ b/src/layouts/schedule/schedule.spec.js @@ -130,6 +130,10 @@ jest.mock("@/mixins/base-mixin.js", () => ({ ]); } }), + filterOutCertainPartTypesOrNumbers: jest.fn(), + hasSubmittedOrder: jest.fn(), + getTotalPriceOfAllLineItemsAndChildParts: jest.fn(), + getTotalLineItemPrice: jest.fn(), }, })); @@ -631,34 +635,6 @@ describe("schedule.vue...", () => { expect.anything() ); }); - - test("if no EARLY BIRD supporting item, then updateSupportingItems should NOT call store action", async () => { - // Arrange - store.getters.order.serviceLocation.appointmentType = "Mobile"; - store.getters.lineItems.supportingItems = []; - const { wrapper } = setupMocks({}); - wrapper.vm.dispatchStoreAction = jest.fn(() => { - return { - data: [], - }; - }); - wrapper.vm.mobilePremiumAppointmentFee = 14.99; - wrapper.setData({ - selectedTimeSlot: { - date: "2019-01-01", - startTime: "09:00", - endTime: "10:00", - routeCode: null, - isPremiumAppointment: false, - }, - }); - - // Act - await wrapper.vm.updateSupportingItems(); - - // Assert - expect(wrapper.vm.dispatchStoreAction).not.toBeCalled(); - }); }); const mockCmsContent = {}; @@ -686,6 +662,7 @@ function setupMocks({ customMountOptions }) { wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.$refs.datePicker.initializeComponent = jest.fn(); + wrapper.vm.$refs.datePickerForPricingByDay.initializeComponent = jest.fn(); wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn(); wrapper.vm.$refs.timeSlotModalQuestion.openModal = jest.fn(); diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 0d688e4d9..7a451accf 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -19,17 +19,20 @@ + :baseDayPrice="baseDayPrice" + :pricingByDayUpcharge="pricingByDayUpcharge" + :showPricingByDay="showPricingByDay" + @date-clicked="handleDateClicked" /> item.partType == PRICING_BY_DAY_UPCHARGE_FEE_PART_TYPE + ); + if (pricingByDayUpchargeFeeIndex > -1) { + includePricingByDaySurcharge = true; + } + } + + // Load page with date already selected? let preSelectedDate = await store.getters.order.schedule.date; if (!preSelectedDate || preSelectedDate.startTime === null) { preSelectedDate = null; } - const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({ - // setup config options for date-picker - selectableDatesSetting: "custom", - initialViewRowsToShow: 5, - customSelectableDatesCallback: getAvailableDates, - preSelectedDate: preSelectedDate, - }); - // TODO - ONCE A DATEPICKER VERSION IS FINALIZED, - // MAKE SURE THE ABOVE METHOD loadInitialData POINTS TO THE RIGHT FILE + // Set up promises + const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + + const alertReasonsPromise = locationAlerts.methods.loadInitialData( + store.getters.order.serviceLocation.zipCodeCtu, + store.getters.order.serviceLocation.provider?.address?.zipCodeCtu + ); + + // While Pricing By Day Experiment is ongoing, there are two different datePickers, but only one will load + let datePickerInitialDataPromise; + if (isPricingByDayExperiment) { + datePickerInitialDataPromise = await datePickerForPricingByDay.methods.loadInitialData({ + // setup config options for date-picker + selectableDatesSetting: "custom", + initialViewRowsToShow: 5, + customSelectableDatesCallback: getAvailableDates, + preSelectedDate: preSelectedDate, + baseDayPrice: baseDayPrice, + pricingByDayUpcharge: pricingByDayUpcharge, + }); + } else { + datePickerInitialDataPromise = await datePicker.methods.loadInitialData({ + // setup config options for date-picker + selectableDatesSetting: "custom", + initialViewRowsToShow: 5, + customSelectableDatesCallback: getAvailableDates, + preSelectedDate: preSelectedDate, + }); + } const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging( storeActions.GET_MOBILE_PREMIUM_FEE, null, "schedule" ); - const premiumFeeWithPricePromise = premiumFeePromise.then((result) => { if (result.data) { return baseMixin.methods.dispatchStoreActionWithLogging( @@ -264,11 +345,6 @@ export default { } }); - const alertReasonsPromise = locationAlerts.methods.loadInitialData( - store.getters.order.serviceLocation.zipCodeCtu, - store.getters.order.serviceLocation.provider?.address?.zipCodeCtu - ); - // Settle promises and get results const promiseResultMap = [ { @@ -295,6 +371,7 @@ export default { next((vm) => { vm.setCmsContent(resultMap.cmsContent); vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData); + vm.$refs.datePickerForPricingByDay.initializeComponent(resultMap.datePickerInitialData); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse; vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice @@ -302,6 +379,12 @@ export default { : null; vm.updateFooterButtonText(vm.selectedTimeSlotInfo); vm.setDisplayWaitList(); + vm.pricingByDayUpchargeLineItem = pricingByDayUpchargeLineItem; + vm.includePricingByDaySurcharge = includePricingByDaySurcharge; + vm.isPricingByDayExperiment = isPricingByDayExperiment; + vm.baseDayPrice = baseDayPrice; + vm.pricingByDayUpcharge = pricingByDayUpcharge; + vm.showPricingByDay = showPricingByDay; }); }, computed: { @@ -324,13 +407,6 @@ export default { (selectableDate) => selectableDate.date === this.selectedDate ); }, - isPricingByDayExperiment() { - return ( - experimentMixin.methods - .getSettingValue(experimentSettings.PRICING_BY_DAY) - ?.toLowerCase() === "true" - ); - }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -377,6 +453,7 @@ export default { this.appointmentType, this.$store.getters.order.serviceLocation.provider.providerNumber ); + // ADD API CALL RESULTS TO EXISTING DATE DATA this.selectableDatesData.days = this.selectableDatesData.days.concat( newShopTimeSlots.days @@ -555,6 +632,28 @@ export default { updateSupportingItems() { const supportingItems = this.getSupportingItems(); + // if we have a pricing by day upcharge, then save/update supporting items with it + const pricingByDayUpchargeFeeIndex = supportingItems?.findIndex( + (item) => item.partType == PRICING_BY_DAY_UPCHARGE_FEE_PART_TYPE + ); + + if (this.includePricingByDaySurcharge) { + if (pricingByDayUpchargeFeeIndex > -1) { + supportingItems[pricingByDayUpchargeFeeIndex].laborAmount = + this.pricingByDayUpchargeLineItem.laborAmount; + supportingItems[pricingByDayUpchargeFeeIndex].sellingPrice = + this.pricingByDayUpchargeLineItem.sellingPrice; + supportingItems[pricingByDayUpchargeFeeIndex].kitPrice = + this.pricingByDayUpchargeLineItem.kitPrice; + } else { + supportingItems.push(this.pricingByDayUpchargeLineItem); + } + } else { + if (pricingByDayUpchargeFeeIndex >= 0) { + supportingItems.splice(pricingByDayUpchargeFeeIndex, 1); + } + } + // if we have a premium fee(early bird), then save/update supporting items if ( this.appointmentType === AppointmentTypeStrings.MOBILE && @@ -564,7 +663,7 @@ export default { (item) => item.partType == PREMIUM_FEE_PART_TYPE ); - if (premiumFeeIndex >= 0) { + if (premiumFeeIndex > -1) { supportingItems[premiumFeeIndex].laborAmount = this.mobilePremiumAppointmentFee.laborAmount; supportingItems[premiumFeeIndex].sellingPrice = @@ -574,12 +673,6 @@ export default { } else { supportingItems.push(this.mobilePremiumAppointmentFee); } - - this.dispatchStoreAction( - this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, - supportingItems, - false - ); } else { if (!supportingItems) { return; @@ -592,17 +685,28 @@ export default { if (removePremiumFeeIndex >= 0) { supportingItems.splice(removePremiumFeeIndex, 1); - this.dispatchStoreAction( - this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, - supportingItems, - false - ); } } + + this.dispatchStoreAction( + this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, + supportingItems, + false + ); }, handleWaitListRequested(value) { this.waitListRequested = value; }, + handleDateClicked(date) { + // do something to mark this as upcharge day or not... + if (date.isPricingByDayUpchargeDay) { + this.includePricingByDaySurcharge = true; + } else { + this.includePricingByDaySurcharge = false; + } + + this.openInshopTimeSlotsModal(); + }, }, watch: { selectedDate(newValue, oldValue) { diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index 1983933ea..6fd6075ab 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -110,6 +110,25 @@ export default { }); return filteredLineItems; }, + filterOutCertainPartTypesOrNumbers( + lineItemsArray, + { partTypesToRemove = [], partNumbersToRemove = [] } + ) { + // lineItems s/b an ARRAY here + if (!Array.isArray(lineItemsArray)) return; + + partTypesToRemove.forEach((partType) => { + lineItemsArray = lineItemsArray.filter((item) => { + return !item?.partType?.includes(partType); + }); + }); + partNumbersToRemove.forEach((partNumber) => { + lineItemsArray = lineItemsArray.filter((item) => { + return !item?.partNumber?.includes(partNumber); + }); + }); + return lineItemsArray; + }, filterOutServicePackageDiscountPart(lineItems) { const filteredLineItems = lineItems?.filter((item) => { return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT); @@ -133,75 +152,18 @@ export default { return totalPrice; }, getTotalLineItemPrice(lineItem, includeTax) { + const kitPrice = lineItem.kitPrice ?? 0; + const laborAmount = lineItem.laborAmount ?? 0; + const sellingPrice = lineItem.sellingPrice ?? 0; + const salesTax = lineItem.salesTax ?? 0; + if (includeTax) { - return ( - lineItem.kitPrice + - lineItem.laborAmount + - lineItem.sellingPrice + - lineItem.salesTax - ); + return kitPrice + laborAmount + sellingPrice + salesTax; } else { - return lineItem.kitPrice + lineItem.laborAmount + lineItem.sellingPrice; + return kitPrice + laborAmount + sellingPrice; } }, - getDisplayAmountDue(lineItems) { - return this.getAmountDue(lineItems).toLocaleString("en-US", { - style: "currency", - currency: "USD", - }); - }, - getAmountDue(lineItems, includeTax = true) { - var amountDue = 0; - if (lineItems?.glassParts) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItems.glassParts, - includeTax - ); - } - if (lineItems?.supportingItems) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItems.supportingItems, - includeTax - ); - } - - const order = this.hasSubmittedOrder() ? this.getSubmittedOrder() : store.getters.order; - if ( - store.getters.coverageIsVerified && - !order.policy.isNoComp && - !order.policy.isItac - ) { - amountDue = order.policy.currentDeductible; - } - - if (lineItems?.vaps) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItems.vaps, - includeTax - ); - } - - if (lineItems?.promos) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItems.promos, - includeTax - ); - } - - return ((amountDue * 100) / 100).toFixed(2); - }, - getSubTotal(lineItems) { - // this is amountDue without sales tax - return this.getAmountDue(lineItems, false); - }, - getSalesTax(lineItems) { - // this is amountDue minus subTotal - return ( - ((this.getAmountDue(lineItems) - this.getSubTotal(lineItems)) * 100) / - 100 - ).toFixed(2); - }, scrollToPageTop() { const container = document.getElementsByClassName("page-container-grouped-styles")[0]; container.scrollTo({ top: 0, left: 0, behavior: "smooth" }); diff --git a/src/mixins/base-mixin.spec.js b/src/mixins/base-mixin.spec.js index a3493910e..2484c0871 100644 --- a/src/mixins/base-mixin.spec.js +++ b/src/mixins/base-mixin.spec.js @@ -144,180 +144,6 @@ describe("baseMixin.js", () => { expect(mixIn.methods.dispatchStoreActionWithLogging).toBeCalled(); }); - - test("getAmountDue for verified deductible should be deductible plus any lineitems", () => { - const mixIn = getMixInInstance({}); - const lineItems = { - glassParts: [], - supportingItems: [], - vaps: [ - { - cartItemType: "FRONT WIPERS", - description: 'WIPER BLADE STANDARD 18"', - kitPrice: 0, - laborAmount: 0, - partNumber: "WB18", - partType: "FRONT WIPER", - salesTax: 2.0, - sellingPrice: 30.0, - }, - { - cartItemType: "FRONT WIPERS", - description: 'WIPER BLADE STANDARD 26"', - kitPrice: 0, - laborAmount: 0, - partNumber: "WB26", - partType: "FRONT WIPER", - salesTax: 1.0, - sellingPrice: 20.0, - }, - ], - promos: [], - }; - - const payment = { insuranceCoverage: { isVerified: false } }; - const policy = { isNoComp: false, isItac: false, currentDeductible: 250 }; - - store.getters = { - payment: payment, - policy: policy, - order: { - payment: payment, - policy: policy, - }, - hasSubmittedOrder: false, - }; - - var amtDue = mixIn.methods.getAmountDue(lineItems); - - expect(amtDue).toEqual("53.00"); - }); - - test("getAmountDue should function with a submitted order", () => { - const mixIn = getMixInInstance({}); - const lineItems = { - glassParts: [], - supportingItems: [], - vaps: [ - { - cartItemType: "FRONT WIPERS", - description: 'WIPER BLADE STANDARD 18"', - kitPrice: 0, - laborAmount: 0, - partNumber: "WB18", - partType: "FRONT WIPER", - salesTax: 2.0, - sellingPrice: 30.0, - }, - { - cartItemType: "FRONT WIPERS", - description: 'WIPER BLADE STANDARD 26"', - kitPrice: 0, - laborAmount: 0, - partNumber: "WB26", - partType: "FRONT WIPER", - salesTax: 1.0, - sellingPrice: 20.0, - }, - ], - promos: [], - }; - - const payment = { insuranceCoverage: { isVerified: false } }; - const policy = { isNoComp: false, isItac: false, currentDeductible: 250 }; - - mixIn.methods.hasSubmittedOrder = jest.fn().mockReturnValue(true); - mixIn.methods.getSubmittedOrder = jest.fn().mockReturnValue({ - payment: payment, - policy: policy, - }); - - store.getters = { - payment: {}, - policy: {}, - order: {}, - }; - - var amtDue = mixIn.methods.getAmountDue(lineItems); - - expect(amtDue).toEqual("53.00"); - }); - - test("getAmountDue for cash should sum lineitems", () => { - const mixIn = getMixInInstance({}); - const lineItems = { - glassParts: [ - { - partNumber: "FW06143GTYN", - description: - "solar, heads-up display, third visor frit, soundproofing, rain/light sensor, lane departure warning system", - color: "Green Tint", - partType: "WINDSHIELD", - canSafeliteRecalibrate: true, - requiresRecalibration: true, - requiresCapabilityQuestions: false, - recalibrationType: "STATIC", - childParts: [ - { - partNumber: "RS 101 PAD", - kitPrice: 0, - laborAmount: 0, - salesTax: 5.0, - sellingPrice: 5.0, - }, - ], - id: "73e80976-79be-455a-b4e7-f9b058b61749", - kitPrice: 0, - laborAmount: 60, - salesTax: 20.0, - sellingPrice: 1000.0, - }, - ], - supportingItems: [], - vaps: [ - { - cartItemType: "FRONT WIPERS", - description: 'WIPER BLADE STANDARD 18"', - kitPrice: 0, - laborAmount: 0, - partNumber: "WB18", - partType: "FRONT WIPER", - salesTax: 2.0, - sellingPrice: 30.0, - }, - { - cartItemType: "FRONT WIPERS", - description: 'WIPER BLADE STANDARD 26"', - kitPrice: 0, - laborAmount: 0, - partNumber: "WB26", - partType: "FRONT WIPER", - salesTax: 1.0, - sellingPrice: 20.0, - }, - ], - promos: [], - }; - - const payment = { isInsurance: null, insuranceCoverage: { isVerified: null } }; - const policy = { isNoComp: false, isItac: false, currentDeductible: 0 }; - - mixIn.methods.hasSubmittedOrder = jest.fn().mockReturnValue(false); - - store.getters = { - payment: payment, - policy: policy, - order: { - payment: payment, - policy: policy, - }, - hasSubmittedOrder: false, - }; - - var amtDue = mixIn.methods.getAmountDue(lineItems); - - expect(amtDue).toEqual("1143.00"); - }); }); function getMixInInstance({ isDispatchSuccess = true }) { diff --git a/src/store/index.js b/src/store/index.js index a7e7c6ae9..1a0a18d7a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1749,6 +1749,18 @@ export const actions = { pageNameToLog: pageNameToLog, }); }, + async getPricingByDayUpchargePart(context, { pageNameToLog }) { + return await globalMethods + .callHttpClient({ + method: endpoints.GetPricingByDayUpchargePart.method, + endpoint: endpoints.GetPricingByDayUpchargePart.url, + logApiCall: true, + pageNameToLog: pageNameToLog, + }) + .then((response) => { + return response; + }); + }, getServicePackageDiscountPart(context, { pageNameToLog }) { const damage = context.getters.damage; const damageType = damage.isRepair ? "Repair" : "Replace";