Merge pull request #2927 from Safelite/rlsmerge/2025.11.06-to-develop

Rlsmerge/2025.11.06 to develop
This commit is contained in:
CarlNation 2025-11-01 09:33:45 -04:00 committed by GitHub
commit 04b1713ba2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 648 additions and 1884 deletions

View file

@ -4,6 +4,7 @@ const experimentUniverses = {
MSR: "MSR",
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
};
const experimentSettings = {
@ -28,6 +29,10 @@ const experimentSettings = {
DISPLAY_AFTERPAY_BREAKOUT_DISPLAY: "Display_AfterpayBreakoutDisplay",
AFTERPAY_EXTENDED_PAY_OPTION_THRESHOLD: "AfterPayExtendedPayOptionThreshold",
DYNAMO_LOGGING: "DynamoLogging",
SHOW_MOBILE_FIRST_APPT: "ShowMobileFirstAppt",
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
};
const experimentTriggers = {

View file

@ -14,6 +14,7 @@
<div class="modal-dialog" :class="{ 'modal-dialog-centered': !isRecal }">
<div class="modal-content">
<div class="modal-header mb-0 mt-6">
<slot name="modal-header-slot"></slot>
<label
v-if="headerText"
class="modal-title d-flex justify-content-center pb-0 w-100">

View file

@ -309,40 +309,6 @@ describe("cart.vue", () => {
expect(found).toBe(true);
});
// Service package discount Fee Cart Item
test("only if there is service package discount fee for the package, a service package discount cart item should be added to the cart", () => {
// Arrange
const lineItems = {
glassParts: [],
supportingItems: [
{
description: null,
id: "bc00294e-6baa-403e-866e-52c267187a15",
kitPrice: 0,
laborAmount: 0,
partNumber: "DISC CASHSAVE70",
partType: "SERVICE PACKAGE DISCOUNT",
salesTax: null,
sellingPrice: -70,
},
],
vaps: [],
promos: [],
};
const availableVaps = [];
// Act
const { wrapper } = setupMocks({
props: {
modelValue: lineItems,
availableVaps: availableVaps,
},
});
// Assert
expect(wrapper.vm.servicePackageDiscountCartItem).toBeNull();
});
// Other Supporting Items Cart Item
test("if there are other supporting items on the order, an other supporting items cart item should be added to the cart but should not be displayed", () => {
// Arrange

View file

@ -312,16 +312,30 @@ export default {
this.lineItems[category] = this.lineItems[category].filter(
(lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType
);
let shouldSaveSupportingItems = category == cartItemCategories.SUPPORTING_ITEMS;
if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) {
this.saveVaps(this.lineItems);
// Check if service package discount should be removed after vaps change
if (
this.servicePackageDiscountCartItem &&
this.discountPackageNames != this.packageLevel
) {
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.servicePackageDiscountCartItem.cartItemType
);
shouldSaveSupportingItems = true;
}
}
if (category == cartItemCategories.SUPPORTING_ITEMS) {
if (shouldSaveSupportingItems) {
await this.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
this.lineItems.supportingItems,
false
);
}
this.$emit("itemRemoved");
},
async saveVaps(lineItems) {
@ -470,14 +484,7 @@ export default {
}
if (this.servicePackageDiscountCartItem) {
if (this.discountPackageNames != this.packageLevel) {
this.removeItem(
this.servicePackageDiscountCartItem.cartItemType,
cartItemCategories.SUPPORTING_ITEMS
);
} else {
cartItems.push(this.servicePackageDiscountCartItem);
}
cartItems.push(this.servicePackageDiscountCartItem);
}
if (this.premiumAppointmentDiscountCartItem) {

View file

@ -290,8 +290,18 @@ export default {
);
if (promoCodeData.isValid) {
if (this.taxPromos) {
const pricedLineItemsToTax = [];
pricedLineItemsToTax.push(...promoCodeData.promoCode); // promoCodeData.promoCode should be an array
const vapsToAdd = getVapsThatNeedToBeAddedToSatisfyPromos(
promoCodeData.promoCode,
this.addableVaps,
this.lineItems
);
const pricedLineItemsToTax = {
glassParts: this.lineItems.glassParts,
promos: promoCodeData.promoCode,
supportingItems: this.lineItems.supportingItems,
vaps: [...this.lineItems.vaps, ...vapsToAdd],
};
const taxedLineItems =
await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
@ -314,25 +324,10 @@ export default {
// Match all line items to the line items as they are in the store
// and rebuild the original structure.
this.lineItems = mapTaxedLineItemsToStoreFormat(
taxedLineItems,
this.lineItems
);
const taxedVaps = mapTaxedLineItemsToStoreFormat(
taxedLineItems,
this.addableVaps
);
const getVaps = getVapsThatNeedToBeAddedToSatisfyPromos(
promoCodeData.promoCode,
taxedVaps,
this.lineItems
);
this.lineItems.vaps?.push(...getVaps);
this.lineItems = taxedLineItems;
} else {
this.lineItems?.promos.push(...promoCodeData.promoCode);
}
this.lineItems?.promos.push(...promoCodeData.promoCode);
this.$emit("promoAdded", this.lineItems);
this.closeModal();

View file

@ -99,3 +99,25 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) {
return pricingResults[0];
}
export function addPricesToLineItems(lineItems, pricingLineItems) {
lineItems.forEach((lineItem) => {
const lineItemIndex = pricingLineItems.findIndex(
(pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber
);
if (lineItem.childParts) {
addPricesToLineItems(lineItem.childParts, pricingLineItems);
}
if (lineItemIndex !== -1) {
const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0];
lineItem.laborAmount = pricedLineItem.laborAmount;
lineItem.sellingPrice = pricedLineItem.sellingPrice;
lineItem.kitPrice = pricedLineItem.kitPrice;
lineItem.salesTax = pricedLineItem.salesTax;
}
});
return lineItems;
}

View file

@ -34,7 +34,8 @@
:isItac="isItac"
:isNoComp="isNoComp"
:isExpandedOnLoad="false"
:isMSRFeeApplicable="isMSRFeeApplicable" />
:isMSRFeeApplicable="isMSRFeeApplicable"
@itemRemoved="evaluatePromosAndTaxItemsOnOrder" />
<afterpayBreakout
v-if="isAfterpayBreakoutDisplay"
@ -154,7 +155,6 @@ import {
getNewlyInactivatedPromos,
} from "@/helpers/promotions-helper";
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { deepClone } from "@/helpers/object-helper";
import { Form } from "vee-validate";
@ -162,18 +162,10 @@ import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { partTypeStrings } from "@/constants/part-type-strings";
import { mapTaxedLineItemsToStoreFormat } from "../../store";
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";
import { getAmountDue, addPricesToLineItems } from "@/helpers/pricing-helper.js";
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
import { debugLog } from "@/helpers/debug-log-helper";
import { ErrorMessage } from "vee-validate";
@ -182,6 +174,23 @@ import { Field } from "vee-validate";
defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
function getFlattenedArrayOfLineItemsWithChildParts(lineItems, childPartRecursiveCall = false) {
let flattenedArray = [];
lineItems?.forEach((lineItem) => {
// this assumes childparts will never be a glass part
lineItem.isChildPart = childPartRecursiveCall;
flattenedArray.push(lineItem);
if (lineItem.childParts) {
flattenedArray = [
...flattenedArray,
...getFlattenedArrayOfLineItemsWithChildParts(lineItem.childParts, true),
];
}
});
return flattenedArray;
}
export default {
name: "paymentMethod",
props: {
@ -191,32 +200,14 @@ export default {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.name);
const lineItemsFromStore = deepClone(store.getters.order.lineItems);
const frontWipersOnOrder =
lineItemsFromStore.vaps.filter(
(wiper) => wiper.partType == partTypeStrings.FRONT_WIPER
) ?? [];
const rearWipersOnOrder =
lineItemsFromStore.vaps.filter(
(wiper) => wiper.partType == partTypeStrings.REAR_WIPER
) ?? [];
const orderHasFrontWipers = frontWipersOnOrder.length > 0;
const orderHasRearWipers = rearWipersOnOrder.length > 0;
const wipersPromise =
!orderHasFrontWipers || !orderHasRearWipers
? baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
{
serviceZipCode: store.getters.order.serviceLocation.zipCode,
carId: store.getters.vehicle.carId,
},
"payment-method"
)
: Promise.resolve([]);
const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
{
serviceZipCode: store.getters.order.serviceLocation.zipCode,
carId: store.getters.vehicle.carId,
},
"payment-method"
);
const rainRepelPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_REPEL,
@ -224,9 +215,6 @@ export default {
"payment-method"
);
// const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
// (removed temporarily for Heritage parity effort)
const promiseResultMap = [
{
resultKey: "cmsContent",
@ -240,57 +228,40 @@ export default {
resultKey: "rainRepel",
promise: rainRepelPromise,
},
// {
// resultKey: "reviewDropdownData",
// promise: reviewDropdownPromise,
// },
// (removed temporarily for Heritage parity effort)
];
const resultMap = await settleAllPromises(promiseResultMap);
let lineItemsFromStore = deepClone(store.getters.order.lineItems);
const glassParts = lineItemsFromStore.glassParts ?? [];
const supportingItems = lineItemsFromStore.supportingItems ?? [];
const vaps = lineItemsFromStore.vaps ?? [];
// if the order already has wipers on it from the quote page, use those as the available wipers
// instead of what comes from the backend. This is to prevent issues with part interchange.
const availableFrontWipers = orderHasFrontWipers
? frontWipersOnOrder
: (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.FRONT_WIPER) ??
[]);
const availableRearWipers = orderHasRearWipers
? rearWipersOnOrder
: (resultMap.wipers.filter((wiper) => wiper.partType == partTypeStrings.REAR_WIPER) ??
[]);
const availableVaps = [resultMap.rainRepel, ...resultMap.wipers];
const allLineItems = [
resultMap.rainRepel,
...supportingItems,
...availableFrontWipers,
...availableRearWipers,
const lineItemsOnOrderAndAvailableVaps = [
...availableVaps,
...glassParts,
...supportingItems,
...vaps,
];
const lineItemsToTax = Array.from(
new Map(allLineItems.map((item) => [item.partNumber, item])).values()
);
const availableVaps = [
resultMap.rainRepel,
...availableFrontWipers,
...availableRearWipers,
];
const pricedLineItemsToTax = await baseMixin.methods.dispatchStoreActionWithLogging(
// All line items are already priced except availableVaps
// Price everything again to ensure that serverData has all values
// Specifically this addresses an error where insurance client glass parts are not in serverData
// See CASH-1713 for details
let pricedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: lineItemsToTax,
availableLineItems: lineItemsOnOrderAndAvailableVaps,
},
"payment-method",
false
);
pricedLineItems = getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems);
// Add prices to the availableVaps
const pricedAvailableVaps = addPricesToLineItems(availableVaps, pricedLineItems);
// Promo logic
// Populate the previous state of promos for toast message usage in "next()"
@ -298,22 +269,38 @@ export default {
const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0);
const promoCodeFromQueryString = consumeQueryFromStash(queryStrings.PROMO);
// New promos are saved to store with this
const { validatePromoResponse, revalidatePromoResponse } =
await revalidatePromosAndValidateQueryStringPromo(
promoCodeFromQueryString,
pricedLineItemsToTax,
lineItemsOnOrderAndAvailableVaps,
"payment-method"
);
// update lineItemsFromStore with newly added promos
let lineItemsForCart = deepClone(store.getters.order.lineItems);
delete lineItemsForCart.serverData;
// Add newly validated promos to the array to get taxed
const newValidatedPromos = validatePromoResponse?.orderPromos ?? [];
newValidatedPromos.push(
...(revalidatePromoResponse ? revalidatePromoResponse.promoLineItems : [])
);
pricedLineItemsToTax.push(...newValidatedPromos);
// End of promo logic
const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos(
lineItemsForCart.promos ?? [],
availableVaps,
lineItemsForCart
);
// Add prices to all line items, sometimes they are not there when they come back from heritage
// See CASH-1713 for details
lineItemsForCart.glassParts = addPricesToLineItems(
lineItemsForCart.glassParts ?? [],
pricedLineItems
);
lineItemsForCart.supportingItems = addPricesToLineItems(
lineItemsForCart.supportingItems ?? [],
pricedLineItems
);
lineItemsForCart.vaps = addPricesToLineItems(lineItemsForCart.vaps ?? [], pricedLineItems);
lineItemsForCart.vaps.push(...vapsToAddToCart);
// Tax items on order
lineItemsForCart = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: store.getters.payment.billToAccountNumber,
@ -322,31 +309,17 @@ export default {
serviceLocationCity: store.getters.order.serviceLocation.city,
serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: pricedLineItemsToTax,
pricedLineItems: lineItemsForCart,
},
"payment-method",
false
);
// Match all line items to the line items as they are in the store
// and rebuild the original structure.
const lineItems = mapTaxedLineItemsToStoreFormat(taxedLineItems, lineItemsFromStore);
const taxedVaps = mapTaxedLineItemsToStoreFormat(taxedLineItems, availableVaps);
lineItems.promos = newValidatedPromos ?? [];
const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos(
newValidatedPromos,
taxedVaps,
lineItems
);
lineItems.vaps = lineItems.vaps ?? [];
lineItems.vaps.push(...vapsToAddToCart);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.availableVaps = taxedVaps;
vm.lineItems = lineItems;
vm.availableVaps = pricedAvailableVaps;
vm.lineItems = lineItemsForCart;
vm.inactivePromos = removeCurrentlyActivePromoCodesFromInactivePromos(
vm.lineItems.promos,
vm.inactivePromos
@ -516,24 +489,6 @@ export default {
});
}
if (revalidatePromoResponse.promoLineItems.length > 0) {
await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: store.getters.payment.billToAccountNumber,
providerNumber:
this.$store.getters.order.serviceLocation.provider.providerNumber,
appointmentType: this.$store.getters.order.serviceLocation.appointmentType,
serviceLocationCity: this.$store.getters.order.serviceLocation.city,
serviceLocationState: this.$store.getters.order.serviceLocation.state,
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
pricedLineItems: revalidatePromoResponse.promoLineItems,
},
"payment-method",
false
);
}
const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos(
revalidatePromoResponse.promoLineItems,
this.availableVaps,
@ -547,6 +502,24 @@ export default {
getPromoCodeWithoutBundleIdentifier(x.promoCode)
);
if (revalidatePromoResponse.promoLineItems.length > 0) {
await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: store.getters.payment.billToAccountNumber,
providerNumber:
this.$store.getters.order.serviceLocation.provider.providerNumber,
appointmentType: this.$store.getters.order.serviceLocation.appointmentType,
serviceLocationCity: this.$store.getters.order.serviceLocation.city,
serviceLocationState: this.$store.getters.order.serviceLocation.state,
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
pricedLineItems: this.lineItems,
},
"payment-method",
false
);
}
// SAVE PROMO CHANGES TO STORE
this.dispatchStoreAction(
storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS,
@ -641,6 +614,28 @@ export default {
this.pageName
);
},
async evaluatePromosAndTaxItemsOnOrder() {
//Revalidate promos if there are any inactive, or active promos
const hasInactivePromos = this.inactivePromos.length > 0;
const hasActivePromos = this.lineItems.promos.length > 0;
if (hasInactivePromos || hasActivePromos) {
await this.revalidatePromos();
}
this.lineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: store.getters.payment.billToAccountNumber,
providerNumber: store.getters.order.serviceLocation.provider.providerNumber,
appointmentType: store.getters.order.serviceLocation.appointmentType,
serviceLocationCity: store.getters.order.serviceLocation.city,
serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: this.lineItems,
},
"payment-method",
false
);
},
hasSubmittedOrder() {
return baseMixin.methods.hasSubmittedOrder();
},
@ -861,7 +856,6 @@ export default {
) {
return;
}
if (oldValue.promos.length < newValue.promos.length) {
const oldPromoCodes = oldValue.promos.map(
(promoObject) => promoObject.promoCode

View file

@ -319,7 +319,6 @@ describe("payment.vue", () => {
);
// Assert
expect(vmMock.availableVaps).not.toBeUndefined();
expect(vmMock.lineItems).not.toBeUndefined();
expect(vmMock.setCmsContent).toBeCalled();

View file

@ -42,7 +42,7 @@
<cart
ref="cart"
:damage="damageInfo"
:availableVaps="availableVaps"
:availableVaps="[]"
:allowItemRemoval="false"
v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
@ -237,25 +237,11 @@ import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import baseMixin from "@/mixins/base-mixin.js";
import { mapTaxedLineItemsToStoreFormat } from "../../store";
import {
revalidatePromosAndValidateQueryStringPromo,
getAddableVapsFromAvailableLineItems,
getVapsThatNeedToBeAddedToSatisfyPromos,
} from "@/helpers/promotions-helper";
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { deepClone } from "@/helpers/object-helper";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.js";
import { coverageStatus } from "@/constants/insurance";
import { getBoolFromString } from "@/helpers/boolean-helper.js";
import {
getDisplayAmountDue,
getAmountDue,
getSubTotal,
getSalesTax,
} from "@/helpers/pricing-helper.js";
import { getDisplayAmountDue, getAmountDue } from "@/helpers/pricing-helper.js";
import { debugLog } from "@/helpers/debug-log-helper.js";
import buttonQuestion from "@/digital-components/button-question/button-question";
import paymentMethodListButton from "@/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button";
@ -290,7 +276,6 @@ export default {
displayAmount: this.getDisplayAmountDue(),
piaLineItems: "",
lineItems: [],
availableVaps: [],
shouldBlockInteraction: false,
paymentMethodListButton: paymentMethodListButton,
showSwitchPaymentMethod: !this.isAfterpay(),
@ -321,21 +306,6 @@ export default {
"payment"
);
const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
{
serviceZipCode: store.getters.order.serviceLocation.zipCode,
carId: store.getters.vehicle.carId,
},
"payment"
);
const rainRepelPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_REPEL,
null,
"payment"
);
const promiseResultMap = [
{
resultKey: "cmsContent",
@ -345,92 +315,14 @@ export default {
resultKey: "signature",
promise: signaturePromise,
},
{
resultKey: "wipers",
promise: wipersPromise,
},
{
resultKey: "rainRepel",
promise: rainRepelPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const lineItemsFromStore = deepClone(store.getters.order.lineItems);
const glassParts = lineItemsFromStore.glassParts ?? [];
const supportingItems = lineItemsFromStore.supportingItems ?? [];
const lineItemsToTax = [
resultMap.rainRepel,
...supportingItems,
...resultMap.wipers,
...glassParts,
];
const availableVaps = [resultMap.rainRepel, ...resultMap.wipers];
const pricedLineItemsToTax = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: lineItemsToTax,
},
"payment",
false
);
// Promo logic
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
const { validatePromoResponse, revalidatePromoResponse } =
await revalidatePromosAndValidateQueryStringPromo(
promoCodeFromQueryString,
pricedLineItemsToTax,
"payment"
);
const newValidatedPromos = validatePromoResponse?.orderPromos ?? [];
newValidatedPromos.push(
...(revalidatePromoResponse ? revalidatePromoResponse.promoLineItems : [])
);
pricedLineItemsToTax.push(...newValidatedPromos);
// End of promo logic
const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: store.getters.payment.billToAccountNumber,
providerNumber: store.getters.order.serviceLocation.provider.providerNumber,
appointmentType: store.getters.order.serviceLocation.appointmentType,
serviceLocationCity: store.getters.order.serviceLocation.city,
serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: pricedLineItemsToTax,
},
"payment",
false
);
// Match all line items to the line items as they are in the store
// and rebuild the original structure.
const lineItems = mapTaxedLineItemsToStoreFormat(taxedLineItems, lineItemsFromStore);
const taxedVaps = mapTaxedLineItemsToStoreFormat(taxedLineItems, availableVaps);
// Add items that were not in the store yet but added via query string promo validation
//newValidatedPromos contains both validated and revalidated promos.
lineItems.promos = Array.from(newValidatedPromos);
const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos(
newValidatedPromos,
taxedVaps,
lineItems
);
lineItems.vaps = lineItems.vaps ?? [];
lineItems.vaps.push(...vapsToAddToCart);
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.availableVaps = taxedVaps;
vm.lineItems = lineItems;
vm.lineItems = deepClone(store.getters.order.lineItems);
vm.$nextTick(() => {
if (vm.$refs.cart) {

View file

@ -65,6 +65,7 @@
pageName="quote"
:taxPromos="false"
:useDefaultCashParentAccount="true"
@promoAdded="promoAdded"
modalWidgetName="PromoModalWidget" />
</div>
</div>
@ -576,9 +577,6 @@ export default {
isInsuranceContinueButtonText() {
return this.getCmsContent("isInsuranceContinueButtonText", "Text");
},
lineItemsCloneForWatcher() {
return Object.assign({}, this.lineItems);
},
isRecalibrationOnOrder() {
return store.getters.isRecalibrationOnOrder;
},
@ -854,31 +852,9 @@ export default {
await this.forwardButtonAction();
}
},
},
watch: {
lineItemsCloneForWatcher: {
handler(newValue, oldValue) {
if (
!oldValue ||
oldValue.length == 0 ||
!oldValue.vaps ||
!newValue ||
newValue.length == 0
) {
return;
}
if (oldValue.promos.length < newValue.promos.length) {
const oldPromoCodes = oldValue.promos.map(
(promoObject) => promoObject.promoCode
);
const newlyActivatedPromoCodes = newValue.promos.filter(
(newPromo) => !oldPromoCodes.includes(newPromo.promoCode)
);
const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
}
},
deep: true,
promoAdded(lineItems) {
const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
},
},
components: {

View file

@ -0,0 +1,222 @@
<template>
<div id="mobile-first-modal-container">
<modal
:ref="modalName"
:headerText="modalHeaderText"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:isFooterButtonPrimary="true"
@footer-button-event="confirmAppointment"
@isModalOpened="setIsModalOpen">
<p class="modal-body-inner" v-html="modalBodyText"></p>
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot:modal-footer-slot>
<button
type="button"
class="btn btn-link"
id="see-more-options"
:disabled="isLoading"
@click="closeModal">
{{ buttonText }}
</button>
</template>
</modal>
</div>
</template>
<script>
// Supporting files
import store from "@/store";
import modal from "@/digital-components/modal/modal";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import { get12HourTimeFormat } from "@/helpers/date-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
const INLINE_SERVICETYPE_TOKEN = "custom:serviceType";
const INLINE_DAY_TOKEN = "custom:day";
const INLINE_DATE_TOKEN = "custom:date";
const INLINE_TIMESLOT_TOKEN = "custom:timeslot";
const INLINE_SERVICE_LENGTH_TOKEN = "custom:serviceLength";
export default {
name: "mobile-first-modal",
props: {
cmsWidgetName: String,
modalWidgetName: String,
},
data() {
return {
isModalOpen: false,
selectedAppontment: {
estimatedServiceMinutes: {
minimum: null,
maximum: null,
},
timeSlot: null,
date: null,
},
};
},
methods: {
splitCopyOnCMSPlaceHolder,
get12HourTimeFormat,
openModal() {
this.modal.openModal();
},
onModalClosed() {
this.$emit("close-mobile-first-modal");
},
closeModal() {
this.modal.closeModal();
},
confirmAppointment() {
let selectedTimeSlot = {
timeSlot: this.selectedAppontment.timeSlot,
routeCode: this.selectedAppontment.timeSlot.id,
};
this.$emit("confirm-appointment", selectedTimeSlot);
this.modal.closeModal();
},
setSelectedAppointment(appointment) {
this.selectedAppontment = appointment;
},
getIsModalOpen() {
return this.isModalOpen;
},
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
},
computed: {
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
modalSubHeaderText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
var tokens = this.splitCopyOnCMSPlaceHolder(
this.getCmsContent(this.modalWidgetName, "BodyText")
);
tokens.forEach((token) => {
if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
const serviceType = "<strong>" + this.getServiceType + "</strong>";
tokens.splice(tokens.indexOf(token), 1, serviceType);
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppontment.date) {
const day =
"<strong>" +
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", {
weekday: "long",
timeZone: "UTC",
}) +
"</strong>";
tokens.splice(tokens.indexOf(token), 1, day);
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) {
const date = new Date(this.selectedAppontment.date).toLocaleDateString(
"en-US",
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
);
tokens.splice(tokens.indexOf(token), 1, date);
} else if (
token.includes(INLINE_TIMESLOT_TOKEN) &&
this.selectedAppontment.timeSlot
) {
const timeslot =
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.startTime) +
" - " +
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.endTime);
tokens.splice(tokens.indexOf(token), 1, timeslot);
} else if (
token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
this.selectedAppontment.estimatedServiceMinutes
) {
const inshopDurationTime = getDisplayTextForDurationLength(
this.selectedAppontment.estimatedServiceMinutes.minimum,
this.selectedAppontment.estimatedServiceMinutes.maximum
);
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime);
}
});
return tokens.join("");
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
buttonText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
getServiceType() {
return store.getters.order.damage.isRepair ? "repair" : "replace";
},
},
components: { modal },
};
</script>
<style lang="scss">
#mobile-first-modal-container {
.modal-component {
@include media-breakpoint-up(md) {
.modal-dialog {
left: 0;
align-content: center;
flex-wrap: wrap;
width: 22.063rem;
transform: translate(0, 0);
.modal-content {
border-radius: $border-radius-lg;
}
}
}
.modal-dialog {
.modal-content {
.modal-header {
flex-direction: column;
& > span {
color: $red;
font-size: $font-size-14;
font-weight: $font-weight-600;
text-transform: uppercase;
}
.modal-title {
font-size: $font-size-20;
font-weight: $font-weight-normal;
}
}
.modal-body {
padding: 0.5rem 1rem;
.modal-body-inner {
ul {
list-style: none;
padding: 1rem;
background-color: #f4f4f4;
font-size: $font-size-14;
}
}
}
.modal-footer {
flex-flow: wrap-reverse;
#see-more-options {
width: 100%;
text-decoration: none;
margin-top: 1.5rem;
font-weight: $font-weight-600;
}
}
}
}
}
}
</style>

View file

@ -1,782 +0,0 @@
<!-- OLD SCHEDULE BEGINS -->
<!-- TODO: REMOVE THIS FILE ONCE CASH-803 (SERVICE-LOCATION AND SCHEDULE PAGE COMBINATION) HAS BEEN VETTED -->
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles page-schedule">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4" />
<template v-if="ChangeShopLink.length">
<textBlock
cmsWidgetName="ChangeShopLink"
justifyText="center"
class="mb-5 text-link-small change-location"
marginTopSizeOverride="1" />
</template>
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker
customComponentId="dateQuestion"
selectableDatesSetting="custom"
ref="datePicker"
v-model="selectedDate"
class="text-link-small"
:customSelectableDatesCallback="getAvailableDatesMethod"
validationRules="date-required"
@date-clicked="handleDateClicked"
:pricingByDayBasePrice="pricingByDayBasePrice"
:pricingByDayUpcharge="pricingByDayUpcharge"
:showPricingByDay="showPricingByDay"
:isPricingByDayExperiment="isPricingByDayExperiment"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate"
:appointmentType="appointmentType"
:premiumAppointmentFee="mobilePremiumAppointmentFee"
:estimatedServiceMinutesMinimum="
selectableDatesData.estimatedServiceMinutesMinimum
"
:estimatedServiceMinutesMaximum="
selectableDatesData.estimatedServiceMinutesMaximum
"
@TimeSlotSelected="updateTimeSlot"
:displayWaitList="displayWaitList"
@waitListRequested="handleWaitListRequested" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
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 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 locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
import textBlock from "@/digital-components/text-block/text-block";
// Supporting files
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import {
calcDaysBetweenDates,
convertDateStringToDate,
sumDateString,
} from "@/layouts/schedule/helpers/schedule-helper";
import {
AppointmentTypeStrings,
PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE,
} from "@/constants/schedule-constants";
import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/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";
import { debugLog } from "@/helpers/debug-log-helper";
// DEFINE VALIDATION RULES
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
// Define constants
const TIME_SLOTS_CALL_DAYS_LIMIT = 34; // needs to be 34 for API limits (35 does not consistently work)
const getAvailableDates = async (
startDateString,
endDateString,
appointmentType,
providerNumber
) => {
const apiEndDateLimit = sumDateString(startDateString, TIME_SLOTS_CALL_DAYS_LIMIT);
const difference = calcDaysBetweenDates(startDateString, endDateString);
const apiCallsCount = Math.ceil(difference / TIME_SLOTS_CALL_DAYS_LIMIT);
const storeActionConfigs = [];
const timeSlotsData = {};
timeSlotsData.days = [];
let apiStartDate = startDateString;
let apiEndDate = endDateString;
for (let i = 1; i <= apiCallsCount; i++) {
let storeActionConfig;
if (i > 1) {
apiStartDate = sumDateString(apiEndDate, 1);
apiEndDate = sumDateString(apiStartDate, TIME_SLOTS_CALL_DAYS_LIMIT);
if (i === apiCallsCount) {
apiEndDate = endDateString;
}
} else {
if (apiEndDate > apiEndDateLimit) {
apiEndDate = apiEndDateLimit;
}
}
if (appointmentType === AppointmentTypeStrings.MOBILE) {
storeActionConfig = {
storeAction: storeActions.GET_MOBILE_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate,
},
};
} else {
storeActionConfig = {
storeAction: storeActions.GET_SHOP_TIME_SLOTS,
payload: {
startDate: apiStartDate,
endDate: apiEndDate,
shopAppointmentType: appointmentType,
providerNumber: providerNumber,
},
};
}
if (apiStartDate < apiEndDate) storeActionConfigs.push(storeActionConfig);
}
const timeSlotsResponsesData = {
days: [],
};
function compareDayStrings(a, b) {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return 0;
}
const makeParallelCalls = async () => {
await Promise.all(
storeActionConfigs.map(async (storeAction) => {
const timeSlotsResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
storeAction.storeAction,
storeAction.payload,
"schedule",
false
);
timeSlotsResponsesData.estimatedServiceMinutesMinimum =
timeSlotsResponse.data.estimatedServiceMinutesMinimum;
timeSlotsResponsesData.estimatedServiceMinutesMaximum =
timeSlotsResponse.data.estimatedServiceMinutesMaximum;
timeSlotsResponsesData.days = [
...timeSlotsResponsesData.days,
...timeSlotsResponse.data.days,
];
})
);
};
return makeParallelCalls().then(() => {
// sort days chronologically
timeSlotsResponsesData.days.sort(compareDayStrings);
return timeSlotsResponsesData;
});
};
export default {
name: "schedule",
data() {
return {
selectedDate: this.getSelectedDate(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
selectableDatesData: [],
mobilePremiumAppointmentFee: null,
waitListRequested: null,
displayWaitList: null,
pricingByDayUpchargeLineItem: null,
includePricingByDayUpcharge: null,
isPricingByDayExperiment: null,
pricingByDayBasePrice: null,
pricingByDayUpcharge: null,
showPricingByDay: null,
};
},
async beforeRouteEnter(to, from, next) {
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);
let includePricingByDayUpcharge = false;
// Check to see if date should be pre-selected
let preSelectedSlot = await store.getters.order.schedule;
if (!preSelectedSlot.date || preSelectedSlot?.date?.length < 1) {
preSelectedSlot = null;
} else {
// Check to see if pre-selected date should have pricing by day upcharge
if (showPricingByDay) {
// is this preSelectedDate a higher priced pricingByDay day?
const dayIndex = convertDateStringToDate(preSelectedSlot?.date).getDay();
const dayObject = DAYS_OF_WEEK[dayIndex];
if (dayObject.isPricingByDayUpchargeDay) {
includePricingByDayUpcharge = true;
}
}
}
// Set up promises
const cmsContentPromise = fetchCmsContentForPage(to.name);
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 datePicker.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: 2,
customSelectableDatesCallback: getAvailableDates,
preSelectedDate: preSelectedSlot ? preSelectedSlot.date : preSelectedSlot,
});
// 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(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: [result.data],
},
"schedule",
false
);
} else {
return result.data;
}
});
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "alertReasons",
promise: alertReasonsPromise,
},
{
resultKey: "datePickerInitialData",
promise: datePickerInitialDataPromise,
},
{
resultKey: "pricingByDayUpchargePart",
promise: pricingByDayUpchargePartPromise,
},
{
resultKey: "premiumFeeWithPrice",
promise: premiumFeeWithPricePromise,
},
];
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(datePickerInitialData);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.selectableDatesData = datePickerInitialData.initialShopTimeSlotsResponse;
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
? resultMap.premiumFeeWithPrice[0]
: 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;
});
},
mounted() {
this.$nextTick(() => {
const selectableDatesData = this.selectableDatesData;
// if no date selected on load
if (!this.selectedDate) {
if (selectableDatesData?.days?.length > 0) {
this.selectedDate = selectableDatesData.days[0].date;
} else {
setTimeout(() => {
this.$refs.datePicker.showAnotherMonth().then((moreSelectableDatesData) => {
this.selectableDatesData = moreSelectableDatesData;
if (selectableDatesData?.days?.length > 0) {
this.selectedDate = selectableDatesData.days[0].date;
}
this.setDisplayWaitList();
});
}, 50);
}
}
});
},
computed: {
ChangeShopLinkText() {
return this.getCmsContent("ChangeShopLink", "Text");
},
ChangeShopLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
},
appointmentType() {
return this.$store.getters.order.serviceLocation.appointmentType;
},
timeSlotsForSelectedDate() {
if (!this.selectedDate) return null;
return this.selectableDatesData.days?.find(
(selectableDate) => selectableDate.date === this.selectedDate
);
},
},
methods: {
splitCopyOnCMSPlaceHolder,
arePagePrerequisitesValid() {
const serviceLocation = store.getters.order.serviceLocation;
const serviceLocationPreReqs =
serviceLocation.zipCode &&
serviceLocation.zipCodeCtu &&
serviceLocation.appointmentType &&
(serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE ||
serviceLocation.provider.providerNumber);
const paymentInfo = store.getters.payment.isInsurance !== null;
const damageInfo =
store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0);
const preReqResult = serviceLocationPreReqs && paymentInfo && damageInfo;
// prettier-ignore
{
debugLog("--- schedule.vue pagePrereqs start ---", null, !preReqResult);
debugLog("store.getters.order.serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult);
debugLog("store.getters.order.serviceLocation.zipCodeCtu:", serviceLocation.zipCodeCtu, !preReqResult);
debugLog("store.getters.order.serviceLocation.appointmentType:", serviceLocation.appointmentType, !preReqResult);
debugLog("store.getters.order.serviceLocation.provider.providerNumber:", serviceLocation.provider?.providerNumber, !preReqResult);
debugLog("store.getters.payment.isInsurance:", store.getters.payment?.isInsurance, !preReqResult);
debugLog("store.getters.order.damage.isRepair:", store.getters.order.damage?.isRepair, !preReqResult);
debugLog("store.getters.order.lineItems.glassParts:", store.getters.order.lineItems?.glassParts, !preReqResult);
debugLog("--- schedule.vue pagePrereqs end ---", null, !preReqResult);
}
return preReqResult;
},
async getAvailableDatesMethod(startDate, endDate) {
const newShopTimeSlots = await getAvailableDates(
startDate,
endDate,
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
);
return newShopTimeSlots;
},
getAvailableDates,
getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu;
},
getSelectedDate() {
return store.getters.order.schedule.date;
},
getSelectedTimeSlotInfo() {
const supportingItems = this.getSupportingItems();
var isPremiumAppointment = false;
if (supportingItems) {
isPremiumAppointment =
!!supportingItems.filter(
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
).length > 0;
}
const selectedTimeSlotInfo = {
timeSlot: store.getters.order.schedule,
isPremiumAppointment: isPremiumAppointment,
};
return selectedTimeSlotInfo;
},
getSupportingItems() {
return store.getters.lineItems.supportingItems;
},
updateFooterButtonText(timeSlotInfo) {
let navbarButtonText;
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
navbarButtonText = "Continue";
} else {
navbarButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(
timeSlotInfo.timeSlot.date
)}`;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.startTime
)}`;
} else if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
!timeSlotInfo.isPremiumAppointment
) {
navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.startTime,
true
)} - ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.endTime,
true
)}`;
}
}
this.$refs.navbar.updateButtonText(navbarButtonText);
},
convertSelectedDateToShortMonthAndDay(selectedDate) {
// This conversion ensures we don't get get GMT induced date changes
const dateObject = convertDateStringToDate(selectedDate);
// Ex: April 25
return dateObject.toLocaleDateString("en-us", { month: "short", day: "numeric" });
},
getDisplayTextForMilitaryTime(militaryTimeInput, shouldTrimMinutesIfEmpty = false) {
// Expected input: "HH:MM"
let hours = parseInt(militaryTimeInput.split(":")[0]);
const minutes = militaryTimeInput.split(":")[1];
const meridianNotation = hours > 11 ? "PM" : "AM";
if (hours > 12) {
hours -= 12;
}
if (shouldTrimMinutesIfEmpty && minutes === "00") {
return `${hours} ${meridianNotation}`;
} else {
return `${hours}:${minutes} ${meridianNotation}`;
}
},
backButtonAction() {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_BACK,
this.pageName
);
},
forwardButtonAction() {
this.updateSupportingItems();
if (this.displayWaitList) {
var gaLabel = "";
const status = this.waitListRequested ? "checked" : "unchecked";
const type = store.getters.isMobileAppointment ? "mobile" : "inshop";
gaLabel = `${status}_${type}`;
const currentDate = new Date();
const dateString = this.selectableDatesData.days[0].date;
const [year, month, day] = dateString.split("-").map(Number);
const appointmentDate = new Date(year, month - 1, day);
const timeDifference = appointmentDate - currentDate;
// Convert the time difference from milliseconds to days
const daysUntilAppointment = Math.ceil(timeDifference / (1000 * 60 * 60 * 24));
gaLabel +=
"_" +
daysUntilAppointment.toString() +
"_" +
store.getters.order.serviceLocation.zipCode +
"_" +
store.getters.order.serviceLocation.zipCodeCtu;
this.pushEventToGA("waitlist", "add_to_waitlist_check_box_status", gaLabel, true);
}
if (this.selectedTimeSlotInfo.timeSlot.jobMinMinutes == null) {
this.selectedTimeSlotInfo.timeSlot.jobMinMinutes =
this.selectableDatesData?.estimatedServiceMinutesMinimum?.toString();
this.selectedTimeSlotInfo.timeSlot.jobMaxMinutes =
this.selectableDatesData?.estimatedServiceMinutesMaximum?.toString();
}
this.dispatchStoreAction(
this.storeActions.SAVE_SCHEDULE,
this.selectedTimeSlotInfo.timeSlot,
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.pageName
);
},
setDisplayWaitList() {
if (
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.DISPLAY_WAITLIST,
"true"
) &&
this.selectableDatesData.days[0]
) {
const dateString = this.selectableDatesData.days[0].date;
const [year, month, day] = dateString.split("-").map(Number);
const targetDate = new Date(year, month - 1, day);
const currentDate = new Date();
const futureDate = new Date(currentDate);
const experimentThresholdDays = experimentMixin.methods.hasSetting(
experimentSettings.WAITLIST_THRESHOLD_DAYS
)
? parseInt(
experimentMixin.methods.getSettingValue(
experimentSettings.WAITLIST_THRESHOLD_DAYS
)
)
: 0;
futureDate.setDate(currentDate.getDate() + experimentThresholdDays);
if (targetDate >= futureDate) {
this.displayWaitList = true;
} else {
this.displayWaitList = false;
}
}
},
updateSupportingItems() {
const supportingItems = this.getSupportingItems();
// if we have a pricing by day upcharge, then save/update supporting items with it
const pricingByDayUpchargeFeeIndex = supportingItems?.findIndex(
(item) => item.partType == PRICING_BY_DAY_PART_TYPE
);
if (this.includePricingByDayUpcharge && this.showPricingByDay) {
if (pricingByDayUpchargeFeeIndex && 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) {
// remove pricing by day upcharge if it already was in store
supportingItems.splice(pricingByDayUpchargeFeeIndex, 1);
}
}
// if we have a premium fee(early bird), then save/update supporting items
if (
this.appointmentType === AppointmentTypeStrings.MOBILE &&
this.selectedTimeSlotInfo?.isPremiumAppointment
) {
const premiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (premiumFeeIndex > -1) {
supportingItems[premiumFeeIndex].laborAmount =
this.mobilePremiumAppointmentFee.laborAmount;
supportingItems[premiumFeeIndex].sellingPrice =
this.mobilePremiumAppointmentFee.sellingPrice;
supportingItems[premiumFeeIndex].kitPrice =
this.mobilePremiumAppointmentFee.kitPrice;
} else {
supportingItems.push(this.mobilePremiumAppointmentFee);
}
} else {
if (!supportingItems) {
return;
}
// if it's not a mobile and/or premium early bird, then make sure we remove any that may have been added
const removePremiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (removePremiumFeeIndex >= 0) {
supportingItems.splice(removePremiumFeeIndex, 1);
}
}
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;
}
},
updateTimeSlot(timeSlot) {
this.selectedTimeSlotInfo = timeSlot;
},
},
watch: {
selectedDate(newValue, oldValue) {
// Clear time slot selection if date selected changes
if (newValue !== oldValue) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
routeCode: null,
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
isPremiumAppointment: null,
};
}
},
selectedTimeSlotInfo(newValue) {
this.updateFooterButtonText(newValue);
},
},
components: {
funnelHeader,
navbar,
funnelSubHeader,
Form,
loadingModal,
datePicker,
locationAlerts,
textBlock,
},
};
</script>
<style lang="scss">
.container-fluid {
&.page-schedule {
padding: 0 1rem;
.text-link-small {
a,
.btn-link {
width: auto;
margin: 0 auto;
height: auto;
font-size: 0.875rem;
line-height: 1.75;
padding: 0;
border-radius: 0;
&:focus {
outline: 1px solid $blue;
}
@include media-breakpoint-up(md) {
font-size: 1rem;
}
}
}
.funnel-sub-header {
h5.dark-header {
margin-bottom: 0.25rem;
}
}
.change-location a {
font-family: AvertaSemibold;
}
.time-slots-question {
padding: 0 0.75rem;
}
}
}
</style>
<!-- OLD SCHEDULE ENDS -->

View file

@ -1,616 +0,0 @@
// Components
import schedule from "@/layouts/schedule/schedule.vue";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import router from "@/router";
import baseMixin from "../../mixins/base-mixin";
// Mock basemixin
jest.mock("@/mixins/base-mixin.js", () => ({
methods: {
dispatchStoreAction: jest.fn().mockImplementation((storeAction) => {
if (storeAction === "getShopTimeSlots" || storeAction === "getMobileTimeSlots") {
return {
data: {
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 120,
days: [
{
date: "2023-12-01",
timeSlots: [
{
id: "06747-01820-S-B*20424*7 AM",
startTime: "07:00",
endTime: "08:00",
offerPremium: false,
},
],
},
],
},
};
}
if (storeAction === "getMobilePremiumFee") {
return Promise.resolve({
data: {
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
});
}
if (storeAction === "priceOrderItemsAndSaveServerData") {
return Promise.resolve([
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
]);
}
if (storeAction === "saveSupportingItemsSuppressingStateResetting") {
return Promise.resolve([
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
]);
}
}),
dispatchStoreActionWithLogging: jest.fn().mockImplementation((storeAction) => {
if (storeAction === "getShopTimeSlots" || storeAction === "getMobileTimeSlots") {
return {
data: {
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 120,
days: [
{
date: "2023-12-01",
timeSlots: [
{
id: "06747-01820-S-B*20424*7 AM",
startTime: "07:00",
endTime: "08:00",
offerPremium: false,
},
],
},
],
},
};
}
if (storeAction === "getMobilePremiumFee") {
return Promise.resolve({
data: {
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
});
}
if (storeAction === "priceOrderItemsAndSaveServerData") {
return Promise.resolve([
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
]);
}
if (storeAction === "saveSupportingItemsSuppressingStateResetting") {
return Promise.resolve([
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
]);
}
}),
filterOutCertainPartTypesOrNumbers: jest.fn(),
hasSubmittedOrder: jest.fn(),
getTotalPriceOfAllLineItemsAndChildParts: jest.fn(),
getTotalLineItemPrice: jest.fn(),
},
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: () => Promise.resolve("content"),
splitCopyOnCMSPlaceHolder: jest.fn(() => ["A", "B"]),
}));
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
store.getters = {
applicationUser: {
experiments: [],
},
order: {
schedule: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: "000",
},
lineItems: {
glassParts: [
{
partNumber: "ABC123",
},
],
supportingItems: [],
},
serviceLocation: {
appointmentType: "Inshop",
zipCode: "12345",
zipCodeCtu: "01234",
provider: {
providerNumber: "123",
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
policy: {
policyNumber: "123",
},
},
payment: {
isInsurance: true,
},
lineItems: {
glassParts: [],
supportingItems: [],
},
experimentSettings: {},
vehicle: {
carId: "123",
},
};
});
afterEach(() => {
store.getters = {};
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe("schedule.vue...", () => {
describe("initial load", () => {
test("should pass arePagePrerequisitesValid with a mobile CASH order and no providerNumber", () => {
// Arrange
const { wrapper } = setupMocks({});
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.order.serviceLocation.provider.policyNumber = null;
store.getters.payment.isInsurance = false;
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("should pass arePagePrerequisitesValid with an inshop order and providerNumber", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("should fail arePagePrerequisitesValid with a replace with no glass parts", async () => {
// Arrange
const { wrapper } = setupMocks({});
store.getters.order.lineItems.glassParts = [];
// Act
const arePagePrerequisitesValid2 = await wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid2).toBe(false);
});
test("should fail arePagePrerequisitesValid without isInsurance", () => {
// Arrange
const { wrapper } = setupMocks({});
store.getters.payment.isInsurance = null;
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(false);
});
test("should return timeslots when getMoreScheduleData is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [],
};
wrapper.vm.selectableDatesMobile = {
days: [],
};
// Act
const newShopTimeSlots = await wrapper.vm.getMoreScheduleData(
"2023-01-01",
"2023-01-31"
);
// Assert
expect(newShopTimeSlots).toStrictEqual({
inshopTimeSlotsData: {
days: [
{
date: "2023-12-01",
timeSlots: [
{
endTime: "08:00",
id: "06747-01820-S-B*20424*7 AM",
offerPremium: false,
startTime: "07:00",
},
],
},
],
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 120,
},
mobileTimeSlotsData: {
days: [
{
date: "2023-12-01",
timeSlots: [
{
endTime: "08:00",
id: "06747-01820-S-B*20424*7 AM",
offerPremium: false,
startTime: "07:00",
},
],
},
],
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 120,
},
});
});
test("should call API service in day ranges of 34 or less when getMoreScheduleData is called with large date ranges", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [],
};
wrapper.vm.selectableDatesMobile = {
days: [],
};
// Act
await wrapper.vm.getMoreScheduleData.call(
wrapper.vm,
"2023-01-01",
"2023-03-31",
"Inshop",
"123"
);
// Assert
expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledTimes(6);
expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
"getShopTimeSlots",
expect.anything(),
expect.anything(),
expect.anything()
);
});
describe("beforeRouteEnter function... ", () => {
// TODO: restore this test (temporarily removed it until CASH-845 is in QA then looping back)
xtest("should call next() and call all functions within next", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [],
};
wrapper.vm.updateFooterButtonText = jest.fn();
wrapper.vm.setDisplayWaitList = jest.fn();
const nextFunction = jest.fn((c) => {
c(wrapper.vm);
});
// Act
await schedule.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "schedule" } },
undefined,
nextFunction
);
// Assert
expect(nextFunction).toHaveBeenCalled();
expect(wrapper.vm.setCmsContent).toHaveBeenCalledWith("content");
expect(wrapper.vm.$refs.datePicker.initializeComponent).toHaveBeenCalledWith(
expect.objectContaining({
calendarViewDirection: "future",
})
);
expect(wrapper.vm.$refs.locationAlerts.initializeComponent).toHaveBeenCalled();
expect(wrapper.vm.selectableDatesInshop).toStrictEqual(
expect.objectContaining({
days: expect.any(Array),
estimatedServiceMinutesMaximum: expect.any(Number),
estimatedServiceMinutesMinimum: expect.any(Number),
})
);
expect(wrapper.vm.mobilePremiumAppointmentFee).toStrictEqual(
expect.objectContaining({
partNumber: expect.any(String),
})
);
expect(wrapper.vm.updateFooterButtonText).toHaveBeenCalled();
expect(wrapper.vm.setDisplayWaitList).toHaveBeenCalled();
});
});
describe("computed properties...", () => {
test("timeSlotsForSelectedDate should return timeslots if selected date is available", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [
{
date: "2022-11-11",
timeSlots: [
{
id: "1820I-01820-M-I*20425*AM",
startTime: "08:00",
endTime: "12:00",
offerPremium: true,
},
{
id: "1820I-01820-M-I*20425*PM",
startTime: "12:00",
endTime: "17:00",
offerPremium: false,
},
],
},
],
};
wrapper.setData({
selectedDate: "2022-11-11",
});
// Act
const testValue = wrapper.vm.timeSlotsForSelectedDate;
// Assert
expect(testValue).toStrictEqual(
expect.objectContaining({
date: "2022-11-11",
})
);
});
test("timeSlotsForSelectedDate should be null if no date has been selected", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [
{
date: "2022-11-11",
timeSlots: [
{
id: "1820I-01820-M-I*20425*AM",
startTime: "08:00",
endTime: "12:00",
offerPremium: true,
},
{
id: "1820I-01820-M-I*20425*PM",
startTime: "12:00",
endTime: "17:00",
offerPremium: false,
},
],
},
],
};
wrapper.setData({
selectedDate: undefined,
});
// Act
const testValue = wrapper.vm.timeSlotsForSelectedDate;
// Assert
expect(testValue).toBe(null);
});
});
});
describe("schedule page methods...", () => {
test("getServiceZipCtuCodeFromStore should return zipCodeCtu", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [],
};
// Act
const testValue = wrapper.vm.getServiceZipCtuCodeFromStore();
// Assert
expect(testValue).toStrictEqual("01234");
});
test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [],
};
const timeInput1 = "15:00";
const timeInput2 = "15:30";
// Act
const testOutput1 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1);
const testOutput2 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2);
const testOutput3 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1, true);
const testOutput4 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2, true);
// Assert
expect(testOutput1).toBe("3:00 PM");
expect(testOutput2).toBe("3:30 PM");
expect(testOutput3).toBe("3 PM");
expect(testOutput4).toBe("3:30 PM");
});
test("Clicking back should fire correct navigation", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesInshop = {
days: [],
};
wrapper.vm.$router.navigateWithoutSaving = jest.fn();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toBeCalledWith(
"CLICKED_BACK",
"schedule"
);
});
});
// TODO: restore this test (temporarily removed it until CASH-845 is in QA then looping back)
xtest("forwardButtonAction should call route method navigateWithoutSaving", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
wrapper.vm.$router.navigateWithSaving = jest.fn(() => {
return {};
});
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
test("for Inshop appts, updateSupportingItems should call store action to save supporting items WITHOUT the EARLY BIRD supporting item", async () => {
// Arrange
store.getters.order.serviceLocation.appointmentType = "Inshop";
store.getters.lineItems.supportingItems = [
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 0,
kitPrice: 0,
},
];
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: true,
},
});
// Act
await wrapper.vm.updateSupportingItems();
// Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith(
"saveSupportingItemsSuppressingStateResetting",
expect.not.arrayContaining([
expect.objectContaining({
partType: "EARLY BIRD",
}),
]),
expect.anything()
);
});
});
const mockCmsContent = {};
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
route: { name: "schedule" },
});
mountOptions.global.mocks["$store"] = store;
mountOptions.global.mocks["$router"] = router;
mountOptions["attachTo"] = document.body;
mountOptions.mixins = [
{
methods: {
getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => {
if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName])
return mockCmsContent[widgetName][fieldName];
}),
},
},
];
mountOptions.global.mocks.pageName = "schedule";
const wrapper = shallowMount(schedule, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.datePicker.initializeComponent = jest.fn();
wrapper.vm.$refs.datePicker.loadInitialData = jest.fn();
wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn();
wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
return { wrapper };
}

View file

@ -2,7 +2,13 @@
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container page-container-grouped-styles">
<mobileFirstModal
modalWidgetName="MobileFirstModalWidget"
ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" />
<div
class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
<div class="row">
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
<mobileFeeWaiverAlert
@ -183,6 +189,7 @@ import shopListButton from "@/layouts/service-location/shop-question/shop-list-b
import shopLocation from "@/layouts/schedule/shop-location/shop-location";
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
@ -203,7 +210,7 @@ import mobileFeeWaiverAlert from "./mobile-fee-waiver-alert/mobile-fee-waiver-al
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments";
import { experimentUniverses, experimentSettings } from "@/constants/experiments";
import {
AppointmentTypeStrings,
RouteCodeFlags,
@ -242,6 +249,11 @@ import { getAmountDue, getPricingByDayPartWithPrice } from "@/helpers/pricing-he
import { getItemsWithoutRecalParts } from "@/helpers/recal-helper";
import { deepClone } from "@/helpers/object-helper";
import { debugLog } from "@/helpers/debug-log-helper";
import {
getSessionKeyValue,
getUserIdValue,
getDeviceIdValue,
} from "@/helpers/heritage-integration/cookie-helper";
// DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => {
@ -460,111 +472,47 @@ export default {
};
},
async beforeRouteEnter(to, from, next) {
const isPricingByDayExperiment = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.PRICING_BY_DAY,
"true"
);
const showPricingByDay = !store.getters.payment.isInsurance && isPricingByDayExperiment;
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
// Get pricingByDayBasePrice needed for Pricing By Day
const lineItems = deepClone(store.getters.order.lineItems);
const isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder;
const shouldHideRecalibration =
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.RECAL_PRICE_REMOVE,
"true"
) && isRecalibrationOnOrder;
const glassParts =
isRecalibrationOnOrder && shouldHideRecalibration
? getItemsWithoutRecalParts(lineItems.glassParts)
: (lineItems.glassParts ?? []);
const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers(
lineItems.supportingItems,
{
partNumbersToRemove: [
partNumberStrings.RECYCLE_FEE,
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);
let includePricingByDayUpcharge = false;
let appointmentType = store.getters.order.serviceLocation.appointmentType;
// Check to see if date should be pre-selected
let scheduleFromStore = await store.getters.order.schedule;
let preSelectedDate;
if (scheduleFromStore.date && scheduleFromStore.date.length > 0) {
preSelectedDate = scheduleFromStore.date;
if (appointmentType === AppointmentTypeStrings.MOBILE) {
preSelectedDate += "-mobile";
}
// Check to see if pre-selected date should have pricing by day upcharge
if (showPricingByDay) {
// is this preSelectedDate a higher priced pricingByDay day?
const dayIndex = convertDateStringToDate(scheduleFromStore?.date).getDay();
const dayObject = DAYS_OF_WEEK[dayIndex];
if (dayObject.isPricingByDayUpchargeDay) {
includePricingByDayUpcharge = true;
}
}
}
// Set up promises
const cmsContentPromise = fetchCmsContentForPage(to.name);
const alertReasonsPromise = locationAlerts.methods.loadInitialData(
store.getters.order.serviceLocation.zipCodeCtu,
store.getters.order.serviceLocation.provider?.address?.zipCodeCtu
);
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name);
const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode,
null,
to.name
"schedule"
);
let zipCodeData;
const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name);
let shopProviderData;
const shopProviderDataPromise = getShopProviderData(serviceZipCode, to.name);
const zipCodeDataAndShopProviderDataPromise = Promise.all([
zipCodeDataPromise,
shopProviderDataPromise,
]);
const mobileFeePartPromise = zipCodeDataAndShopProviderDataPromise.then(
([zipCodeDataResult, shopProviderDataResult]) => {
zipCodeData = zipCodeDataResult;
shopProviderData = shopProviderDataResult;
return baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_FEE_PART,
{
serviceZipCode: serviceZipCode,
serviceZipCodeCtu: zipCodeData.zipCodeCtu,
mobileProviderNumber: shopProviderData.data.mobileProviderNumber,
},
to.name,
false
);
}
);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, to.name);
const shopProviderData = await getShopProviderData(serviceZipCode, to.name);
const providerNumber = shopProviderData?.data?.shopProviders[0]?.providerNumber;
// Get pricingByDayUpcharge needed for Pricing By Day
const pricingByDayUpchargePartPromise = showPricingByDay
? getPricingByDayPartWithPrice()
: null;
const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_PREMIUM_FEE,
null,
to.name
);
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) {
return baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: [result.data],
},
to.name,
false
);
} else {
return result.data;
}
});
// Settle promises and get results
const promiseResultMap = [
{
@ -575,18 +523,6 @@ export default {
resultKey: "alertReasons",
promise: alertReasonsPromise,
},
{
resultKey: "pricingByDayUpchargePart",
promise: pricingByDayUpchargePartPromise,
},
{
resultKey: "premiumFeeWithPrice",
promise: premiumFeeWithPricePromise,
},
{
resultKey: "zipCodeData",
promise: zipCodeDataPromise,
},
{
resultKey: "mobileFeePart",
promise: mobileFeePartPromise,
@ -595,39 +531,70 @@ export default {
resultKey: "serviceabilityDetails",
promise: serviceabilityDetailsPromise,
},
{
resultKey: "premiumFee",
promise: premiumFeePromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const itemsToPrice = [];
if (resultMap.mobileFeePart) {
itemsToPrice.push(resultMap.mobileFeePart);
}
if (resultMap.premiumFee) {
itemsToPrice.push(resultMap.premiumFee);
}
// add pricing by day data
const pricingByDayUpcharge =
showPricingByDay && resultMap.pricingByDayUpchargePart
? await baseMixin.methods.getTotalLineItemPrice(
resultMap.pricingByDayUpchargePart,
false
)
: null;
let pricedMobileFeePart = null;
let pricedPremiumFee = null;
if (itemsToPrice.length) {
const pricedItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: itemsToPrice,
},
to.name,
false
);
pricedMobileFeePart = pricedItems.find(
(item) => item.partNumber === resultMap.mobileFeePart.partNumber
);
pricedPremiumFee = pricedItems.find(
(item) => item.partNumber === resultMap.premiumFee.partNumber
);
}
let appointmentType = store.getters.order.serviceLocation.appointmentType;
// Check to see if date should be pre-selected
let scheduleFromStore = await store.getters.order.schedule;
let preSelectedDate;
if (scheduleFromStore.date && scheduleFromStore.date.length > 0) {
preSelectedDate = scheduleFromStore.date;
if (appointmentType === AppointmentTypeStrings.MOBILE) {
preSelectedDate += "-mobile";
}
}
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice
? resultMap.premiumFeeWithPrice[0]
: null;
vm.mobilePremiumAppointmentFee = pricedPremiumFee;
vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
vm.pricingByDayUpchargeLineItem = resultMap.pricingByDayUpchargePart;
vm.includePricingByDayUpcharge = includePricingByDayUpcharge;
vm.isPricingByDayExperiment = isPricingByDayExperiment;
vm.pricingByDayBasePrice = pricingByDayBasePrice;
vm.pricingByDayUpcharge = pricingByDayUpcharge;
vm.showPricingByDay = showPricingByDay;
vm.pricingByDayUpchargeLineItem = null; // Pricing By Day Upcharge Line Item is not used in this version
vm.includePricingByDayUpcharge = false; // Pricing By Day Upcharge is not used in this version
vm.isPricingByDayExperiment = false; // Pricing By Day Experiment is not used in this version
vm.pricingByDayBasePrice = null; // Pricing By Day Base Price is not used in this version
vm.pricingByDayUpcharge = null; // Pricing By Day Upcharge is not used in this version
vm.showPricingByDay = false; // Pricing By Day is not used in this version
vm.preSelectedDate = preSelectedDate;
vm.appointmentType = appointmentType;
vm.setData(
resultMap.zipCodeData,
vm.setDataOnLoad(
zipCodeData,
resultMap.serviceabilityDetails,
resultMap.mobileFeePart,
pricedMobileFeePart,
shopProviderData.data
);
vm.initializeDatePicker();
@ -890,6 +857,9 @@ export default {
)
);
},
isMobileFirstModalOpen() {
return this.isMobileSelected ? this.$refs.mobileFirstModal?.getIsModalOpen() : false;
},
},
methods: {
splitCopyOnCMSPlaceHolder,
@ -966,7 +936,7 @@ export default {
}
},
setData(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) {
setDataOnLoad(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) {
if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
this.zipCodeCtu = zipCodeData.zipCodeCtu;
@ -1178,6 +1148,7 @@ export default {
this.resetWaitlist();
this.preSelectedDate = null;
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
const oldProvider = this.selectedProvider;
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
});
@ -1189,7 +1160,12 @@ export default {
selectedProvider
);
} else {
this.updateSelectedProvider(selectedProvider);
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider.providerNumber;
if (didProviderNumberChange) {
this.updateSelectedProvider(selectedProvider);
this.initializeDatePicker();
}
}
},
async getMoreScheduleData(startDate, endDate) {
@ -1211,12 +1187,16 @@ export default {
moreShopTimeSlots.mobileTimeSlotsData.days
);
this.showMobileFirstModal();
return moreShopTimeSlots;
},
async initializeDatePicker() {
this.selectedDate = null;
const includeMobileTimeSlots = this.isServiceableMobile;
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
const datePickerInitialData = await this.$refs.datePicker.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
@ -1225,8 +1205,8 @@ export default {
preSelectedDate: this.preSelectedDate,
providerNumber: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
includeMobileTimeSlots: includeMobileTimeSlots,
includeInshopTimeSlots: includeInshopTimeSlots,
});
datePickerInitialData.pricingByDayBasePrice = this.pricingByDayBasePrice;
datePickerInitialData.pricingByDayUpcharge = this.pricingByDayUpcharge;
@ -1242,8 +1222,8 @@ export default {
} else {
// if no date is preselected on load, make sure there are some dates available
if (
this.selectableDatesInshop.days.length < 1 ||
this.selectableDatesMobile.days.length < 1
(includeInshopTimeSlots && this.selectableDatesInshop.days.length < 1) ||
(includeMobileTimeSlots && this.selectableDatesMobile.days.length < 1)
) {
await this.$nextTick();
await this.$refs.datePicker.showAnotherMonth();
@ -1667,15 +1647,9 @@ export default {
this.selectedProvider = new Provider();
this.updateSelectedProvider();
},
updateSelectedProvider(newProvider) {
if (newProvider) {
const didProviderNumberChange =
this.selectedProvider.providerNumber !== newProvider.providerNumber;
this.selectedProvider = newProvider;
if (didProviderNumberChange) {
this.initializeDatePicker();
}
updateSelectedProvider(newShopProvider) {
if (newShopProvider) {
this.selectedProvider = newShopProvider;
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
this.selectedProvider = {
providerNumber: this.shopProviderData.mobileProviderNumber.toString(),
@ -1742,6 +1716,7 @@ export default {
}
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.updateSelectedProvider();
this.showMobileFirstModal();
} else if (newAppointmentType) {
if (this.appointmentType != AppointmentTypeStrings.MOBILE) {
// Clear last shop selected if appointment type was changed in any manner other than from Mobile
@ -1755,7 +1730,6 @@ export default {
} else {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
}
// make sure a selectedProvider exists
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
} else {
@ -1767,6 +1741,113 @@ export default {
setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate();
},
showMobileFirstModal() {
if (this.appointmentType == AppointmentTypeStrings.MOBILE) {
const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
);
const pmMobileDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_PM_MOBILE_DAYS
);
const noPMMobileDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_NO_PM_MOBILE_DAYS
);
const noMobileAvailableDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_NO_MOBILE_AVAILABLE_DAYS
);
let preSelectedMobileAppointment = null;
const todaysDate = getTodayDate();
let firstMobilePMAppt = this.getFirstAvailableMobileApptByTOD("PM");
if (!firstMobilePMAppt) {
return;
}
let firstMobilePMDate = firstMobilePMAppt ? firstMobilePMAppt.date : null;
let numberOfDaysToFirstMobilePMDate =
(new Date(firstMobilePMDate) - todaysDate) / (1000 * 60 * 60 * 24);
const shouldExposeMobileFirstAppointment = () => {
return (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate <= noMobileAvailableDays
);
};
if (shouldExposeMobileFirstAppointment()) {
const mobileFirstExperiment = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.MOBILE_FIRST_APPOINTMENT
);
const hasExposedMobileFirst = mobileFirstExperiment?.isExposed;
if (!hasExposedMobileFirst && mobileFirstExperiment) {
baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
{
userId: getUserIdValue(),
deviceId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: "schedule",
experiment: mobileFirstExperiment,
},
"schedule",
false
);
}
if (isShowMobileFirstAppt) {
if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) {
preSelectedMobileAppointment = firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) {
let firstMobileAMAppt = this.getFirstAvailableMobileApptByTOD("AM");
preSelectedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt
: null;
!preSelectedMobileAppointment &&
(preSelectedMobileAppointment = firstMobilePMAppt);
}
this.$refs.mobileFirstModal.setSelectedAppointment(
preSelectedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
}
}
}
},
getFirstAvailableMobileApptByTOD(timeOfDay) {
if (!this.selectableDatesMobile?.days?.length) {
return null;
} else {
for (const dateObj of this.selectableDatesMobile.days) {
let matchingTimeSlot = {
estimatedServiceMinutes: {
minimum: this.estimatedServiceMinutesMinimum,
maximum: this.estimatedServiceMinutesMaximum,
},
timeSlot: null,
date: null,
};
const isMatchingApptDay = dateObj.timeSlots.some(
(slot) =>
slot.id.includes(timeOfDay) &&
(matchingTimeSlot.timeSlot = slot) &&
(matchingTimeSlot.date = dateObj.date)
);
if (isMatchingApptDay && matchingTimeSlot) {
return matchingTimeSlot;
}
}
return null;
}
},
updateTimeSlotandNavigateForward(timeSlotObj) {
this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction();
},
},
watch: {
appointmentTypeFromAppointmentTypeQuestion: {
@ -1811,6 +1892,7 @@ export default {
locationAlerts,
textBlock,
timeSlotQuestion,
mobileFirstModal,
alert,
serviceZipModalQuestion,
@ -1834,6 +1916,9 @@ export default {
margin-bottom: 0.25rem;
}
}
&.hidden-background {
display: none;
}
}
.alert.alert-warning {
.alert-heading {

View file

@ -280,6 +280,8 @@ export default {
sessionData.insuranceCo = order?.policy?.insuranceCompanyName;
sessionData.deductible = order?.policy?.currentDeductible;
sessionData.isVerified = order?.payment?.insuranceCoverage?.isVerified ?? false;
sessionData.coverageStatus = order?.payment?.insuranceCoverage?.coverageStatus;
sessionData.coverageSubStatus = order?.payment?.insuranceCoverage?.coverageSubStatus;
sessionData.isNoComp = order?.policy?.isNoComp;
sessionData.isItac = order?.policy?.isItac;
sessionData.subTotalPrice = getSubTotal(order?.lineItems);
@ -469,7 +471,7 @@ export default {
}
// Cash Quote or Cash Price Sub Total
payload.cashPriceSubTotal = store.getters.order?.cashPriceSubTotal ?? "";
payload.cashPriceSubTotal = order?.cashPriceSubTotal ?? "";
//unverified (in scenarios we dont display the price)
if (

View file

@ -59,6 +59,7 @@ import {
} from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters";
import { experimentSettings } from "@/constants/experiments";
import { addPricesToLineItems } from "@/helpers/pricing-helper";
// Export State
const getDefaultState = () => {
@ -1636,6 +1637,8 @@ export const actions = {
insuranceCo,
deductible,
isVerified,
coverageStatus,
coverageSubStatus,
isNoComp,
isItac,
subTotalPrice,
@ -1659,6 +1662,8 @@ export const actions = {
hasVin: hasVin,
cashOrInsuranceAccountType: cashOrInsuranceAccountType,
isVerified: isVerified,
coverageStatus: coverageStatus,
coverageSubStatus: coverageSubStatus,
damageType: damageType,
productType: productType,
eon: eon,
@ -3063,8 +3068,14 @@ export const actions = {
pageNameToLog,
}
) {
const arrayOfLineItems = [
...(pricedLineItems.glassParts ?? []),
...(pricedLineItems.promos ?? []),
...(pricedLineItems.supportingItems ?? []),
...(pricedLineItems.vaps ?? []),
];
const flattenedLineItemsWithChildParts =
getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems);
getFlattenedArrayOfLineItemsWithChildParts(arrayOfLineItems);
const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({
partNumber: lineItem.partNumber,
@ -3113,8 +3124,12 @@ export const actions = {
});
context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
pricedLineItems = addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);
Object.keys(pricedLineItems).forEach((key) => {
pricedLineItems[key] = addTaxesToPricedLineItems(
pricedLineItems[key] ?? [],
response.data.taxedLineItems
);
});
return pricedLineItems;
},
@ -3796,26 +3811,6 @@ function convertGlassPieceNamingFromApi(glassArray) {
return glassArray;
}
function addPricesToLineItems(lineItems, pricingLineItems) {
lineItems.forEach((lineItem) => {
const lineItemIndex = pricingLineItems.findIndex(
(pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber
);
if (lineItem.childParts) {
addPricesToLineItems(lineItem.childParts, pricingLineItems);
}
const pricedLineItem = pricingLineItems.splice(lineItemIndex, 1)[0];
lineItem.laborAmount = pricedLineItem.laborAmount;
lineItem.sellingPrice = pricedLineItem.sellingPrice;
lineItem.kitPrice = pricedLineItem.kitPrice;
lineItem.salesTax = pricedLineItem.salesTax;
});
return lineItems;
}
function addTaxesToPricedLineItems(pricedLineItems, taxingLineItems = []) {
pricedLineItems.forEach((pricedLineItem) => {
const lineItemIndex = taxingLineItems.findIndex(

View file

@ -123,14 +123,15 @@ $body-color: $gray-600;
//Fonts
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
$font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
monospace;
$font-family-monospace:
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
// stylelint-enable value-keyword-case
$font-family-base: $font-family-sans-serif;
$font-family-code: $font-family-monospace;
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
$font-size-12: $font-size-base * 0.75; // 12px
$font-size-14: $font-size-base * 0.875; // 14px
$font-size-20: $font-size-base * 1.25; // 20px
//Custom Font size (extra small)