Merge branch 'release/2025.11.06' into feature/CASH-1712

This commit is contained in:
AdamCaouetteSafelite 2025-10-28 15:54:38 -04:00
commit 4d705ab3f1
10 changed files with 247 additions and 445 deletions

View file

@ -309,40 +309,6 @@ describe("cart.vue", () => {
expect(found).toBe(true); 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 // 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", () => { 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 // Arrange

View file

@ -312,16 +312,30 @@ export default {
this.lineItems[category] = this.lineItems[category].filter( this.lineItems[category] = this.lineItems[category].filter(
(lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType (lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType
); );
let shouldSaveSupportingItems = category == cartItemCategories.SUPPORTING_ITEMS;
if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) { if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) {
this.saveVaps(this.lineItems); 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( await this.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
this.lineItems.supportingItems, this.lineItems.supportingItems,
false false
); );
} }
this.$emit("itemRemoved");
}, },
async saveVaps(lineItems) { async saveVaps(lineItems) {
@ -470,14 +484,7 @@ export default {
} }
if (this.servicePackageDiscountCartItem) { if (this.servicePackageDiscountCartItem) {
if (this.discountPackageNames != this.packageLevel) { cartItems.push(this.servicePackageDiscountCartItem);
this.removeItem(
this.servicePackageDiscountCartItem.cartItemType,
cartItemCategories.SUPPORTING_ITEMS
);
} else {
cartItems.push(this.servicePackageDiscountCartItem);
}
} }
if (this.premiumAppointmentDiscountCartItem) { if (this.premiumAppointmentDiscountCartItem) {

View file

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

View file

@ -99,3 +99,25 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) {
return pricingResults[0]; 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" :isItac="isItac"
:isNoComp="isNoComp" :isNoComp="isNoComp"
:isExpandedOnLoad="false" :isExpandedOnLoad="false"
:isMSRFeeApplicable="isMSRFeeApplicable" /> :isMSRFeeApplicable="isMSRFeeApplicable"
@itemRemoved="evaluatePromosAndTaxItemsOnOrder" />
<afterpayBreakout <afterpayBreakout
v-if="isAfterpayBreakoutDisplay" v-if="isAfterpayBreakoutDisplay"
@ -154,7 +155,6 @@ import {
getNewlyInactivatedPromos, getNewlyInactivatedPromos,
} from "@/helpers/promotions-helper"; } from "@/helpers/promotions-helper";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { deepClone } from "@/helpers/object-helper"; import { deepClone } from "@/helpers/object-helper";
import { Form } from "vee-validate"; import { Form } from "vee-validate";
@ -162,18 +162,10 @@ import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { partTypeStrings } from "@/constants/part-type-strings";
import { mapTaxedLineItemsToStoreFormat } from "../../store";
import { coverageStatus } from "@/constants/insurance"; import { coverageStatus } from "@/constants/insurance";
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
import { containsRecalParts } from "@/helpers/recal-helper"; import { containsRecalParts } from "@/helpers/recal-helper";
import { getBoolFromString } from "@/helpers/boolean-helper"; import { getBoolFromString } from "@/helpers/boolean-helper";
import { import { getAmountDue, addPricesToLineItems } from "@/helpers/pricing-helper.js";
getDisplayAmountDue,
getAmountDue,
getSubTotal,
getSalesTax,
} from "@/helpers/pricing-helper.js";
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash"; import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
import { debugLog } from "@/helpers/debug-log-helper"; import { debugLog } from "@/helpers/debug-log-helper";
import { ErrorMessage } from "vee-validate"; import { ErrorMessage } from "vee-validate";
@ -182,6 +174,23 @@ import { Field } from "vee-validate";
defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
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 { export default {
name: "paymentMethod", name: "paymentMethod",
props: { props: {
@ -191,32 +200,14 @@ export default {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.name); const cmsContentPromise = fetchCmsContentForPage(to.name);
const lineItemsFromStore = deepClone(store.getters.order.lineItems); const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
const frontWipersOnOrder = {
lineItemsFromStore.vaps.filter( serviceZipCode: store.getters.order.serviceLocation.zipCode,
(wiper) => wiper.partType == partTypeStrings.FRONT_WIPER carId: store.getters.vehicle.carId,
) ?? []; },
"payment-method"
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 rainRepelPromise = baseMixin.methods.dispatchStoreActionWithLogging( const rainRepelPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_REPEL, storeActions.GET_RAIN_REPEL,
@ -224,9 +215,6 @@ export default {
"payment-method" "payment-method"
); );
// const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
// (removed temporarily for Heritage parity effort)
const promiseResultMap = [ const promiseResultMap = [
{ {
resultKey: "cmsContent", resultKey: "cmsContent",
@ -240,57 +228,40 @@ export default {
resultKey: "rainRepel", resultKey: "rainRepel",
promise: rainRepelPromise, promise: rainRepelPromise,
}, },
// {
// resultKey: "reviewDropdownData",
// promise: reviewDropdownPromise,
// },
// (removed temporarily for Heritage parity effort)
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
let lineItemsFromStore = deepClone(store.getters.order.lineItems);
const glassParts = lineItemsFromStore.glassParts ?? []; const glassParts = lineItemsFromStore.glassParts ?? [];
const supportingItems = lineItemsFromStore.supportingItems ?? []; const supportingItems = lineItemsFromStore.supportingItems ?? [];
const vaps = lineItemsFromStore.vaps ?? []; const vaps = lineItemsFromStore.vaps ?? [];
// if the order already has wipers on it from the quote page, use those as the available wipers const availableVaps = [resultMap.rainRepel, ...resultMap.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 allLineItems = [ const lineItemsOnOrderAndAvailableVaps = [
resultMap.rainRepel, ...availableVaps,
...supportingItems,
...availableFrontWipers,
...availableRearWipers,
...glassParts, ...glassParts,
...supportingItems,
...vaps, ...vaps,
]; ];
const lineItemsToTax = Array.from( // All line items are already priced except availableVaps
new Map(allLineItems.map((item) => [item.partNumber, item])).values() // 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
const availableVaps = [ let pricedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
resultMap.rainRepel,
...availableFrontWipers,
...availableRearWipers,
];
const pricedLineItemsToTax = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{ {
availableLineItems: lineItemsToTax, availableLineItems: lineItemsOnOrderAndAvailableVaps,
}, },
"payment-method", "payment-method",
false false
); );
pricedLineItems = getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems);
// Add prices to the availableVaps
const pricedAvailableVaps = addPricesToLineItems(availableVaps, pricedLineItems);
// Promo logic // Promo logic
// Populate the previous state of promos for toast message usage in "next()" // 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 oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0);
const promoCodeFromQueryString = consumeQueryFromStash(queryStrings.PROMO); const promoCodeFromQueryString = consumeQueryFromStash(queryStrings.PROMO);
// New promos are saved to store with this
const { validatePromoResponse, revalidatePromoResponse } = const { validatePromoResponse, revalidatePromoResponse } =
await revalidatePromosAndValidateQueryStringPromo( await revalidatePromosAndValidateQueryStringPromo(
promoCodeFromQueryString, promoCodeFromQueryString,
pricedLineItemsToTax, lineItemsOnOrderAndAvailableVaps,
"payment-method" "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 // 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, storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{ {
billToAccountNumber: store.getters.payment.billToAccountNumber, billToAccountNumber: store.getters.payment.billToAccountNumber,
@ -322,31 +309,17 @@ export default {
serviceLocationCity: store.getters.order.serviceLocation.city, serviceLocationCity: store.getters.order.serviceLocation.city,
serviceLocationState: store.getters.order.serviceLocation.state, serviceLocationState: store.getters.order.serviceLocation.state,
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
pricedLineItems: pricedLineItemsToTax, pricedLineItems: lineItemsForCart,
}, },
"payment-method", "payment-method",
false 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. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.availableVaps = taxedVaps; vm.availableVaps = pricedAvailableVaps;
vm.lineItems = lineItems; vm.lineItems = lineItemsForCart;
vm.inactivePromos = removeCurrentlyActivePromoCodesFromInactivePromos( vm.inactivePromos = removeCurrentlyActivePromoCodesFromInactivePromos(
vm.lineItems.promos, vm.lineItems.promos,
vm.inactivePromos vm.inactivePromos
@ -641,6 +614,28 @@ export default {
this.pageName 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() { hasSubmittedOrder() {
return baseMixin.methods.hasSubmittedOrder(); return baseMixin.methods.hasSubmittedOrder();
}, },
@ -861,7 +856,6 @@ export default {
) { ) {
return; return;
} }
if (oldValue.promos.length < newValue.promos.length) { if (oldValue.promos.length < newValue.promos.length) {
const oldPromoCodes = oldValue.promos.map( const oldPromoCodes = oldValue.promos.map(
(promoObject) => promoObject.promoCode (promoObject) => promoObject.promoCode

View file

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

View file

@ -42,7 +42,7 @@
<cart <cart
ref="cart" ref="cart"
:damage="damageInfo" :damage="damageInfo"
:availableVaps="availableVaps" :availableVaps="[]"
:allowItemRemoval="false" :allowItemRemoval="false"
v-model="lineItems" v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
@ -237,25 +237,11 @@ import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants"; import { paymentMethods } from "@/constants/payment-method-constants";
import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import baseMixin from "@/mixins/base-mixin.js"; 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 { deepClone } from "@/helpers/object-helper";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.js"; import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.js";
import { coverageStatus } from "@/constants/insurance"; import { coverageStatus } from "@/constants/insurance";
import { getBoolFromString } from "@/helpers/boolean-helper.js"; import { getDisplayAmountDue, getAmountDue } from "@/helpers/pricing-helper.js";
import {
getDisplayAmountDue,
getAmountDue,
getSubTotal,
getSalesTax,
} from "@/helpers/pricing-helper.js";
import { debugLog } from "@/helpers/debug-log-helper.js"; import { debugLog } from "@/helpers/debug-log-helper.js";
import buttonQuestion from "@/digital-components/button-question/button-question"; 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"; 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(), displayAmount: this.getDisplayAmountDue(),
piaLineItems: "", piaLineItems: "",
lineItems: [], lineItems: [],
availableVaps: [],
shouldBlockInteraction: false, shouldBlockInteraction: false,
paymentMethodListButton: paymentMethodListButton, paymentMethodListButton: paymentMethodListButton,
showSwitchPaymentMethod: !this.isAfterpay(), showSwitchPaymentMethod: !this.isAfterpay(),
@ -321,21 +306,6 @@ export default {
"payment" "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 = [ const promiseResultMap = [
{ {
resultKey: "cmsContent", resultKey: "cmsContent",
@ -345,92 +315,14 @@ export default {
resultKey: "signature", resultKey: "signature",
promise: signaturePromise, promise: signaturePromise,
}, },
{
resultKey: "wipers",
promise: wipersPromise,
},
{
resultKey: "rainRepel",
promise: rainRepelPromise,
},
]; ];
const resultMap = await settleAllPromises(promiseResultMap); 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. // Call the "next" function to complete the transition to this page.
next(async (vm) => { next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.availableVaps = taxedVaps; vm.lineItems = deepClone(store.getters.order.lineItems);
vm.lineItems = lineItems;
vm.$nextTick(() => { vm.$nextTick(() => {
if (vm.$refs.cart) { if (vm.$refs.cart) {

View file

@ -65,6 +65,7 @@
pageName="quote" pageName="quote"
:taxPromos="false" :taxPromos="false"
:useDefaultCashParentAccount="true" :useDefaultCashParentAccount="true"
@promoAdded="promoAdded"
modalWidgetName="PromoModalWidget" /> modalWidgetName="PromoModalWidget" />
</div> </div>
</div> </div>
@ -576,9 +577,6 @@ export default {
isInsuranceContinueButtonText() { isInsuranceContinueButtonText() {
return this.getCmsContent("isInsuranceContinueButtonText", "Text"); return this.getCmsContent("isInsuranceContinueButtonText", "Text");
}, },
lineItemsCloneForWatcher() {
return Object.assign({}, this.lineItems);
},
isRecalibrationOnOrder() { isRecalibrationOnOrder() {
return store.getters.isRecalibrationOnOrder; return store.getters.isRecalibrationOnOrder;
}, },
@ -854,31 +852,9 @@ export default {
await this.forwardButtonAction(); await this.forwardButtonAction();
} }
}, },
}, promoAdded(lineItems) {
watch: { const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
lineItemsCloneForWatcher: { this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
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,
}, },
}, },
components: { components: {

View file

@ -460,111 +460,47 @@ export default {
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
const isPricingByDayExperiment = experimentMixin.methods.hasSettingEqualTo( const serviceZipCode = store.getters.order.serviceLocation.zipCode;
experimentSettings.PRICING_BY_DAY,
"true"
);
const showPricingByDay = !store.getters.payment.isInsurance && isPricingByDayExperiment;
// Get pricingByDayBasePrice needed for Pricing By Day
const lineItems = deepClone(store.getters.order.lineItems);
const isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder;
const shouldHideRecalibration =
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.RECAL_PRICE_REMOVE,
"true"
) && isRecalibrationOnOrder;
const glassParts =
isRecalibrationOnOrder && shouldHideRecalibration
? getItemsWithoutRecalParts(lineItems.glassParts)
: (lineItems.glassParts ?? []);
const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers(
lineItems.supportingItems,
{
partNumbersToRemove: [
partNumberStrings.RECYCLE_FEE,
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 cmsContentPromise = fetchCmsContentForPage(to.name);
const alertReasonsPromise = locationAlerts.methods.loadInitialData( const alertReasonsPromise = locationAlerts.methods.loadInitialData(
store.getters.order.serviceLocation.zipCodeCtu, store.getters.order.serviceLocation.zipCodeCtu,
store.getters.order.serviceLocation.provider?.address?.zipCodeCtu store.getters.order.serviceLocation.provider?.address?.zipCodeCtu
); );
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const zipCodeDataPromise = getZipCodeData(serviceZipCode, to.name);
const serviceabilityDetailsPromise = getServiceabilityDetails( const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode, serviceZipCode,
null, 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( const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_PREMIUM_FEE, storeActions.GET_MOBILE_PREMIUM_FEE,
null, null,
to.name 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 // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
{ {
@ -575,18 +511,6 @@ export default {
resultKey: "alertReasons", resultKey: "alertReasons",
promise: alertReasonsPromise, promise: alertReasonsPromise,
}, },
{
resultKey: "pricingByDayUpchargePart",
promise: pricingByDayUpchargePartPromise,
},
{
resultKey: "premiumFeeWithPrice",
promise: premiumFeeWithPricePromise,
},
{
resultKey: "zipCodeData",
promise: zipCodeDataPromise,
},
{ {
resultKey: "mobileFeePart", resultKey: "mobileFeePart",
promise: mobileFeePartPromise, promise: mobileFeePartPromise,
@ -595,39 +519,75 @@ export default {
resultKey: "serviceabilityDetails", resultKey: "serviceabilityDetails",
promise: serviceabilityDetailsPromise, promise: serviceabilityDetailsPromise,
}, },
{
resultKey: "premiumFee",
promise: premiumFeePromise,
},
]; ];
const resultMap = await settleAllPromises(promiseResultMap); 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 let pricedMobileFeePart = null;
const pricingByDayUpcharge = let pricedPremiumFee = null;
showPricingByDay && resultMap.pricingByDayUpchargePart
? await baseMixin.methods.getTotalLineItemPrice( if (itemsToPrice.length) {
resultMap.pricingByDayUpchargePart, const pricedItems = await baseMixin.methods.dispatchStoreActionWithLogging(
false storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
) {
: null; 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. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons); vm.$refs.locationAlerts.initializeComponent(resultMap.alertReasons);
vm.mobilePremiumAppointmentFee = resultMap.premiumFeeWithPrice vm.mobilePremiumAppointmentFee = pricedPremiumFee;
? resultMap.premiumFeeWithPrice[0]
: null;
vm.updateFooterButtonText(vm.selectedTimeSlotInfo); vm.updateFooterButtonText(vm.selectedTimeSlotInfo);
vm.pricingByDayUpchargeLineItem = resultMap.pricingByDayUpchargePart; vm.pricingByDayUpchargeLineItem = null; // Pricing By Day Upcharge Line Item is not used in this version
vm.includePricingByDayUpcharge = includePricingByDayUpcharge; vm.includePricingByDayUpcharge = false; // Pricing By Day Upcharge is not used in this version
vm.isPricingByDayExperiment = isPricingByDayExperiment; vm.isPricingByDayExperiment = false; // Pricing By Day Experiment is not used in this version
vm.pricingByDayBasePrice = pricingByDayBasePrice; vm.pricingByDayBasePrice = null; // Pricing By Day Base Price is not used in this version
vm.pricingByDayUpcharge = pricingByDayUpcharge; vm.pricingByDayUpcharge = null; // Pricing By Day Upcharge is not used in this version
vm.showPricingByDay = showPricingByDay; vm.showPricingByDay = false; // Pricing By Day is not used in this version
vm.preSelectedDate = preSelectedDate; vm.preSelectedDate = preSelectedDate;
vm.appointmentType = appointmentType; vm.appointmentType = appointmentType;
<<<<<<< HEAD
vm.setDataOnLoad( vm.setDataOnLoad(
resultMap.zipCodeData, resultMap.zipCodeData,
=======
vm.setData(
zipCodeData,
>>>>>>> release/2025.11.06
resultMap.serviceabilityDetails, resultMap.serviceabilityDetails,
resultMap.mobileFeePart, pricedMobileFeePart,
shopProviderData.data shopProviderData.data
); );
vm.initializeDatePicker(); vm.initializeDatePicker();

View file

@ -59,6 +59,7 @@ import {
} from "@/helpers/recal-helper"; } from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters"; import { externalParameterStatus } from "@/constants/external-parameters";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { addPricesToLineItems } from "@/helpers/pricing-helper";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
@ -3063,8 +3064,14 @@ export const actions = {
pageNameToLog, pageNameToLog,
} }
) { ) {
const arrayOfLineItems = [
...(pricedLineItems.glassParts ?? []),
...(pricedLineItems.promos ?? []),
...(pricedLineItems.supportingItems ?? []),
...(pricedLineItems.vaps ?? []),
];
const flattenedLineItemsWithChildParts = const flattenedLineItemsWithChildParts =
getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems); getFlattenedArrayOfLineItemsWithChildParts(arrayOfLineItems);
const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({ const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({
partNumber: lineItem.partNumber, partNumber: lineItem.partNumber,
@ -3113,8 +3120,12 @@ export const actions = {
}); });
context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData); context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
Object.keys(pricedLineItems).forEach((key) => {
pricedLineItems = addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems); pricedLineItems[key] = addTaxesToPricedLineItems(
pricedLineItems[key] ?? [],
response.data.taxedLineItems
);
});
return pricedLineItems; return pricedLineItems;
}, },
@ -3796,26 +3807,6 @@ function convertGlassPieceNamingFromApi(glassArray) {
return 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 = []) { function addTaxesToPricedLineItems(pricedLineItems, taxingLineItems = []) {
pricedLineItems.forEach((pricedLineItem) => { pricedLineItems.forEach((pricedLineItem) => {
const lineItemIndex = taxingLineItems.findIndex( const lineItemIndex = taxingLineItems.findIndex(