Merge pull request #2379 from Safelite/rlsmerge/develop-to-heavy.truck
Release merge develop to heavy truck
This commit is contained in:
commit
b1588a8e9b
38 changed files with 1135 additions and 490 deletions
|
|
@ -72,6 +72,10 @@ const endpoints = {
|
|||
url: "/parts/api/v1/parts/mobile-fee",
|
||||
method: "GET",
|
||||
},
|
||||
GetPricingByDayPart: {
|
||||
url: "/parts/api/v1/parts/pricing-by-day-part-number",
|
||||
method: "GET",
|
||||
},
|
||||
GetServicePackageDiscountPart: {
|
||||
url: "/parts/api/v1/parts/service-package-discount",
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ const partNumberStrings = {
|
|||
MOBILE_STATIC_RECAL_FEE: "RECAL MOBILE",
|
||||
MOBILE_DUAL_RECAL_FEE: "RECAL MOBILEDUAL",
|
||||
DONATION: "DONATION",
|
||||
// Fees
|
||||
RECYCLE_FEE: "RECYCLE FEE",
|
||||
PRICING_BY_DAY_UPCHARGE: "PREMAPPT GEN",
|
||||
};
|
||||
|
||||
export { partNumberStrings };
|
||||
|
|
|
|||
|
|
@ -8,9 +8,17 @@ const PREMIUM_TIME_SLOT_ID_FLAG = "-PREMIUM";
|
|||
|
||||
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
|
||||
|
||||
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
|
||||
|
||||
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_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_PART: "getPricingByDayPart",
|
||||
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
|
||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,43 @@ const MONTHS_OF_YEAR = [
|
|||
"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",
|
||||
isPricingByDayUpchargeDay: true,
|
||||
},
|
||||
{
|
||||
label: "Monday",
|
||||
cssClass: "monday",
|
||||
isPricingByDayUpchargeDay: true,
|
||||
},
|
||||
{
|
||||
label: "Tuesday",
|
||||
cssClass: "tuesday",
|
||||
isPricingByDayUpchargeDay: false,
|
||||
},
|
||||
{
|
||||
label: "Wednesday",
|
||||
cssClass: "wednesday",
|
||||
isPricingByDayUpchargeDay: false,
|
||||
},
|
||||
{
|
||||
label: "Thursday",
|
||||
cssClass: "thursday",
|
||||
isPricingByDayUpchargeDay: false,
|
||||
},
|
||||
{
|
||||
label: "Friday",
|
||||
cssClass: "friday",
|
||||
isPricingByDayUpchargeDay: true,
|
||||
},
|
||||
{
|
||||
label: "Saturday",
|
||||
cssClass: "saturday",
|
||||
isPricingByDayUpchargeDay: true,
|
||||
},
|
||||
];
|
||||
|
||||
export { TIMINGFUNC_MAP, BUFFER_OFFSET, MONTHS_OF_YEAR, DAYS_OF_WEEK };
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ export default {
|
|||
},
|
||||
closeModal() {
|
||||
const modal = Modal.getInstance(document.getElementById(this.modalId));
|
||||
modal.hide();
|
||||
modal?.hide();
|
||||
this.$emit("isModalOpened", false);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
<template>
|
||||
<div class="textarea-question">
|
||||
<div class="label-wrapper d-flex flex-column mb-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>
|
||||
<div class="label-wrapper d-flex flex-column mb-1 mt-1" :aria-label="questionText">
|
||||
<textarea
|
||||
:id="textAreaLabelCopy"
|
||||
ref="textarea"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<div class="date-picker text-center" :class="calendarViewDirection">
|
||||
<div
|
||||
class="date-picker text-center"
|
||||
:class="`${calendarViewDirection} ${isPricingByDayClass} ${showPricingByDayClass}`">
|
||||
<fieldset id="date-picker-fieldset" ref="datePickerFieldset">
|
||||
<legend class="sr-only">Select a day and time</legend>
|
||||
<div
|
||||
|
|
@ -60,18 +62,24 @@
|
|||
date.dateNum === 1 ? 'first-day-' + month.startDateDayIndex : '',
|
||||
date.dayClasses,
|
||||
date.isSelectable ? 'selectable-day' : '',
|
||||
date.isPricingByDayUpchargeDay ? 'upch-day' : '',
|
||||
]">
|
||||
<input
|
||||
:disabled="!date.isSelectable"
|
||||
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 +111,7 @@ import {
|
|||
TIMINGFUNC_MAP,
|
||||
BUFFER_OFFSET,
|
||||
MONTHS_OF_YEAR,
|
||||
DAYS_OF_WEEK,
|
||||
} from "@/digital-components/date-picker/mixins/constants";
|
||||
import {
|
||||
selectableDaysOptions,
|
||||
|
|
@ -156,6 +165,10 @@ export default {
|
|||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showPricingByDay: Boolean,
|
||||
pricingByDayBasePrice: Number,
|
||||
pricingByDayUpcharge: Number,
|
||||
isPricingByDayExperiment: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -207,6 +220,12 @@ export default {
|
|||
if (this.selectableDatesSetting === "custom") return "future";
|
||||
return "past";
|
||||
},
|
||||
isPricingByDayClass() {
|
||||
return this.isPricingByDayExperiment ? "pricing-by-day" : "";
|
||||
},
|
||||
showPricingByDayClass() {
|
||||
return this.showPricingByDay ? "show-pricing-by-day" : "";
|
||||
},
|
||||
selectedDate: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
|
|
@ -220,17 +239,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);
|
||||
|
|
@ -453,6 +467,8 @@ export default {
|
|||
initialViewEndDate: config.initialViewEndDate,
|
||||
hideSecondMonth: hideSecondMonth,
|
||||
preSelectedDate: config.preSelectedDate,
|
||||
pricingByDayBasePrice: config.pricingByDayBasePrice,
|
||||
pricingByDayUpcharge: config.pricingByDayUpcharge,
|
||||
};
|
||||
if (direction === "future") {
|
||||
// first 0, then 1
|
||||
|
|
@ -586,6 +602,17 @@ export default {
|
|||
("0" + monthNum).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) {
|
||||
dayClasses += " current-day";
|
||||
|
|
@ -596,8 +623,8 @@ export default {
|
|||
if (offset === 0 && i > this.todayDateNum && calendarViewDirection === "past") {
|
||||
dayClasses += " unavailable-day";
|
||||
}
|
||||
if (convertDateStringToDate(dateString).getDay() === 0) {
|
||||
dayClasses += " sunday";
|
||||
if (dayIndex === 0) {
|
||||
dayClasses += " " + dayObject.cssClass;
|
||||
}
|
||||
if (
|
||||
this.hideSomeDaysForInitialView &&
|
||||
|
|
@ -611,10 +638,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 +805,7 @@ export default {
|
|||
|
||||
.grid-item {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin: 10px 3px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
|
||||
|
|
@ -814,13 +840,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 +864,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 +919,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 +961,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 +998,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 +1094,112 @@ 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: 100%;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.date-picker.show-pricing-by-day {
|
||||
.radio-wrapper {
|
||||
&.selectable-day label span {
|
||||
text-decoration: none;
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
font-family: "AvertaSemibold";
|
||||
|
||||
&.price {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
&.upch-day label span.price {
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
input[type="radio"] {
|
||||
&:focus + label,
|
||||
&:checked:focus + label {
|
||||
color: $gray-600;
|
||||
}
|
||||
&:checked + label {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 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) {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -526,17 +526,9 @@ export default {
|
|||
// try to call API to add donation to order
|
||||
|
||||
// TEMP DUMMY CALL TO SIMULATE SUCCESS OR ERROR FROM API CALL
|
||||
// TO BE REPLACED BY REAL CALL LATER
|
||||
const donationResponse = await this.dispatchStoreActionWithLogging(
|
||||
storeActions.LOOKUP_VIN_BY_PLATE,
|
||||
{ licensePlate: plateTemp, licenseState: "OH" },
|
||||
"test",
|
||||
false
|
||||
).catch((error) => {
|
||||
return error;
|
||||
});
|
||||
const donationResponse = this.createMockedResponse(amount);
|
||||
|
||||
if (donationResponse?.status == "200") {
|
||||
if (donationResponse?.wasSuccessful) {
|
||||
this.showDonationSuccess = true;
|
||||
this.showDonationError = false;
|
||||
const donationResponse = await baseMixin.methods.dispatchStoreAction(
|
||||
|
|
@ -554,6 +546,26 @@ export default {
|
|||
);
|
||||
}
|
||||
},
|
||||
//mocked response. should be removed once integrate with actual api call.
|
||||
createMockedResponse(amount) {
|
||||
let result;
|
||||
|
||||
switch (amount) {
|
||||
case 1:
|
||||
result = { wasSuccessful: true, wasLocked: false };
|
||||
break;
|
||||
case 3:
|
||||
result = { wasSuccessful: false, wasLocked: true };
|
||||
break;
|
||||
case 5:
|
||||
result = { wasSuccessful: false, wasLocked: false };
|
||||
break;
|
||||
default:
|
||||
result = { wasSuccessful: null, wasLocked: null }; // Default case if amount doesn't match
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
donationAmount(newValue) {
|
||||
|
|
|
|||
|
|
@ -1,53 +1,80 @@
|
|||
// Components
|
||||
import customerDetails from "@/layouts/customer-details/customer-details.vue";
|
||||
import { shallowMount, mount } from "@vue/test-utils";
|
||||
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
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent() {
|
||||
return "Mocked CMS Content";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Mock our module for promises.
|
||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||
settleAllPromises: jest.fn(),
|
||||
}));
|
||||
const mockStoreActions = {
|
||||
SAVE_CUSTOMER_DETAILS: "SAVE_CUSTOMER_DETAILS",
|
||||
SAVE_SERVICE_LOCATION_TECH_NOTES: "SAVE_SERVICE_LOCATION_TECH_NOTES",
|
||||
SAVE_PAYMENT_METHOD_CHOICE: "SAVE_PAYMENT_METHOD_CHOICE",
|
||||
};
|
||||
|
||||
describe("customer-details.vue", () => {
|
||||
describe("navigation", () => {
|
||||
test("if the back button is clicked, navigate back", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks();
|
||||
describe("CustomerDetails.vue", () => {
|
||||
let wrapper;
|
||||
|
||||
// Act
|
||||
await wrapper.vm.backButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("if the continue button is clicked, navigate forward", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks();
|
||||
|
||||
// Act
|
||||
await wrapper.vm.forwardButtonAction();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
||||
beforeEach(() => {
|
||||
wrapper = mount(CustomerDetails, {
|
||||
global: {
|
||||
components: {
|
||||
TechNotes,
|
||||
TextboxQuestion,
|
||||
PhoneNumberQuestion,
|
||||
CheckboxQuestion,
|
||||
TextBlock,
|
||||
FunnelHeader,
|
||||
FunnelSubHeader,
|
||||
Navbar,
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
mocks: {
|
||||
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"
|
||||
v-model="isSmsOptIn" />
|
||||
|
||||
<textareaQuestion
|
||||
class="mb-4"
|
||||
v-model="techNotes"
|
||||
cmsWidgetName="TextAreaContentWidget"
|
||||
textAreaLabelCopy="Notes for your technician"
|
||||
maxLength="150" />
|
||||
|
||||
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
||||
<techNotes v-model="techNotes" :textAreaLabelCopy="textAreaLabelCopy" />
|
||||
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
|
|
@ -62,6 +55,8 @@
|
|||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
|
||||
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -72,20 +67,18 @@
|
|||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
||||
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 phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||
import techNotes from "@/layouts/customer-details/tech-notes/tech-notes";
|
||||
//Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { routerParams } from "@/router/router-constants/router-params";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import { useField, validate } from "vee-validate";
|
||||
import store from "@/store";
|
||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||
|
||||
|
|
@ -199,13 +192,13 @@ export default {
|
|||
components: {
|
||||
funnelHeader,
|
||||
funnelSubHeader,
|
||||
textareaQuestion,
|
||||
textboxQuestion,
|
||||
navbar,
|
||||
Form,
|
||||
textBlock,
|
||||
phoneNumberQuestion,
|
||||
checkboxQuestion,
|
||||
techNotes,
|
||||
},
|
||||
};
|
||||
</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(),
|
||||
}));
|
||||
|
||||
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" />
|
||||
:pricingByDayBasePrice="pricingByDayBasePrice"
|
||||
:pricingByDayUpcharge="pricingByDayUpcharge"
|
||||
:showPricingByDay="showPricingByDay"
|
||||
:isPricingByDayExperiment="isPricingByDayExperiment"
|
||||
@date-clicked="handleDateClicked" />
|
||||
<timeSlotModalQuestion
|
||||
ref="timeSlotModalQuestion"
|
||||
customComponentId="timeSlotModalQuestion"
|
||||
|
|
@ -61,7 +54,7 @@
|
|||
:estimatedServiceMinutesMaximum="
|
||||
selectableDatesData.estimatedServiceMinutesMaximum
|
||||
"
|
||||
@waitListRequested-emitted="handleWaitListRequested"
|
||||
@waitListRequested="handleWaitListRequested"
|
||||
@time-slot-modal-closed="timeSlotModalClosed"
|
||||
validationRules="time-slot-selection-required"
|
||||
@TimeSlotSelected="forwardButtonAction" />
|
||||
|
|
@ -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_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, 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
|
||||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||
|
|
@ -223,32 +223,98 @@ export default {
|
|||
mobilePremiumAppointmentFee: null,
|
||||
waitListRequested: null,
|
||||
displayWaitList: null,
|
||||
pricingByDayUpchargeLineItem: null,
|
||||
includePricingByDayUpcharge: null,
|
||||
isPricingByDayExperiment: null,
|
||||
pricingByDayBasePrice: null,
|
||||
pricingByDayUpcharge: null,
|
||||
showPricingByDay: null,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
const isPricingByDayExperiment = experimentMixin.methods.hasSettingEqualTo(
|
||||
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 supportingItemsFromStore = lineItems?.supportingItems;
|
||||
const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers(
|
||||
lineItems.supportingItems,
|
||||
{
|
||||
partNumbersToRemove: [
|
||||
partNumberStrings.RECYCLE_FEE,
|
||||
partNumberStrings.PRICING_BY_DAY_UPCHARGE,
|
||||
],
|
||||
}
|
||||
);
|
||||
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 (supportingItemsFromStore) {
|
||||
const pricingByDayUpchargeFeeIndex = supportingItemsFromStore?.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;
|
||||
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 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 = showPricingByDay
|
||||
? getPricingByDayPartWithPrice()
|
||||
: null;
|
||||
|
||||
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 +330,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 = [
|
||||
{
|
||||
|
|
@ -283,6 +344,10 @@ export default {
|
|||
resultKey: "datePickerInitialData",
|
||||
promise: datePickerInitialDataPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "pricingByDayUpchargePart",
|
||||
promise: pricingByDayUpchargePartPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "premiumFeeWithPrice",
|
||||
promise: premiumFeeWithPricePromise,
|
||||
|
|
@ -291,10 +356,21 @@ export default {
|
|||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
const pricingByDayUpcharge = showPricingByDay
|
||||
? await baseMixin.methods.getTotalLineItemPrice(
|
||||
resultMap.pricingByDayUpchargePart,
|
||||
false
|
||||
)
|
||||
: null;
|
||||
|
||||
const datePickerInitialData = resultMap.datePickerInitialData;
|
||||
datePickerInitialData.pricingByDayBasePrice = pricingByDayBasePrice;
|
||||
datePickerInitialData.pricingByDayUpcharge = pricingByDayUpcharge;
|
||||
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.$refs.datePicker.initializeComponent(resultMap.datePickerInitialData);
|
||||
vm.$refs.datePicker.initializeComponent(datePickerInitialData);
|
||||
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
|
||||
vm.selectableDatesData = resultMap.datePickerInitialData.initialShopTimeSlotsResponse;
|
||||
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
|
||||
|
|
@ -302,6 +378,12 @@ export default {
|
|||
: null;
|
||||
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
|
||||
vm.setDisplayWaitList();
|
||||
vm.pricingByDayUpchargeLineItem = resultMap.pricingByDayUpchargePart;
|
||||
vm.includePricingByDayUpcharge = includePricingByDayUpcharge;
|
||||
vm.isPricingByDayExperiment = isPricingByDayExperiment;
|
||||
vm.pricingByDayBasePrice = pricingByDayBasePrice;
|
||||
vm.pricingByDayUpcharge = pricingByDayUpcharge;
|
||||
vm.showPricingByDay = showPricingByDay;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -316,21 +398,12 @@ export default {
|
|||
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||
},
|
||||
timeSlotsForSelectedDate() {
|
||||
if (!this.selectedDate) {
|
||||
return null;
|
||||
}
|
||||
if (!this.selectedDate) return null;
|
||||
|
||||
return this.selectableDatesData.days?.find(
|
||||
(selectableDate) => selectableDate.date === this.selectedDate
|
||||
);
|
||||
},
|
||||
isPricingByDayExperiment() {
|
||||
return (
|
||||
experimentMixin.methods
|
||||
.getSettingValue(experimentSettings.PRICING_BY_DAY)
|
||||
?.toLowerCase() === "true"
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
|
|
@ -377,6 +450,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 +586,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 +629,30 @@ export default {
|
|||
updateSupportingItems() {
|
||||
const supportingItems = this.getSupportingItems();
|
||||
|
||||
// if we have a pricing by day upcharge, then save/update supporting items with it
|
||||
if (this.showPricingByDay) {
|
||||
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 (
|
||||
this.appointmentType === AppointmentTypeStrings.MOBILE &&
|
||||
|
|
@ -562,7 +662,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 +672,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 +684,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.includePricingByDayUpcharge = true;
|
||||
} else {
|
||||
this.includePricingByDayUpcharge = false;
|
||||
}
|
||||
|
||||
this.openInshopTimeSlotsModal();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedDate(newValue, oldValue) {
|
||||
|
|
@ -629,7 +734,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() {
|
||||
|
|
@ -526,13 +539,26 @@ export default {
|
|||
};
|
||||
},
|
||||
waitListChecked(event) {
|
||||
this.$emit("waitListRequested-emitted", event.target.checked);
|
||||
this.$emit("waitListRequested", 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: 0.75rem 1rem;
|
||||
margin-bottom: -0.5rem;
|
||||
.success-text {
|
||||
margin-left: 0.5rem;
|
||||
strong {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0%;
|
||||
}
|
||||
}
|
||||
.success-image {
|
||||
margin-top: 4.5px;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: #0c7e47;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { storeActions } from "@/constants/store-actions";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { partNumberStrings } from "@/constants/part-number-strings";
|
||||
|
||||
export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
|
||||
if (!serviceZipCode) {
|
||||
|
|
@ -42,6 +43,50 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
|
|||
return pricingResults[0];
|
||||
}
|
||||
|
||||
export async function getPricedRecycleFeePart(serviceZipCode, pageNameToLog) {
|
||||
if (!serviceZipCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const zipCodeData = await getZipCodeData(serviceZipCode);
|
||||
|
||||
// Get supporting items
|
||||
const supportingItems = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_SUPPORTING_ITEMS,
|
||||
null,
|
||||
pageNameToLog,
|
||||
false
|
||||
);
|
||||
|
||||
if (
|
||||
supportingItems.data === null ||
|
||||
supportingItems.data === undefined ||
|
||||
supportingItems.data === ""
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recycleFeePart = supportingItems.data.find(
|
||||
(item) => item.partNumber === partNumberStrings.RECYCLE_FEE
|
||||
);
|
||||
|
||||
if (recycleFeePart) {
|
||||
// Get the Recycle Fee Part Price
|
||||
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||
{
|
||||
availableLineItems: [recycleFeePart],
|
||||
serviceZipCode: serviceZipCode,
|
||||
serviceZipCodeCtu: zipCodeData.zipCodeCtu,
|
||||
},
|
||||
pageNameToLog,
|
||||
false
|
||||
);
|
||||
|
||||
return pricingResults[0];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServiceabilityDetails(serviceZipCode, lineItems, pageNameToLog) {
|
||||
// Get the Mobile Fee Part
|
||||
return await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,23 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
|
|||
return Promise.resolve(mobileFeePart);
|
||||
};
|
||||
|
||||
const mockGetPricedRecycleFeePart = (mockServiceZipCode) => {
|
||||
let recycleFeePart = {};
|
||||
|
||||
if (mockServiceZipCode === "43235") {
|
||||
recycleFeePart = {
|
||||
partNumber: "RECYCLE FEE",
|
||||
description: "RECYCLE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 39.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.resolve(recycleFeePart);
|
||||
};
|
||||
|
||||
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
|
||||
const serviceabilityDetails = {
|
||||
isGlassServiceableInshop: true,
|
||||
|
|
@ -123,6 +140,9 @@ jest.mock(
|
|||
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedMobileFeePart(mockServiceZipCode);
|
||||
}),
|
||||
getPricedRecycleFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedRecycleFeePart(mockServiceZipCode);
|
||||
}),
|
||||
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetServiceabilityDetails(mockServiceZipCode);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location
|
|||
import { deepClone } from "@/helpers/object-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getPricedRecycleFeePart,
|
||||
getServiceabilityDetails,
|
||||
getBillToAccountNumber,
|
||||
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
|
@ -267,6 +268,12 @@ export default {
|
|||
"service-location"
|
||||
);
|
||||
|
||||
// retrieve recycle fee part
|
||||
const recycleFeePart = await getPricedRecycleFeePart(
|
||||
serviceZipCode,
|
||||
"service-location"
|
||||
);
|
||||
|
||||
// retrieve serviceability details
|
||||
const serviceabilityDetails = await getServiceabilityDetails(
|
||||
serviceZipCode,
|
||||
|
|
@ -280,6 +287,7 @@ export default {
|
|||
|
||||
// update content related to service zip code
|
||||
this.$emit("updated-mobile-fee-part", mobileFeePart);
|
||||
this.$emit("updated-recycle-fee-part", recycleFeePart);
|
||||
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
||||
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
||||
this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
ref="serviceZipCodeQuestion"
|
||||
:mobileFeePart="mobileFeePart"
|
||||
@updated-mobile-fee-part="setMobileFeePart"
|
||||
@updated-recycle-fee-part="setRecycleFeePart"
|
||||
@updated-serviceability="setServiceabilityDetails"
|
||||
@updated-contains-military-base="setContainsMilitaryBase"
|
||||
@updated-bill-to-account-number="setBillToAccountNumber"
|
||||
|
|
@ -90,6 +91,7 @@
|
|||
:mobileFeePart="mobileFeePart"
|
||||
:mobileFeeApplies="mobileFeeApplies"
|
||||
@updated-mobile-fee-part="setMobileFeePart"
|
||||
@updated-recycle-fee-part="setRecycleFeePart"
|
||||
@updated-serviceability="setServiceabilityDetails"
|
||||
@updated-contains-military-base="setContainsMilitaryBase"
|
||||
@updated-mobile-ctu="setCtuForMobile"
|
||||
|
|
@ -195,6 +197,7 @@ export default {
|
|||
selectedAppointmentType: this.getSelectedAppointmentType(),
|
||||
selectedProvider: this.getSelectedProvider(),
|
||||
mobileFeePart: null,
|
||||
recycleFeePart: null,
|
||||
zipContainsMilitaryBase: false,
|
||||
zipCodeCtu: null,
|
||||
billToAccountNumber: null,
|
||||
|
|
@ -480,6 +483,9 @@ export default {
|
|||
setMobileFeePart(mobileFeePart) {
|
||||
this.mobileFeePart = mobileFeePart;
|
||||
},
|
||||
setRecycleFeePart(recycleFeePart) {
|
||||
this.recycleFeePart = recycleFeePart;
|
||||
},
|
||||
//creating async function as the computed property can not directly handle asynchronous operations or promises.
|
||||
async setMobileLocationQuestions(newValue) {
|
||||
var newZipCode = newValue.addressQuestions.zipCode;
|
||||
|
|
@ -592,6 +598,7 @@ export default {
|
|||
},
|
||||
updateAndSaveSupportingItems() {
|
||||
let supportingItems = store.getters.lineItems.supportingItems;
|
||||
let shouldSaveSupportingItems = false;
|
||||
|
||||
supportingItems =
|
||||
!supportingItems && this.isMobileStaticRecalibrationApplicable
|
||||
|
|
@ -603,6 +610,21 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
// update recyle fee price
|
||||
if (this.recycleFeePart) {
|
||||
const recycleFeeIndex = supportingItems.findIndex(
|
||||
(item) => item.partNumber == partNumberStrings.RECYCLE_FEE
|
||||
);
|
||||
if (recycleFeeIndex >= 0) {
|
||||
supportingItems[recycleFeeIndex].laborAmount = this.recycleFeePart.laborAmount;
|
||||
supportingItems[recycleFeeIndex].sellingPrice =
|
||||
this.recycleFeePart.sellingPrice;
|
||||
supportingItems[recycleFeeIndex].kitPrice = this.recycleFeePart.kitPrice;
|
||||
|
||||
shouldSaveSupportingItems = true;
|
||||
}
|
||||
}
|
||||
|
||||
// if we have a mobile fee, then save/update supporting items
|
||||
if (this.selectedAppointmentType == "Mobile") {
|
||||
const mobileFeeIndex = supportingItems.findIndex(
|
||||
|
|
@ -617,11 +639,7 @@ export default {
|
|||
if (this.mobileFeePart !== null) supportingItems.push(this.mobileFeePart);
|
||||
}
|
||||
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||
supportingItems,
|
||||
false
|
||||
);
|
||||
shouldSaveSupportingItems = true;
|
||||
} else {
|
||||
// if it's not a mobile, then make sure we remove any that may have been added
|
||||
const removeMobileFeeIndex = supportingItems?.findIndex(
|
||||
|
|
@ -630,13 +648,18 @@ export default {
|
|||
|
||||
if (removeMobileFeeIndex >= 0) {
|
||||
supportingItems.splice(removeMobileFeeIndex, 1);
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||
supportingItems,
|
||||
false
|
||||
);
|
||||
shouldSaveSupportingItems = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use shouldSaveSupportingItems flag to determine if we need to save supporting items. Prevents unnecessary/multiple saves
|
||||
if (shouldSaveSupportingItems) {
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||
supportingItems,
|
||||
false
|
||||
);
|
||||
}
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
this.navigatingForward = true;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,23 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
|
|||
return Promise.resolve(mobileFeePart);
|
||||
};
|
||||
|
||||
const mockGetPricedRecycleFeePart = (mockServiceZipCode) => {
|
||||
let recycleFeePart = {};
|
||||
|
||||
if (mockServiceZipCode === "43235") {
|
||||
recycleFeePart = {
|
||||
partNumber: "RECYCLE FEE",
|
||||
description: "RECYCLE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 39.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.resolve(recycleFeePart);
|
||||
};
|
||||
|
||||
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
|
||||
const serviceabilityDetails = {
|
||||
isGlassServiceableInshop: true,
|
||||
|
|
@ -49,6 +66,9 @@ jest.mock(
|
|||
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedMobileFeePart(mockServiceZipCode);
|
||||
}),
|
||||
getPricedRecycleFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedRecycleFeePart(mockServiceZipCode);
|
||||
}),
|
||||
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetServiceabilityDetails(mockServiceZipCode);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import alert from "@/ux-components/alert/alert";
|
|||
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getPricedRecycleFeePart,
|
||||
getServiceabilityDetails,
|
||||
getBillToAccountNumber,
|
||||
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
|
@ -164,6 +165,12 @@ export default {
|
|||
"service-location"
|
||||
);
|
||||
|
||||
// retrieve recycle fee part
|
||||
const recycleFeePart = await getPricedRecycleFeePart(
|
||||
serviceZipCode,
|
||||
"service-location"
|
||||
);
|
||||
|
||||
// retrieve serviceability details
|
||||
const serviceabilityDetails = await getServiceabilityDetails(
|
||||
serviceZipCode,
|
||||
|
|
@ -177,6 +184,7 @@ export default {
|
|||
|
||||
// update content related to service zip code
|
||||
this.$emit("updated-mobile-fee-part", mobileFeePart);
|
||||
this.$emit("updated-recycle-fee-part", recycleFeePart);
|
||||
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
||||
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
||||
this.$emit("updated-bill-to-account-number", billToAccountNumber);
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -729,7 +729,8 @@ export const mutations = {
|
|||
// if we're loading a session and we do not have a provider number yet, then set isInsurance to null so that
|
||||
// a service package is not selected by default on the quote page.
|
||||
if (
|
||||
!sessionInformation.order.payment.isInsurance &&
|
||||
(sessionInformation.order.payment.isInsurance === null ||
|
||||
sessionInformation.order.payment.isInsurance === undefined) &&
|
||||
!sessionInformation.order.serviceLocation.provider?.providerNumber
|
||||
) {
|
||||
state.order.payment.isInsurance = null;
|
||||
|
|
@ -1770,6 +1771,18 @@ export const actions = {
|
|||
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 }) {
|
||||
const damage = context.getters.damage;
|
||||
const damageType = damage.isRepair ? "Repair" : "Replace";
|
||||
|
|
|
|||
Loading…
Reference in a new issue