Merge branch 'release/2025.03.27' into feature/CASH-185v2
This commit is contained in:
commit
da377df9f6
30 changed files with 875 additions and 462 deletions
|
|
@ -72,6 +72,10 @@ const endpoints = {
|
||||||
url: "/parts/api/v1/parts/mobile-fee",
|
url: "/parts/api/v1/parts/mobile-fee",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
GetPricingByDayPart: {
|
||||||
|
url: "/parts/api/v1/parts/pricing-by-day-part-number",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
GetServicePackageDiscountPart: {
|
GetServicePackageDiscountPart: {
|
||||||
url: "/parts/api/v1/parts/service-package-discount",
|
url: "/parts/api/v1/parts/service-package-discount",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ const partNumberStrings = {
|
||||||
MOBILE_STATIC_RECAL_FEE: "RECAL MOBILE",
|
MOBILE_STATIC_RECAL_FEE: "RECAL MOBILE",
|
||||||
MOBILE_DUAL_RECAL_FEE: "RECAL MOBILEDUAL",
|
MOBILE_DUAL_RECAL_FEE: "RECAL MOBILEDUAL",
|
||||||
DONATION: "DONATION",
|
DONATION: "DONATION",
|
||||||
|
// Fees
|
||||||
|
RECYCLE_FEE: "RECYCLE FEE",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { partNumberStrings };
|
export { partNumberStrings };
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,18 @@ const PREMIUM_TIME_SLOT_ID_FLAG = "-PREMIUM";
|
||||||
|
|
||||||
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
|
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
|
||||||
|
|
||||||
|
// TODO; This will need updating with real PricingByDay part type
|
||||||
|
const PRICING_BY_DAY_PART_TYPE = "DISC CASHSAVE20";
|
||||||
|
|
||||||
const RouteCodeFlags = {
|
const RouteCodeFlags = {
|
||||||
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
|
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
|
||||||
OVERNIGHT_DROP_OFF: "OVERNIGHT 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_PART_TYPE,
|
||||||
|
RouteCodeFlags,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const storeActions = {
|
||||||
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
|
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
|
||||||
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
|
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
|
||||||
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
||||||
|
GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
|
||||||
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
|
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
|
||||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,50 @@ const MONTHS_OF_YEAR = [
|
||||||
"December",
|
"December",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DAYS_OF_WEEK = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
const DAYS_OF_WEEK = [
|
||||||
|
// Monday, Friday, Saturday are the pricing by day premium days
|
||||||
|
{
|
||||||
|
label: "Sunday",
|
||||||
|
cssClass: "sunday",
|
||||||
|
index: 0,
|
||||||
|
isPricingByDayUpchargeDay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Monday",
|
||||||
|
cssClass: "monday",
|
||||||
|
index: 1,
|
||||||
|
isPricingByDayUpchargeDay: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tuesday",
|
||||||
|
cssClass: "tuesday",
|
||||||
|
index: 2,
|
||||||
|
isPricingByDayUpchargeDay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Wednesday",
|
||||||
|
cssClass: "wednesday",
|
||||||
|
index: 3,
|
||||||
|
isPricingByDayUpchargeDay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Thursday",
|
||||||
|
cssClass: "thursday",
|
||||||
|
index: 4,
|
||||||
|
isPricingByDayUpchargeDay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Friday",
|
||||||
|
cssClass: "friday",
|
||||||
|
index: 5,
|
||||||
|
isPricingByDayUpchargeDay: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Saturday",
|
||||||
|
cssClass: "saturday",
|
||||||
|
index: 6,
|
||||||
|
isPricingByDayUpchargeDay: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK };
|
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK };
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ export default {
|
||||||
},
|
},
|
||||||
closeModal() {
|
closeModal() {
|
||||||
const modal = Modal.getInstance(document.getElementById(this.modalId));
|
const modal = Modal.getInstance(document.getElementById(this.modalId));
|
||||||
modal.hide();
|
modal?.hide();
|
||||||
this.$emit("isModalOpened", false);
|
this.$emit("isModalOpened", false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="textarea-question">
|
<div class="textarea-question">
|
||||||
<div class="label-wrapper d-flex flex-column mb-1" :aria-label="questionText">
|
<div class="label-wrapper d-flex flex-column mb-1 mt-1" :aria-label="questionText">
|
||||||
<!-- Wrap label and span because v-html prevents v-if from displaying if v-if <span> is inside <label>-->
|
|
||||||
<div class="d-flex mb-1">
|
|
||||||
<label :for="textAreaLabelCopy" class="fw-bold" v-html="questionText"></label>
|
|
||||||
<span v-if="!isRequired" class="fw-normal ms-1">(Optional)</span>
|
|
||||||
</div>
|
|
||||||
<textarea
|
<textarea
|
||||||
:id="textAreaLabelCopy"
|
:id="textAreaLabelCopy"
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
||||||
<legend class="sr-only">Select a day and time</legend>
|
<legend class="sr-only">Select a day and time</legend>
|
||||||
<div
|
<div
|
||||||
|
|
@ -66,12 +66,17 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="day-of-month"
|
name="day-of-month"
|
||||||
v-model="selectedDate"
|
v-model="selectedDate"
|
||||||
@click="fireDateSelectedEvent"
|
@click="fireDateSelectedEvent($event, date)"
|
||||||
@keypress.enter="fireDateSelectedEvent"
|
@keypress.enter="fireDateSelectedEvent($event, date)"
|
||||||
:value="date.inputValue"
|
:value="date.inputValue"
|
||||||
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
:id="`${month.monthLabel}-${date.dateNum.toString()}`" />
|
||||||
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
<label :for="`${month.monthLabel}-${date.dateNum.toString()}`">
|
||||||
<span>{{ date.dateNum.toString() }}</span>
|
<span>{{ date.dateNum.toString() }}</span>
|
||||||
|
<span
|
||||||
|
v-if="isPricingByDayExperiment && showPricingByDay && date.isSelectable"
|
||||||
|
class="price">
|
||||||
|
{{ date.priceString }}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,6 +108,7 @@ import {
|
||||||
TIMINGFUNC_MAP,
|
TIMINGFUNC_MAP,
|
||||||
BUFFER_OFFSET,
|
BUFFER_OFFSET,
|
||||||
MONTHS_OF_YEAR,
|
MONTHS_OF_YEAR,
|
||||||
|
DAYS_OF_WEEK,
|
||||||
} from "@/digital-components/date-picker/mixins/constants";
|
} from "@/digital-components/date-picker/mixins/constants";
|
||||||
import {
|
import {
|
||||||
selectableDaysOptions,
|
selectableDaysOptions,
|
||||||
|
|
@ -156,6 +162,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
showPricingByDay: Boolean,
|
||||||
|
pricingByDayBasePrice: Number,
|
||||||
|
pricingByDayUpcharge: Number,
|
||||||
|
isPricingByDayExperiment: Boolean,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
|
|
@ -207,6 +217,9 @@ export default {
|
||||||
if (this.selectableDatesSetting === "custom") return "future";
|
if (this.selectableDatesSetting === "custom") return "future";
|
||||||
return "past";
|
return "past";
|
||||||
},
|
},
|
||||||
|
isPricingByDayClass() {
|
||||||
|
return this.isPricingByDayExperiment ? "pricing-by-day" : "";
|
||||||
|
},
|
||||||
selectedDate: {
|
selectedDate: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue;
|
return this.modelValue;
|
||||||
|
|
@ -220,17 +233,12 @@ export default {
|
||||||
initializeComponent(initialData) {
|
initializeComponent(initialData) {
|
||||||
this.setCalendarData(initialData);
|
this.setCalendarData(initialData);
|
||||||
},
|
},
|
||||||
fireDateSelectedEvent(event) {
|
fireDateSelectedEvent(event, date) {
|
||||||
// Ignore if arrow key selected radioButton
|
// Ignore if arrow key selected radioButton
|
||||||
if (event.screenX === 0 && event.screenY === 0) {
|
if (event.screenX === 0 && event.screenY === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$emit("date-clicked");
|
this.$emit("date-clicked", date);
|
||||||
},
|
|
||||||
fireDateSelectedEvent2(date) {
|
|
||||||
console.log("date: ", date);
|
|
||||||
this.selectedDate = date;
|
|
||||||
this.$emit("date-clicked");
|
|
||||||
},
|
},
|
||||||
getWeekStartDate(dateString) {
|
getWeekStartDate(dateString) {
|
||||||
const date = convertDateStringToDate(dateString);
|
const date = convertDateStringToDate(dateString);
|
||||||
|
|
@ -453,6 +461,8 @@ export default {
|
||||||
initialViewEndDate: config.initialViewEndDate,
|
initialViewEndDate: config.initialViewEndDate,
|
||||||
hideSecondMonth: hideSecondMonth,
|
hideSecondMonth: hideSecondMonth,
|
||||||
preSelectedDate: config.preSelectedDate,
|
preSelectedDate: config.preSelectedDate,
|
||||||
|
pricingByDayBasePrice: config.pricingByDayBasePrice,
|
||||||
|
pricingByDayUpcharge: config.pricingByDayUpcharge,
|
||||||
};
|
};
|
||||||
if (direction === "future") {
|
if (direction === "future") {
|
||||||
// first 0, then 1
|
// first 0, then 1
|
||||||
|
|
@ -586,6 +596,17 @@ export default {
|
||||||
("0" + monthNum).slice(-2) +
|
("0" + monthNum).slice(-2) +
|
||||||
"-" +
|
"-" +
|
||||||
("0" + i).slice(-2);
|
("0" + i).slice(-2);
|
||||||
|
const dayIndex = convertDateStringToDate(dateString).getDay();
|
||||||
|
const dayObject = DAYS_OF_WEEK[dayIndex];
|
||||||
|
const isSelectable =
|
||||||
|
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
const isPricingByDayUpchargeDay = dayObject.isPricingByDayUpchargeDay;
|
||||||
|
const displayPrice = isPricingByDayUpchargeDay
|
||||||
|
? options.pricingByDayBasePrice + options.pricingByDayUpcharge
|
||||||
|
: options.pricingByDayBasePrice;
|
||||||
|
const priceString = "$" + displayPrice;
|
||||||
|
|
||||||
if (offset === 0 && i === this.todayDateNum) {
|
if (offset === 0 && i === this.todayDateNum) {
|
||||||
dayClasses += " current-day";
|
dayClasses += " current-day";
|
||||||
|
|
@ -596,8 +617,8 @@ export default {
|
||||||
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
||||||
dayClasses += " unavailable-day";
|
dayClasses += " unavailable-day";
|
||||||
}
|
}
|
||||||
if (convertDateStringToDate(dateString).getDay() === 0) {
|
if (dayIndex === 0) {
|
||||||
dayClasses += " sunday";
|
dayClasses += " " + dayObject.cssClass;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
this.hideSomeDaysForInitialView &&
|
this.hideSomeDaysForInitialView &&
|
||||||
|
|
@ -611,10 +632,9 @@ export default {
|
||||||
dateNum: i,
|
dateNum: i,
|
||||||
dayClasses: dayClasses,
|
dayClasses: dayClasses,
|
||||||
inputValue: dateString,
|
inputValue: dateString,
|
||||||
isSelectable:
|
priceString: priceString,
|
||||||
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
|
isSelectable: isSelectable,
|
||||||
? true
|
isPricingByDayUpchargeDay: isPricingByDayUpchargeDay,
|
||||||
: false,
|
|
||||||
};
|
};
|
||||||
dates.push(dateObject);
|
dates.push(dateObject);
|
||||||
}
|
}
|
||||||
|
|
@ -779,7 +799,7 @@ export default {
|
||||||
|
|
||||||
.grid-item {
|
.grid-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 10px 3px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
|
|
@ -814,13 +834,12 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.month-year {
|
.month-year {
|
||||||
grid-area: 1 / 1 / 2 / 8;
|
grid-area: 1 / 1 / 2 / 5;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: 0.75px;
|
letter-spacing: 0.75px;
|
||||||
}
|
}
|
||||||
.legend {
|
.legend {
|
||||||
display: none;
|
|
||||||
grid-area: 1 / 5 / 2 / 8;
|
grid-area: 1 / 5 / 2 / 8;
|
||||||
.legend-circle {
|
.legend-circle {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
@ -839,27 +858,43 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
width: 100%;
|
height: 1.5rem;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition:
|
transition:
|
||||||
height ease 250ms,
|
height ease 250ms,
|
||||||
opacity ease 250ms;
|
opacity ease 250ms;
|
||||||
|
|
||||||
color: $gray-500;
|
|
||||||
background-color: $gray-100;
|
|
||||||
|
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
position: absolute; //override bootstrap
|
position: absolute; //override bootstrap
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
&:checked + label {
|
&:focus-visible + label {
|
||||||
background: $blue-100;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
border: 2px solid $blue;
|
}
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
|
&: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 {
|
&:after {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
|
|
@ -878,28 +913,17 @@ export default {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
width: 2.5rem;
|
|
||||||
font-size: 8px;
|
|
||||||
background: pink;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 2.5rem;
|
min-width: 2.5rem;
|
||||||
height: 3rem;
|
width: 2.5rem;
|
||||||
width: 100%;
|
height: 2.5rem;
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 0.25rem;
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-top: 0.25rem;
|
|
||||||
&.small {
|
&.small {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: $gray-550;
|
color: $gray-550;
|
||||||
|
|
@ -931,17 +955,23 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selectable-day {
|
&.selectable-day {
|
||||||
background-color: $white;
|
|
||||||
label {
|
label {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
cursor: pointer;
|
background-color: $blue-100;
|
||||||
|
border: 1px solid $blue;
|
||||||
span {
|
min-width: 2.5rem;
|
||||||
text-decoration: underline;
|
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) {
|
&.unavailable-day:not(&.sunday) {
|
||||||
label {
|
label {
|
||||||
&::before {
|
&::before {
|
||||||
|
|
@ -962,11 +992,12 @@ export default {
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
margin-top: 0.2rem;
|
|
||||||
width: 0.25rem;
|
width: 0.25rem;
|
||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
}
|
}
|
||||||
.first-day {
|
.first-day {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
|
|
@ -1057,7 +1088,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 {
|
.btn-link {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -169,10 +169,16 @@ import { deepClone } from "@/helpers/object-helper";
|
||||||
import {
|
import {
|
||||||
getHighestFullySatisfiedTier,
|
getHighestFullySatisfiedTier,
|
||||||
getPackageContents,
|
getPackageContents,
|
||||||
getDiscountPackageName,
|
getDiscountedPackageName,
|
||||||
} from "@/helpers/service-package-helper";
|
} from "@/helpers/service-package-helper";
|
||||||
import { getPromoCodeWithoutBundleIdentifier } from "@/helpers/promotions-helper";
|
import { getPromoCodeWithoutBundleIdentifier } from "@/helpers/promotions-helper";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
import {
|
||||||
|
getDisplayAmountDue,
|
||||||
|
getAmountDue,
|
||||||
|
getSubTotal,
|
||||||
|
getSalesTax,
|
||||||
|
} from "@/helpers/pricing-helper.js";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
|
@ -467,7 +473,7 @@ export default {
|
||||||
const discountServicePackage = experimentMixin.methods.getSettingValue(
|
const discountServicePackage = experimentMixin.methods.getSettingValue(
|
||||||
experimentSettings.PROMO_ON_PACKAGE
|
experimentSettings.PROMO_ON_PACKAGE
|
||||||
);
|
);
|
||||||
return getDiscountPackageName(discountServicePackage);
|
return getDiscountedPackageName(discountServicePackage);
|
||||||
},
|
},
|
||||||
servicePackageTitleWidget() {
|
servicePackageTitleWidget() {
|
||||||
const servicePackageNames = this.getCmsContent(
|
const servicePackageNames = this.getCmsContent(
|
||||||
|
|
@ -1082,8 +1088,8 @@ export default {
|
||||||
subTotal() {
|
subTotal() {
|
||||||
if (!this.lineItems || this.lineItems.length < 1) return;
|
if (!this.lineItems || this.lineItems.length < 1) return;
|
||||||
return this.isInsurance || !this.shouldHideRecalibration
|
return this.isInsurance || !this.shouldHideRecalibration
|
||||||
? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
|
? getSubTotal(this.lineItems) // calculate with recal (if on order)
|
||||||
: baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculate without recal
|
: getSubTotal(this.lineItemsWithoutRecal); // calculate without recal
|
||||||
},
|
},
|
||||||
salesTax() {
|
salesTax() {
|
||||||
if (!this.lineItems || this.lineItems.length < 1) return;
|
if (!this.lineItems || this.lineItems.length < 1) return;
|
||||||
|
|
@ -1098,15 +1104,15 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.isInsurance || !this.shouldHideRecalibration
|
return this.isInsurance || !this.shouldHideRecalibration
|
||||||
? baseMixin.methods.getSalesTax(this.lineItems) // calculate with recal (if on order)
|
? getSalesTax(this.lineItems) // calculate with recal (if on order)
|
||||||
: baseMixin.methods.getSalesTax(this.lineItemsWithoutRecal); // calculated without recal
|
: getSalesTax(this.lineItemsWithoutRecal); // calculated without recal
|
||||||
},
|
},
|
||||||
amountDue() {
|
amountDue() {
|
||||||
if (!this.lineItems || this.lineItems.length < 1) return;
|
if (!this.lineItems || this.lineItems.length < 1) return;
|
||||||
if (this.showAsPaid) return 0;
|
if (this.showAsPaid) return 0;
|
||||||
return this.isInsurance || !this.shouldHideRecalibration
|
return this.isInsurance || !this.shouldHideRecalibration
|
||||||
? baseMixin.methods.getAmountDue(this.lineItems) // calculate with recal (if on order)
|
? getAmountDue(this.lineItems) // calculate with recal (if on order)
|
||||||
: baseMixin.methods.getAmountDue(this.lineItemsWithoutRecal); // calculated without recal
|
: getAmountDue(this.lineItemsWithoutRecal); // calculated without recal
|
||||||
},
|
},
|
||||||
amountPaid() {
|
amountPaid() {
|
||||||
if (!this.showAsPaid) {
|
if (!this.showAsPaid) {
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export default {
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||||
this.modal.closeModal();
|
this.modal?.closeModal();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
buttonText() {
|
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 getPricingByDayPartWithPrice(pageNameToLog) {
|
||||||
|
// Get the Pricing By Day Part
|
||||||
|
const basePriceByDayPart = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||||
|
storeActions.GET_PRICING_BY_DAY_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) {
|
export function getItemsWithoutRecalParts(lineItemsArray) {
|
||||||
const copy = deepClone(lineItems);
|
if (!lineItemsArray || !Array.isArray(lineItemsArray)) return null;
|
||||||
|
const firstLevelFiltered = deepClone(lineItemsArray).filter((li) => !isRecalPart(li));
|
||||||
const firstLevelFiltered = copy.filter((li) => !isRecalPart(li));
|
|
||||||
|
|
||||||
const childrenFiltered = firstLevelFiltered.map((li) => {
|
const childrenFiltered = firstLevelFiltered.map((li) => {
|
||||||
if (li.childParts && li.childParts.length > 0) {
|
if (li.childParts && li.childParts.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
|
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
|
||||||
import { packageNames } from "@/constants/package-names";
|
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) {
|
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
|
||||||
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
|
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
|
||||||
|
|
@ -239,7 +245,7 @@ export function getPackageNameByType(packageType) {
|
||||||
return package_names[packageType] || null;
|
return package_names[packageType] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDiscountPackageName(discountPackage) {
|
export function getDiscountedPackageName(discountPackage) {
|
||||||
const package_names = {
|
const package_names = {
|
||||||
ECON: packageNames.TIER_ONE,
|
ECON: packageNames.TIER_ONE,
|
||||||
STANDARD: packageNames.TIER_TWO,
|
STANDARD: packageNames.TIER_TWO,
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,80 @@
|
||||||
// Components
|
import { shallowMount, mount } from "@vue/test-utils";
|
||||||
import customerDetails from "@/layouts/customer-details/customer-details.vue";
|
import CustomerDetails from "./customer-details.vue";
|
||||||
|
import TechNotes from "./tech-notes/tech-notes.vue";
|
||||||
|
import TextboxQuestion from "@/digital-components/textbox-question/textbox-question.vue";
|
||||||
|
import PhoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question.vue";
|
||||||
|
import CheckboxQuestion from "@/digital-components/checkbox-question/checkbox-question.vue";
|
||||||
|
import TextBlock from "@/digital-components/text-block/text-block.vue";
|
||||||
|
import FunnelHeader from "@/fmg-components/funnel-header/funnel-header.vue";
|
||||||
|
import FunnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header.vue";
|
||||||
|
import Navbar from "@/fmg-components/nav-bar/nav-bar.vue";
|
||||||
|
|
||||||
// Supporting Files
|
const mockMixin = {
|
||||||
import { shallowMount } from "@vue/test-utils";
|
methods: {
|
||||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
getCmsContent() {
|
||||||
|
return "Mocked CMS Content";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Mock our module for promises.
|
const mockStoreActions = {
|
||||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
SAVE_CUSTOMER_DETAILS: "SAVE_CUSTOMER_DETAILS",
|
||||||
settleAllPromises: jest.fn(),
|
SAVE_SERVICE_LOCATION_TECH_NOTES: "SAVE_SERVICE_LOCATION_TECH_NOTES",
|
||||||
}));
|
SAVE_PAYMENT_METHOD_CHOICE: "SAVE_PAYMENT_METHOD_CHOICE",
|
||||||
|
};
|
||||||
|
|
||||||
describe("customer-details.vue", () => {
|
describe("CustomerDetails.vue", () => {
|
||||||
describe("navigation", () => {
|
let wrapper;
|
||||||
test("if the back button is clicked, navigate back", async () => {
|
|
||||||
// Arrange
|
|
||||||
const { wrapper } = setupMocks();
|
|
||||||
|
|
||||||
// Act
|
beforeEach(() => {
|
||||||
await wrapper.vm.backButtonAction();
|
wrapper = mount(CustomerDetails, {
|
||||||
|
global: {
|
||||||
// Assert
|
components: {
|
||||||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
TechNotes,
|
||||||
});
|
TextboxQuestion,
|
||||||
|
PhoneNumberQuestion,
|
||||||
test("if the continue button is clicked, navigate forward", async () => {
|
CheckboxQuestion,
|
||||||
// Arrange
|
TextBlock,
|
||||||
const { wrapper } = setupMocks();
|
FunnelHeader,
|
||||||
|
FunnelSubHeader,
|
||||||
// Act
|
Navbar,
|
||||||
await wrapper.vm.forwardButtonAction();
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
// Assert
|
mocks: {
|
||||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
storeActions: mockStoreActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
firstName: "John",
|
||||||
|
lastName: "Doe",
|
||||||
|
emailAddress: "john.doe@example.com",
|
||||||
|
phoneNumber: "1234567890",
|
||||||
|
isSmsOptIn: true,
|
||||||
|
techNotes: "Initial Tech Notes",
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should render the CustomerDetails component", () => {
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should render all child components", () => {
|
||||||
|
expect(wrapper.findComponent(TechNotes).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(TextboxQuestion).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(PhoneNumberQuestion).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(CheckboxQuestion).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(TextBlock).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(FunnelHeader).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(FunnelSubHeader).exists()).toBe(true);
|
||||||
|
expect(wrapper.findComponent(Navbar).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should pass the correct props to TechNotes", () => {
|
||||||
|
const techNotes = wrapper.findComponent(TechNotes);
|
||||||
|
expect(techNotes.props("modelValue")).toBe("Initial Tech Notes");
|
||||||
|
expect(techNotes.props("textAreaLabelCopy")).toBe("Mocked CMS Content");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupMocks() {
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
customerDetails,
|
|
||||||
getMountOptions({
|
|
||||||
router: {
|
|
||||||
navigate: jest.fn(),
|
|
||||||
navigate: jest.fn(),
|
|
||||||
navigateWithSaving: jest.fn(),
|
|
||||||
navigateWithoutSaving: jest.fn(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return { wrapper };
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,7 @@
|
||||||
cmsWidgetName="TextMeQuestionWidget"
|
cmsWidgetName="TextMeQuestionWidget"
|
||||||
v-model="isSmsOptIn" />
|
v-model="isSmsOptIn" />
|
||||||
|
|
||||||
<textareaQuestion
|
<techNotes v-model="techNotes" :textAreaLabelCopy="textAreaLabelCopy" />
|
||||||
class="mb-4"
|
|
||||||
v-model="techNotes"
|
|
||||||
cmsWidgetName="TextAreaContentWidget"
|
|
||||||
textAreaLabelCopy="Notes for your technician"
|
|
||||||
maxLength="150" />
|
|
||||||
|
|
||||||
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
|
||||||
|
|
||||||
<navbar
|
<navbar
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
|
@ -62,6 +55,8 @@
|
||||||
:isForwardActionDisabled="!meta.valid"
|
:isForwardActionDisabled="!meta.valid"
|
||||||
@back-clicked="backButtonAction"
|
@back-clicked="backButtonAction"
|
||||||
@ForwardClicked="forwardButtonAction" />
|
@ForwardClicked="forwardButtonAction" />
|
||||||
|
|
||||||
|
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,20 +67,18 @@
|
||||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
|
|
||||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||||
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
||||||
import textBlock from "@/digital-components/text-block/text-block";
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||||
|
import techNotes from "@/layouts/customer-details/tech-notes/tech-notes";
|
||||||
//Supporting files
|
//Supporting files
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
import { routerParams } from "@/router/router-constants/router-params";
|
|
||||||
import { required, regex } from "@/helpers/validation-rules";
|
import { required, regex } from "@/helpers/validation-rules";
|
||||||
import { Form, defineRule } from "vee-validate";
|
import { Form, defineRule } from "vee-validate";
|
||||||
import { useField, validate } from "vee-validate";
|
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
|
|
||||||
|
|
@ -199,13 +192,13 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
funnelHeader,
|
funnelHeader,
|
||||||
funnelSubHeader,
|
funnelSubHeader,
|
||||||
textareaQuestion,
|
|
||||||
textboxQuestion,
|
textboxQuestion,
|
||||||
navbar,
|
navbar,
|
||||||
Form,
|
Form,
|
||||||
textBlock,
|
textBlock,
|
||||||
phoneNumberQuestion,
|
phoneNumberQuestion,
|
||||||
checkboxQuestion,
|
checkboxQuestion,
|
||||||
|
techNotes,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
76
src/layouts/customer-details/tech-notes/tech-notes.spec.js
Normal file
76
src/layouts/customer-details/tech-notes/tech-notes.spec.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { shallowMount, mount } from "@vue/test-utils";
|
||||||
|
import TechNotes from "./tech-notes.vue";
|
||||||
|
import TextareaQuestion from "@/digital-components/textarea-question/textarea-question.vue";
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent() {
|
||||||
|
return "Mocked CMS Content";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("TechNotes.vue", () => {
|
||||||
|
it("Should render TechNotes", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = shallowMount(TechNotes, {
|
||||||
|
props: {
|
||||||
|
modelValue: "Initial Tech Notes",
|
||||||
|
textAreaLabelCopy: "Notes for your technician",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const techNotes = wrapper.findComponent({ ref: "techNotes" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(techNotes.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should populate the techNotes prop and render it correctly", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = mount(TechNotes, {
|
||||||
|
props: {
|
||||||
|
modelValue: "Populated Tech Notes",
|
||||||
|
textAreaLabelCopy: "Notes for your technician",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
components: {
|
||||||
|
TextareaQuestion,
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const textarea = wrapper.find("textarea");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(textarea.element.value).toBe("Populated Tech Notes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should toggle the active class when clicked", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = shallowMount(TechNotes, {
|
||||||
|
props: {
|
||||||
|
modelValue: "Initial Tech Notes",
|
||||||
|
textAreaLabelCopy: "Notes for your technician",
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleElement = wrapper.find(".tech-notes-toggle");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
expect(wrapper.vm.isActive).toBe(false); // inactive
|
||||||
|
await toggleElement.trigger("click");
|
||||||
|
expect(wrapper.vm.isActive).toBe(true); // Active after first click
|
||||||
|
await toggleElement.trigger("click");
|
||||||
|
expect(wrapper.vm.isActive).toBe(false); // Inactive after second click
|
||||||
|
});
|
||||||
|
});
|
||||||
94
src/layouts/customer-details/tech-notes/tech-notes.vue
Normal file
94
src/layouts/customer-details/tech-notes/tech-notes.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div class="tech-notes">
|
||||||
|
<div class="tech-notes-toggle" :class="{ active: isActive }" @click="toggleClass">
|
||||||
|
<textLink linkType="text" href="#!" :text="textAreaLabelCopy" />
|
||||||
|
</div>
|
||||||
|
<div class="tech-notes-content">
|
||||||
|
<textareaQuestion
|
||||||
|
ref="techNotes"
|
||||||
|
class="mb-4"
|
||||||
|
v-model="localTechNotes"
|
||||||
|
cmsWidgetName="TextAreaContentWidget"
|
||||||
|
:textAreaLabelCopy="textAreaLabelCopy"
|
||||||
|
maxLength="150" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "techNotes",
|
||||||
|
components: {
|
||||||
|
textLink,
|
||||||
|
textareaQuestion,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
textAreaLabelCopy: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
localTechNotes: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isActive: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleClass() {
|
||||||
|
this.isActive = !this.isActive;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tech-notes {
|
||||||
|
.tech-notes-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.9' xml:space='preserve'%3e%3cpath d='M8 8.9c-.2 0-.5-.1-.6-.3L.3 1.5C.1 1.4 0 1.1 0 .9 0 .7.1.4.3.3.4.1.7 0 .9 0c.2 0 .5.1.6.3L8 6.7 14.5.2c.1-.1.4-.2.6-.2.2 0 .5.1.6.3s.3.4.3.6c0 .2-.1.5-.3.6L8.6 8.6c-.1.2-.4.3-.6.3z' fill='%231474a2'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right center;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
width: 16px;
|
||||||
|
height: 9px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
&.active:after {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
&.active + .tech-notes-content {
|
||||||
|
max-height: 500px;
|
||||||
|
transition: all 250ms ease-in;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tech-notes-content {
|
||||||
|
max-height: 0;
|
||||||
|
transition: all 250ms ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -16,6 +16,10 @@ jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
|
||||||
submitWorkOrder: jest.fn(),
|
submitWorkOrder: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/helpers/pricing-helper.js", () => ({
|
||||||
|
getAmountDue: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
let piaDisabledFlag = false;
|
let piaDisabledFlag = false;
|
||||||
|
|
||||||
describe("payment-method.vue", () => {
|
describe("payment-method.vue", () => {
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,12 @@ import { coverageStatus } from "@/constants/insurance";
|
||||||
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
|
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
|
||||||
import { containsRecalParts } from "@/helpers/recal-helper";
|
import { containsRecalParts } from "@/helpers/recal-helper";
|
||||||
import { getBoolFromString } from "@/helpers/boolean-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("payment-method-required", required(errorMessages.OPTION_REQUIRED));
|
||||||
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
|
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
|
||||||
|
|
@ -767,7 +773,7 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
totalAmountDue() {
|
totalAmountDue() {
|
||||||
return baseMixin.methods.getAmountDue(this.lineItems);
|
return getAmountDue(this.lineItems);
|
||||||
},
|
},
|
||||||
isPiaEnabled() {
|
isPiaEnabled() {
|
||||||
const piaExperience = this.getSettingValue(experimentSettings.PIA_EXPERIENCE);
|
const piaExperience = this.getSettingValue(experimentSettings.PIA_EXPERIENCE);
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,17 @@ import { queryStrings } from "@/constants/query-strings";
|
||||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import baseMixin from "@/mixins/base-mixin.js";
|
|
||||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||||
import { Form } from "vee-validate";
|
import { Form } from "vee-validate";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
import { routerParams } from "@/router/router-constants/router-params";
|
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
|
// iframeResizer IS loaded into the page and necessary for the package to
|
||||||
// to auto scale the iFrame this page is loaded in
|
// to auto scale the iFrame this page is loaded in
|
||||||
// Do not remove despite showing as "unused" CASH-309
|
// Do not remove despite showing as "unused" CASH-309
|
||||||
|
|
@ -163,7 +169,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getAmountDue() {
|
getAmountDue() {
|
||||||
return baseMixin.methods.getAmountDue(store.getters.order.lineItems);
|
return getAmountDue(store.getters.order.lineItems);
|
||||||
},
|
},
|
||||||
async saveAndSubmitWorkOrder() {
|
async saveAndSubmitWorkOrder() {
|
||||||
// Final work order submit after returning from PIA.
|
// 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 { shallowMount } from "@vue/test-utils";
|
||||||
import { getMountOptions } from "@/helpers/unit-test-helper";
|
import { getMountOptions } from "@/helpers/unit-test-helper";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import baseMixin from "@/mixins/base-mixin";
|
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||||
|
|
||||||
|
jest.mock("@/helpers/pricing-helper.js", () => ({
|
||||||
|
getAmountDue: jest.fn(),
|
||||||
|
getDisplayAmountDue: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const parts = {
|
const parts = {
|
||||||
windshield: {
|
windshield: {
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,12 @@ import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.
|
||||||
import { routerParams } from "@/router/router-constants/router-params";
|
import { routerParams } from "@/router/router-constants/router-params";
|
||||||
import { coverageStatus } from "@/constants/insurance";
|
import { coverageStatus } from "@/constants/insurance";
|
||||||
import { getBoolFromString } from "@/helpers/boolean-helper.js";
|
import { getBoolFromString } from "@/helpers/boolean-helper.js";
|
||||||
|
import {
|
||||||
|
getDisplayAmountDue,
|
||||||
|
getAmountDue,
|
||||||
|
getSubTotal,
|
||||||
|
getSalesTax,
|
||||||
|
} from "@/helpers/pricing-helper.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "payment",
|
name: "payment",
|
||||||
|
|
@ -656,10 +662,10 @@ export default {
|
||||||
this.piaLineItems = lineItems.join("||");
|
this.piaLineItems = lineItems.join("||");
|
||||||
},
|
},
|
||||||
getAmountDue() {
|
getAmountDue() {
|
||||||
return baseMixin.methods.getAmountDue(store.getters.order.lineItems);
|
return getAmountDue(store.getters.order.lineItems);
|
||||||
},
|
},
|
||||||
getDisplayAmountDue() {
|
getDisplayAmountDue() {
|
||||||
return baseMixin.methods.getDisplayAmountDue(store.getters.order.lineItems);
|
return getDisplayAmountDue(store.getters.order.lineItems);
|
||||||
},
|
},
|
||||||
async paymentFailedPayLater() {
|
async paymentFailedPayLater() {
|
||||||
this.$refs.loadingModal.isModalVisible = true;
|
this.$refs.loadingModal.isModalVisible = true;
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,7 @@ export default {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// This will remove any servicePackageDiscount item from lineItems.supportingItems
|
||||||
baseMixin.methods.dispatchStoreAction(
|
baseMixin.methods.dispatchStoreAction(
|
||||||
storeActions.SAVE_SUPPORTING_ITEMS,
|
storeActions.SAVE_SUPPORTING_ITEMS,
|
||||||
resultMap.supportingItems,
|
resultMap.supportingItems,
|
||||||
|
|
@ -484,7 +485,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.attachCustomEvents();
|
this.attachCustomEventsForAnalytics();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lineItemsCloneForWatcher() {
|
lineItemsCloneForWatcher() {
|
||||||
|
|
@ -670,7 +671,7 @@ export default {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attachCustomEvents() {
|
attachCustomEventsForAnalytics() {
|
||||||
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
||||||
if (!this.isInsuranceSelected && this.lineItems.vaps?.length == 0) {
|
if (!this.isInsuranceSelected && this.lineItems.vaps?.length == 0) {
|
||||||
const tierOnePrice =
|
const tierOnePrice =
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
containsLineItemWithPartType,
|
containsLineItemWithPartType,
|
||||||
findLineItemsWithPartType,
|
findLineItemsWithPartType,
|
||||||
getPackageNameByType,
|
getPackageNameByType,
|
||||||
getDiscountPackageName,
|
getDiscountedPackageName,
|
||||||
} from "@/helpers/service-package-helper";
|
} from "@/helpers/service-package-helper";
|
||||||
import {
|
import {
|
||||||
getPromosThatMatchLineItemsOnOrder,
|
getPromosThatMatchLineItemsOnOrder,
|
||||||
|
|
@ -166,7 +166,7 @@ export default {
|
||||||
const discountServicePackage = experimentMixin.methods.getSettingValue(
|
const discountServicePackage = experimentMixin.methods.getSettingValue(
|
||||||
experimentSettings.PROMO_ON_PACKAGE
|
experimentSettings.PROMO_ON_PACKAGE
|
||||||
);
|
);
|
||||||
return getDiscountPackageName(discountServicePackage);
|
return getDiscountedPackageName(discountServicePackage);
|
||||||
},
|
},
|
||||||
frontWipersApplicableForTierTwo() {
|
frontWipersApplicableForTierTwo() {
|
||||||
return shouldFrontWipersBeAvailable(
|
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()
|
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 = {};
|
const mockCmsContent = {};
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
</template>
|
</template>
|
||||||
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
|
||||||
<datePickerForPricingByDay
|
<datePickerForPricingByDay
|
||||||
v-if="isPricingByDayExperiment"
|
|
||||||
customComponentId="dateQuestion"
|
customComponentId="dateQuestion"
|
||||||
selectableDatesSetting="custom"
|
selectableDatesSetting="custom"
|
||||||
ref="datePicker"
|
ref="datePicker"
|
||||||
|
|
@ -27,17 +26,11 @@
|
||||||
class="text-link-small"
|
class="text-link-small"
|
||||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
:customSelectableDatesCallback="getAvailableDatesMethod"
|
||||||
validationRules="date-required"
|
validationRules="date-required"
|
||||||
@date-clicked="openInshopTimeSlotsModal" />
|
:pricingByDayBasePrice="pricingByDayBasePrice"
|
||||||
<datePicker
|
:pricingByDayUpcharge="pricingByDayUpcharge"
|
||||||
v-else
|
:showPricingByDay="showPricingByDay"
|
||||||
customComponentId="dateQuestion"
|
:isPricingByDayExperiment="isPricingByDayExperiment"
|
||||||
selectableDatesSetting="custom"
|
@date-clicked="handleDateClicked" />
|
||||||
ref="datePicker"
|
|
||||||
v-model="selectedDate"
|
|
||||||
class="text-link-small"
|
|
||||||
:customSelectableDatesCallback="getAvailableDatesMethod"
|
|
||||||
validationRules="date-required"
|
|
||||||
@date-clicked="openInshopTimeSlotsModal" />
|
|
||||||
<timeSlotModalQuestion
|
<timeSlotModalQuestion
|
||||||
ref="timeSlotModalQuestion"
|
ref="timeSlotModalQuestion"
|
||||||
customComponentId="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 funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||||
import { Form, defineRule } from "vee-validate";
|
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 datePickerForPricingByDay from "@/experiment-components/date-picker-for-pricing-by-day";
|
||||||
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
|
||||||
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
|
||||||
|
|
@ -103,12 +95,20 @@ import {
|
||||||
convertDateStringToDate,
|
convertDateStringToDate,
|
||||||
sumDateString,
|
sumDateString,
|
||||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
} 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_PART_TYPE,
|
||||||
|
} from "@/constants/schedule-constants";
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
import { required } from "@/helpers/validation-rules";
|
import { required } from "@/helpers/validation-rules";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||||
import { experimentSettings } from "@/constants/experiments";
|
import { experimentSettings } from "@/constants/experiments";
|
||||||
|
import { getAmountDue, getPricingByDayPartWithPrice } 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
|
// DEFINE VALIDATION RULES
|
||||||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||||
|
|
@ -223,32 +223,90 @@ export default {
|
||||||
mobilePremiumAppointmentFee: null,
|
mobilePremiumAppointmentFee: null,
|
||||||
waitListRequested: null,
|
waitListRequested: null,
|
||||||
displayWaitList: null,
|
displayWaitList: null,
|
||||||
|
pricingByDayUpchargeLineItem: null,
|
||||||
|
includePricingByDayUpcharge: null,
|
||||||
|
isPricingByDayExperiment: null,
|
||||||
|
pricingByDayBasePrice: null,
|
||||||
|
pricingByDayUpcharge: null,
|
||||||
|
showPricingByDay: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async beforeRouteEnter(to, from, next) {
|
async beforeRouteEnter(to, from, next) {
|
||||||
// Call APIs
|
const isPricingByDayExperiment = experimentMixin.methods.hasSettingEqualTo(
|
||||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
experimentSettings.PRICING_BY_DAY,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
const showPricingByDay = !store.getters.payment.isInsurance && isPricingByDayExperiment;
|
||||||
|
|
||||||
|
// Get pricingByDayBasePrice needed for Pricing By Day
|
||||||
|
const lineItems = deepClone(store.getters.order.lineItems);
|
||||||
|
const isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder;
|
||||||
|
const shouldHideRecalibration =
|
||||||
|
experimentMixin.methods.hasSettingEqualTo(
|
||||||
|
experimentSettings.RECAL_PRICE_REMOVE,
|
||||||
|
"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 priceString = 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 pricingByDayBasePrice = parseInt(priceStringIntegerRoundedDown);
|
||||||
|
|
||||||
|
// Check to see if includePricingByDayUpcharge should already be set (based on order lineItems)
|
||||||
|
let includePricingByDayUpcharge = false;
|
||||||
|
if (supportingItemsWithoutFees) {
|
||||||
|
const pricingByDayUpchargeFeeIndex = supportingItemsWithoutFees.findIndex(
|
||||||
|
(item) => item.partType == PRICING_BY_DAY_PART_TYPE
|
||||||
|
);
|
||||||
|
if (pricingByDayUpchargeFeeIndex > -1) {
|
||||||
|
includePricingByDayUpcharge = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load page with date already selected?
|
||||||
let preSelectedDate = await store.getters.order.schedule.date;
|
let preSelectedDate = await store.getters.order.schedule.date;
|
||||||
if (!preSelectedDate || preSelectedDate.startTime === null) {
|
if (!preSelectedDate || preSelectedDate.startTime === null) {
|
||||||
preSelectedDate = null;
|
preSelectedDate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
|
// Set up promises
|
||||||
// setup config options for date-picker
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
selectableDatesSetting: "custom",
|
|
||||||
initialViewRowsToShow: 5,
|
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
|
||||||
customSelectableDatesCallback: getAvailableDates,
|
store.getters.order.serviceLocation.zipCodeCtu,
|
||||||
preSelectedDate: preSelectedDate,
|
store.getters.order.serviceLocation.provider?.address?.zipCodeCtu
|
||||||
});
|
);
|
||||||
// TODO - ONCE A DATEPICKER VERSION IS FINALIZED,
|
|
||||||
// MAKE SURE THE ABOVE METHOD loadInitialData POINTS TO THE RIGHT FILE
|
// While Pricing By Day Experiment is active, using the updated datePicker
|
||||||
|
const datePickerInitialDataPromise =
|
||||||
|
await datePickerForPricingByDay.methods.loadInitialData({
|
||||||
|
// setup config options for date-picker
|
||||||
|
selectableDatesSetting: "custom",
|
||||||
|
initialViewRowsToShow: 5,
|
||||||
|
customSelectableDatesCallback: getAvailableDates,
|
||||||
|
preSelectedDate: preSelectedDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get pricingByDayUpcharge needed for Pricing By Day
|
||||||
|
const pricingByDayUpchargePartPromise = getPricingByDayPartWithPrice();
|
||||||
|
|
||||||
const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
|
const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
|
||||||
storeActions.GET_MOBILE_PREMIUM_FEE,
|
storeActions.GET_MOBILE_PREMIUM_FEE,
|
||||||
null,
|
null,
|
||||||
"schedule"
|
"schedule"
|
||||||
);
|
);
|
||||||
|
|
||||||
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
|
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
return baseMixin.methods.dispatchStoreActionWithLogging(
|
return baseMixin.methods.dispatchStoreActionWithLogging(
|
||||||
|
|
@ -264,11 +322,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
|
// Settle promises and get results
|
||||||
const promiseResultMap = [
|
const promiseResultMap = [
|
||||||
{
|
{
|
||||||
|
|
@ -283,6 +336,10 @@ export default {
|
||||||
resultKey: "datePickerInitialData",
|
resultKey: "datePickerInitialData",
|
||||||
promise: datePickerInitialDataPromise,
|
promise: datePickerInitialDataPromise,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
resultKey: "pricingByDayUpchargePart",
|
||||||
|
promise: pricingByDayUpchargePartPromise,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
resultKey: "premiumFeeWithPrice",
|
resultKey: "premiumFeeWithPrice",
|
||||||
promise: premiumFeeWithPricePromise,
|
promise: premiumFeeWithPricePromise,
|
||||||
|
|
@ -291,10 +348,19 @@ export default {
|
||||||
|
|
||||||
const resultMap = await settleAllPromises(promiseResultMap);
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
||||||
|
const pricingByDayUpcharge = await baseMixin.methods.getTotalLineItemPrice(
|
||||||
|
resultMap.pricingByDayUpchargePart,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const datePickerInitialData = resultMap.datePickerInitialData;
|
||||||
|
datePickerInitialData.pricingByDayBasePrice = pricingByDayBasePrice;
|
||||||
|
datePickerInitialData.pricingByDayUpcharge = pricingByDayUpcharge;
|
||||||
|
|
||||||
// Call the "next" function to complete the transition to this page.
|
// Call the "next" function to complete the transition to this page.
|
||||||
next((vm) => {
|
next((vm) => {
|
||||||
vm.setCmsContent(resultMap.cmsContent);
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
vm.$refs.datePicker.initializeComponent(datePickerInitialData);
|
||||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||||
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
||||||
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
|
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
|
||||||
|
|
@ -302,6 +368,12 @@ export default {
|
||||||
: null;
|
: null;
|
||||||
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
||||||
vm.setDisplayWaitList();
|
vm.setDisplayWaitList();
|
||||||
|
vm.pricingByDayUpchargeLineItem = resultMap.pricingByDayUpchargePart;
|
||||||
|
vm.includePricingByDayUpcharge = includePricingByDayUpcharge;
|
||||||
|
vm.isPricingByDayExperiment = isPricingByDayExperiment;
|
||||||
|
vm.pricingByDayBasePrice = pricingByDayBasePrice;
|
||||||
|
vm.pricingByDayUpcharge = pricingByDayUpcharge;
|
||||||
|
vm.showPricingByDay = showPricingByDay;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -316,21 +388,12 @@ export default {
|
||||||
return this.$store.getters.order.serviceLocation.appointmentType;
|
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||||
},
|
},
|
||||||
timeSlotsForSelectedDate() {
|
timeSlotsForSelectedDate() {
|
||||||
if (!this.selectedDate) {
|
if (!this.selectedDate) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.selectableDatesData.days?.find(
|
return this.selectableDatesData.days?.find(
|
||||||
(selectableDate) => selectableDate.date === this.selectedDate
|
(selectableDate) => selectableDate.date === this.selectedDate
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isPricingByDayExperiment() {
|
|
||||||
return (
|
|
||||||
experimentMixin.methods
|
|
||||||
.getSettingValue(experimentSettings.PRICING_BY_DAY)
|
|
||||||
?.toLowerCase() === "true"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
splitCopyOnCMSPlaceHolder,
|
splitCopyOnCMSPlaceHolder,
|
||||||
|
|
@ -377,6 +440,7 @@ export default {
|
||||||
this.appointmentType,
|
this.appointmentType,
|
||||||
this.$store.getters.order.serviceLocation.provider.providerNumber
|
this.$store.getters.order.serviceLocation.provider.providerNumber
|
||||||
);
|
);
|
||||||
|
|
||||||
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
// ADD API CALL RESULTS TO EXISTING DATE DATA
|
||||||
this.selectableDatesData.days = this.selectableDatesData.days.concat(
|
this.selectableDatesData.days = this.selectableDatesData.days.concat(
|
||||||
newShopTimeSlots.days
|
newShopTimeSlots.days
|
||||||
|
|
@ -555,6 +619,28 @@ export default {
|
||||||
updateSupportingItems() {
|
updateSupportingItems() {
|
||||||
const supportingItems = this.getSupportingItems();
|
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_PART_TYPE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.includePricingByDayUpcharge) {
|
||||||
|
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 we have a premium fee(early bird), then save/update supporting items
|
||||||
if (
|
if (
|
||||||
this.appointmentType === AppointmentTypeStrings.MOBILE &&
|
this.appointmentType === AppointmentTypeStrings.MOBILE &&
|
||||||
|
|
@ -564,7 +650,7 @@ export default {
|
||||||
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
(item) => item.partType == PREMIUM_FEE_PART_TYPE
|
||||||
);
|
);
|
||||||
|
|
||||||
if (premiumFeeIndex >= 0) {
|
if (premiumFeeIndex > -1) {
|
||||||
supportingItems[premiumFeeIndex].laborAmount =
|
supportingItems[premiumFeeIndex].laborAmount =
|
||||||
this.mobilePremiumAppointmentFee.laborAmount;
|
this.mobilePremiumAppointmentFee.laborAmount;
|
||||||
supportingItems[premiumFeeIndex].sellingPrice =
|
supportingItems[premiumFeeIndex].sellingPrice =
|
||||||
|
|
@ -574,12 +660,6 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
supportingItems.push(this.mobilePremiumAppointmentFee);
|
supportingItems.push(this.mobilePremiumAppointmentFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchStoreAction(
|
|
||||||
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
|
||||||
supportingItems,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
if (!supportingItems) {
|
if (!supportingItems) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -592,17 +672,28 @@ export default {
|
||||||
|
|
||||||
if (removePremiumFeeIndex >= 0) {
|
if (removePremiumFeeIndex >= 0) {
|
||||||
supportingItems.splice(removePremiumFeeIndex, 1);
|
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) {
|
handleWaitListRequested(value) {
|
||||||
this.waitListRequested = value;
|
this.waitListRequested = value;
|
||||||
},
|
},
|
||||||
|
handleDateClicked(date) {
|
||||||
|
// do something to mark this as upcharge day or not...
|
||||||
|
if (date.isPricingByDayUpchargeDay) {
|
||||||
|
this.includePricingByDayUpcharge = true;
|
||||||
|
} else {
|
||||||
|
this.includePricingByDayUpcharge = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openInshopTimeSlotsModal();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedDate(newValue, oldValue) {
|
selectedDate(newValue, oldValue) {
|
||||||
|
|
@ -631,7 +722,6 @@ export default {
|
||||||
funnelSubHeader,
|
funnelSubHeader,
|
||||||
Form,
|
Form,
|
||||||
loadingModal,
|
loadingModal,
|
||||||
datePicker,
|
|
||||||
datePickerForPricingByDay,
|
datePickerForPricingByDay,
|
||||||
locationAlerts,
|
locationAlerts,
|
||||||
timeSlotModalQuestion,
|
timeSlotModalQuestion,
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,25 @@ export default {
|
||||||
});
|
});
|
||||||
return filteredLineItems;
|
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) {
|
filterOutServicePackageDiscountPart(lineItems) {
|
||||||
const filteredLineItems = lineItems?.filter((item) => {
|
const filteredLineItems = lineItems?.filter((item) => {
|
||||||
return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT);
|
return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT);
|
||||||
|
|
@ -133,75 +152,18 @@ export default {
|
||||||
return totalPrice;
|
return totalPrice;
|
||||||
},
|
},
|
||||||
getTotalLineItemPrice(lineItem, includeTax) {
|
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) {
|
if (includeTax) {
|
||||||
return (
|
return kitPrice + laborAmount + sellingPrice + salesTax;
|
||||||
lineItem.kitPrice +
|
|
||||||
lineItem.laborAmount +
|
|
||||||
lineItem.sellingPrice +
|
|
||||||
lineItem.salesTax
|
|
||||||
);
|
|
||||||
} else {
|
} 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() {
|
scrollToPageTop() {
|
||||||
const container = document.getElementsByClassName("page-container-grouped-styles")[0];
|
const container = document.getElementsByClassName("page-container-grouped-styles")[0];
|
||||||
container.scrollTo({ top: 0, left: 0, behavior: "smooth" });
|
container.scrollTo({ top: 0, left: 0, behavior: "smooth" });
|
||||||
|
|
|
||||||
|
|
@ -144,180 +144,6 @@ describe("baseMixin.js", () => {
|
||||||
|
|
||||||
expect(mixIn.methods.dispatchStoreActionWithLogging).toBeCalled();
|
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 }) {
|
function getMixInInstance({ isDispatchSuccess = true }) {
|
||||||
|
|
|
||||||
|
|
@ -1749,6 +1749,18 @@ export const actions = {
|
||||||
pageNameToLog: pageNameToLog,
|
pageNameToLog: pageNameToLog,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async getPricingByDayPart(context, { pageNameToLog }) {
|
||||||
|
return await globalMethods
|
||||||
|
.callHttpClient({
|
||||||
|
method: endpoints.GetPricingByDayPart.method,
|
||||||
|
endpoint: endpoints.GetPricingByDayPart.url,
|
||||||
|
logApiCall: true,
|
||||||
|
pageNameToLog: pageNameToLog,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
},
|
||||||
getServicePackageDiscountPart(context, { pageNameToLog }) {
|
getServicePackageDiscountPart(context, { pageNameToLog }) {
|
||||||
const damage = context.getters.damage;
|
const damage = context.getters.damage;
|
||||||
const damageType = damage.isRepair ? "Repair" : "Replace";
|
const damageType = damage.isRepair ? "Repair" : "Replace";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue