Merge branch 'release/2025.03.27' into origin/feature/CASH-400
This commit is contained in:
commit
c7b7cfc282
26 changed files with 631 additions and 398 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export default {
|
|||
},
|
||||
closeModal() {
|
||||
const modal = Modal.getInstance(document.getElementById(this.modalId));
|
||||
modal.hide();
|
||||
modal?.hide();
|
||||
this.$emit("isModalOpened", false);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="date-picker text-center" :class="calendarViewDirection">
|
||||
<div class="date-picker text-center" :class="`${calendarViewDirection} ${isPricingByDayClass}`">
|
||||
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
||||
<legend class="sr-only">Select a day and time</legend>
|
||||
<div
|
||||
|
|
@ -66,12 +66,17 @@
|
|||
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()}`" />
|
||||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
||||
<span>{{ date.dateNum.toString() }}</span>
|
||||
<span
|
||||
v-if="isPricingByDayExperiment && showPricingByDay && date.isSelectable"
|
||||
class="price">
|
||||
{{ date.priceString }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -103,6 +108,7 @@ import {
|
|||
TIMINGFUNC_MAP,
|
||||
BUFFER_OFFSET,
|
||||
MONTHS_OF_YEAR,
|
||||
PREMIUM_DAY_INDEXES,
|
||||
} from "@/digital-components/date-picker/mixins/constants";
|
||||
import {
|
||||
selectableDaysOptions,
|
||||
|
|
@ -156,6 +162,10 @@ export default {
|
|||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showPricingByDay: Boolean,
|
||||
baseDayPrice: Number,
|
||||
pricingByDayUpcharge: Number,
|
||||
isPricingByDayExperiment: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -207,6 +217,9 @@ export default {
|
|||
if (this.selectableDatesSetting === "custom") return "future";
|
||||
return "past";
|
||||
},
|
||||
isPricingByDayClass() {
|
||||
return this.isPricingByDayExperiment ? "pricing-by-day" : "";
|
||||
},
|
||||
selectedDate: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
|
|
@ -220,17 +233,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 +437,8 @@ export default {
|
|||
hideSomeDaysForInitialView: hideSomeDaysForInitialView,
|
||||
hideSecondMonth: hideSecondMonth,
|
||||
preSelectedDate: config.preSelectedDate,
|
||||
baseDayPrice: config.baseDayPrice,
|
||||
pricingByDayUpcharge: config.pricingByDayUpcharge,
|
||||
};
|
||||
return initialData;
|
||||
});
|
||||
|
|
@ -453,6 +463,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 +598,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 +619,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 +634,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);
|
||||
}
|
||||
|
|
@ -779,7 +801,7 @@ export default {
|
|||
|
||||
.grid-item {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin: 10px 3px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
|
||||
|
|
@ -814,13 +836,12 @@ export default {
|
|||
}
|
||||
|
||||
.month-year {
|
||||
grid-area: 1 / 1 / 2 / 8;
|
||||
grid-area: 1 / 1 / 2 / 5;
|
||||
text-transform: uppercase;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.75px;
|
||||
}
|
||||
.legend {
|
||||
display: none;
|
||||
grid-area: 1 / 5 / 2 / 8;
|
||||
.legend-circle {
|
||||
border-radius: 50%;
|
||||
|
|
@ -839,27 +860,43 @@ export default {
|
|||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
opacity: 1;
|
||||
transition:
|
||||
height ease 250ms,
|
||||
opacity ease 250ms;
|
||||
|
||||
color: $gray-500;
|
||||
background-color: $gray-100;
|
||||
|
||||
input[type="radio"] {
|
||||
position: absolute; //override bootstrap
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&:checked + label {
|
||||
background: $blue-100;
|
||||
border: 2px solid $blue;
|
||||
border-radius: 4px;
|
||||
&:focus-visible + label {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
|
||||
&:focus + label,
|
||||
&:checked:focus + label {
|
||||
box-shadow:
|
||||
0 0 0 3px #fff,
|
||||
0 0 0 5.5px #1574a1;
|
||||
background-color: $blue;
|
||||
color: $white;
|
||||
&.current-day {
|
||||
&:after {
|
||||
background-color: $white;
|
||||
}
|
||||
.first-day {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + label {
|
||||
color: $white;
|
||||
background: $blue;
|
||||
&:after {
|
||||
background-color: $white;
|
||||
}
|
||||
|
|
@ -878,28 +915,17 @@ export default {
|
|||
z-index: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 2.5rem;
|
||||
font-size: 8px;
|
||||
background: pink;
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 2.5rem;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
justify-content: flex-start;
|
||||
padding: 0.25rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
|
||||
span {
|
||||
margin-top: 0.25rem;
|
||||
&.small {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-550;
|
||||
|
|
@ -931,17 +957,23 @@ export default {
|
|||
}
|
||||
|
||||
&.selectable-day {
|
||||
background-color: $white;
|
||||
label {
|
||||
color: $blue;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
background-color: $blue-100;
|
||||
border: 1px solid $blue;
|
||||
min-width: 2.5rem;
|
||||
width: 2.5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
&.unavailable-day {
|
||||
label {
|
||||
color: $gray-500;
|
||||
background-color: $gray-100;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.unavailable-day:not(&.sunday) {
|
||||
label {
|
||||
&::before {
|
||||
|
|
@ -962,11 +994,12 @@ export default {
|
|||
|
||||
&:after {
|
||||
content: "";
|
||||
margin-top: 0.2rem;
|
||||
width: 0.25rem;
|
||||
height: 0.25rem;
|
||||
border-radius: 50%;
|
||||
background-color: $black;
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
}
|
||||
.first-day {
|
||||
color: $blue;
|
||||
|
|
@ -1057,7 +1090,83 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pricing-by-day {
|
||||
// pricing by day override styles
|
||||
.calendar-grid-container .grid-item {
|
||||
margin: 0;
|
||||
}
|
||||
.month-year {
|
||||
grid-area: 1 / 1 / 2 / 8;
|
||||
}
|
||||
.legend {
|
||||
display: none;
|
||||
}
|
||||
.radio-wrapper {
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
color: $gray-500;
|
||||
background-color: $gray-100;
|
||||
|
||||
input[type="radio"] {
|
||||
&:focus-visible + label {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:focus + label,
|
||||
&:checked:focus + label {
|
||||
box-shadow: none;
|
||||
background-color: $white;
|
||||
color: $blue;
|
||||
}
|
||||
&:checked + label {
|
||||
background: $blue-100;
|
||||
border: 2px solid $blue;
|
||||
border-radius: 4px;
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
label {
|
||||
flex-direction: column;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
justify-content: flex-start;
|
||||
padding: 0.25rem;
|
||||
|
||||
span {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
&.selectable-day {
|
||||
background-color: $white;
|
||||
label {
|
||||
background-color: $white;
|
||||
border: none;
|
||||
min-width: initial;
|
||||
width: initial;
|
||||
height: -webkit-fill-available;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.current-day {
|
||||
label:after {
|
||||
margin-top: 0.2rem;
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
93
src/helpers/pricing-helper.js
Normal file
93
src/helpers/pricing-helper.js
Normal file
|
|
@ -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];
|
||||
}
|
||||
64
src/helpers/pricing-helper.spec.js
Normal file
64
src/helpers/pricing-helper.spec.js
Normal file
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
</template>
|
||||
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||
<datePickerForPricingByDay
|
||||
v-if="isPricingByDayExperiment"
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
ref="datePicker"
|
||||
|
|
@ -27,17 +26,11 @@
|
|||
class="text-link-small"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
validationRules="date-required"
|
||||
@date-clicked="openInshopTimeSlotsModal" />
|
||||
<datePicker
|
||||
v-else
|
||||
customComponentId="dateQuestion"
|
||||
selectableDatesSetting="custom"
|
||||
ref="datePicker"
|
||||
v-model="selectedDate"
|
||||
class="text-link-small"
|
||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||
validationRules="date-required"
|
||||
@date-clicked="openInshopTimeSlotsModal" />
|
||||
:baseDayPrice="baseDayPrice"
|
||||
:pricingByDayUpcharge="pricingByDayUpcharge"
|
||||
:showPricingByDay="showPricingByDay"
|
||||
:isPricingByDayExperiment="isPricingByDayExperiment"
|
||||
@date-clicked="handleDateClicked" />
|
||||
<timeSlotModalQuestion
|
||||
ref="timeSlotModalQuestion"
|
||||
customComponentId="timeSlotModalQuestion"
|
||||
|
|
@ -84,7 +77,6 @@ import navbar from "@/fmg-components/nav-bar/nav-bar";
|
|||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||
import datePickerForPricingByDay from "@/experiment-components/date-picker-for-pricing-by-day";
|
||||
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
||||
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||
|
|
@ -103,12 +95,20 @@ import {
|
|||
convertDateStringToDate,
|
||||
sumDateString,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||
import {
|
||||
AppointmentTypeStrings,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
PRICING_BY_DAY_UPCHARGE_FEE_PART_TYPE,
|
||||
} from "@/constants/schedule-constants";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import store from "@/store";
|
||||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { getAmountDue, getPriceUpchargeByDayPart } from "@/helpers/pricing-helper.js";
|
||||
import { getItemsWithoutRecalParts } from "@/helpers/recal-helper";
|
||||
import { partNumberStrings } from "@/constants/part-number-strings";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||
|
|
@ -223,32 +223,92 @@ export default {
|
|||
mobilePremiumAppointmentFee: null,
|
||||
waitListRequested: null,
|
||||
displayWaitList: null,
|
||||
pricingByDayUpchargeLineItem: null,
|
||||
includePricingByDaySurcharge: null,
|
||||
isPricingByDayExperiment: null,
|
||||
baseDayPrice: null,
|
||||
pricingByDayUpcharge: null,
|
||||
showPricingByDay: null,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
// Get data needed for Pricing By Day
|
||||
const lineItems = deepClone(await store.getters.order.lineItems);
|
||||
const isRecalibrationOnOrder = await store.getters.isRecalibrationOnOrder;
|
||||
const shouldHideRecalibration =
|
||||
experimentMixin.methods
|
||||
.getSettingValue(experimentSettings.RECAL_PRICE_REMOVE)
|
||||
?.toLowerCase() === "true" && isRecalibrationOnOrder;
|
||||
const glassParts =
|
||||
isRecalibrationOnOrder && shouldHideRecalibration
|
||||
? getItemsWithoutRecalParts(lineItems.glassParts)
|
||||
: (lineItems.glassParts ?? []);
|
||||
const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers(
|
||||
lineItems.supportingItems,
|
||||
{ partNumbersToRemove: [partNumberStrings.RECYCLE_FEE] }
|
||||
);
|
||||
const lineItemsToBePriced = {
|
||||
glassParts: glassParts,
|
||||
supportingItems: supportingItemsWithoutFees,
|
||||
vaps: lineItems.vaps ?? [],
|
||||
promos: lineItems.promos ?? [],
|
||||
};
|
||||
const isPricingByDayExperiment =
|
||||
(await experimentMixin.methods
|
||||
.getSettingValue(experimentSettings.PRICING_BY_DAY)
|
||||
?.toLowerCase()) === "true";
|
||||
const priceString = await getAmountDue(lineItemsToBePriced, false); // pass the IncludeTax param as false
|
||||
const priceStringIntegerRoundedDown = priceString?.split(".")[0]; // same method used as getDisplayPrice() in service-package-radio used on /quote
|
||||
const baseDayPrice = parseInt(priceStringIntegerRoundedDown);
|
||||
const showPricingByDay = !store.getters.payment.isInsurance && isPricingByDayExperiment;
|
||||
const pricingByDayUpchargeLineItem = await getPriceUpchargeByDayPart();
|
||||
const pricingByDayUpcharge = await baseMixin.methods.getTotalLineItemPrice(
|
||||
pricingByDayUpchargeLineItem,
|
||||
false
|
||||
);
|
||||
|
||||
// Check to see if includePricingByDaySurcharge should already be set (based on order lineItems)
|
||||
let includePricingByDaySurcharge = false;
|
||||
if (supportingItemsWithoutFees) {
|
||||
const pricingByDayUpchargeFeeIndex = supportingItemsWithoutFees.findIndex(
|
||||
(item) => 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,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@
|
|||
v-model="waitListRequested"
|
||||
@click="waitListChecked" />
|
||||
</div>
|
||||
<div
|
||||
v-if="waitListRequested"
|
||||
class="mt-5 rounded waitlist-success"
|
||||
ref="waitlistSuccessMessage">
|
||||
<img :src="WaitListSuccessImage" class="success-image" />
|
||||
<span v-html="WaitListSuccessText" class="success-text"></span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-5 mb-2 supplemental-information"
|
||||
v-if="supplementalInformationBlock"
|
||||
|
|
@ -359,6 +366,12 @@ export default {
|
|||
displayWaitListFeature() {
|
||||
return this.displayWaitList;
|
||||
},
|
||||
WaitListSuccessImage() {
|
||||
return this.getCmsContent("WaitListSuccessWidget", "Image");
|
||||
},
|
||||
WaitListSuccessText() {
|
||||
return this.getCmsContent("WaitListSuccessWidget", "BodyText");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
|
|
@ -528,11 +541,24 @@ export default {
|
|||
waitListChecked(event) {
|
||||
this.$emit("waitListRequested-emitted", event.target.checked);
|
||||
},
|
||||
scrollToSuccessMessage() {
|
||||
const successMessageElement = this.$refs.waitlistSuccessMessage;
|
||||
if (successMessageElement) {
|
||||
successMessageElement.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
if (this.isModalOpened) this.closeModal();
|
||||
},
|
||||
watch: {
|
||||
waitListRequested(newVal) {
|
||||
if (newVal) {
|
||||
this.$nextTick(() => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue