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..c94f10a17 100644 --- a/src/experiment-components/date-picker-for-pricing-by-day.vue +++ b/src/experiment-components/date-picker-for-pricing-by-day.vue @@ -1,5 +1,5 @@ - + :baseDayPrice="baseDayPrice" + :pricingByDayUpcharge="pricingByDayUpcharge" + :showPricingByDay="showPricingByDay" + :isPricingByDayExperiment="isPricingByDayExperiment" + @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, using the updated datePicker + const datePickerInitialDataPromise = + await datePickerForPricingByDay.methods.loadInitialData({ + // setup config options for date-picker + selectableDatesSetting: "custom", + initialViewRowsToShow: 5, + customSelectableDatesCallback: getAvailableDates, + preSelectedDate: preSelectedDate, + baseDayPrice: baseDayPrice, + pricingByDayUpcharge: pricingByDayUpcharge, + }); 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 +324,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 = [ { @@ -302,6 +357,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 +385,6 @@ export default { (selectableDate) => selectableDate.date === this.selectedDate ); }, - isPricingByDayExperiment() { - return ( - experimentMixin.methods - .getSettingValue(experimentSettings.PRICING_BY_DAY) - ?.toLowerCase() === "true" - ); - }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -377,6 +431,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 @@ -512,11 +567,13 @@ export default { false ); - this.dispatchStoreAction( - this.storeActions.SAVE_WAITLIST_REQUESTED, - this.waitListRequested, - false - ); + if (this.waitListRequested !== null && this.waitListRequested !== undefined) { + this.dispatchStoreAction( + this.storeActions.SAVE_WAITLIST_REQUESTED, + this.waitListRequested, + false + ); + } this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); }, @@ -553,6 +610,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 && @@ -562,7 +641,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 = @@ -572,12 +651,6 @@ export default { } else { supportingItems.push(this.mobilePremiumAppointmentFee); } - - this.dispatchStoreAction( - this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, - supportingItems, - false - ); } else { if (!supportingItems) { return; @@ -590,17 +663,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) { @@ -629,7 +713,6 @@ export default { funnelSubHeader, Form, loadingModal, - datePicker, datePickerForPricingByDay, locationAlerts, timeSlotModalQuestion, diff --git a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue index 40c3926ba..82e11ea41 100644 --- a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue +++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue @@ -39,6 +39,13 @@ v-model="waitListRequested" @click="waitListChecked" /> +
+ + +
{ + this.scrollToSuccessMessage(); + }); + } + }, modelValue: { handler(newValue) { this.handleChange(newValue); @@ -583,10 +609,37 @@ export default { margin-bottom: -8px; .ui-checkbox { padding-left: 0px; + input { + border-radius: 4px; + box-shadow: 0px 1px 4px 0px #00000033; + } } .form-check-input { margin-left: 0px; } } + .waitlist-success { + background-color: #ecf5e9; + display: flex; + align-items: flex-start; + border: 1px solid #0c7e47; + border-radius: 5px; + font-size: $h5-font-size; + padding: 12px 16px; + margin-bottom: -8px; + .success-text { + margin-left: 8px; + strong { + font-weight: 600; + letter-spacing: 0%; + } + } + .success-image { + margin-top: 4.5px; + width: 16px; + height: 16px; + color: #0c7e47; + } + } } 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";