Merge remote-tracking branch 'origin/develop' into rlsmerge/develop-to-heavy.truck

This commit is contained in:
Scott Kiener 2025-03-26 12:18:45 -04:00
commit a311a9ba6b
38 changed files with 1135 additions and 490 deletions

View file

@ -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",

View file

@ -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 };

View file

@ -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,
};

View file

@ -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",

View file

@ -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 };

View file

@ -135,7 +135,7 @@ export default {
},
closeModal() {
const modal = Modal.getInstance(document.getElementById(this.modalId));
modal.hide();
modal?.hide();
this.$emit("isModalOpened", false);
},
},

View file

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

View file

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

View file

@ -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) {

View file

@ -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() {

View 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];
}

View 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");
});
});
});

View file

@ -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) {

View file

@ -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,

View file

@ -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) {

View file

@ -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 };
}

View file

@ -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>

View 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
});
});

View 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>

View file

@ -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", () => {

View file

@ -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);

View file

@ -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.

View file

@ -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: {

View file

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

View file

@ -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 =

View file

@ -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(

View file

@ -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 = {};

View file

@ -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,

View file

@ -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>

View file

@ -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(

View file

@ -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);
}),

View file

@ -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);

View file

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

View file

@ -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);
}),

View file

@ -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);

View file

@ -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" });

View file

@ -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 }) {

View file

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