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";