Merge branch 'release/2025.05.22' into feature/CASH-45

This commit is contained in:
Chloe Herd 2025-05-08 16:20:09 -04:00
commit 9f35810aee
17 changed files with 217 additions and 99 deletions

View file

@ -23,7 +23,6 @@ const GaActions = {
SUBMITTED: "Submitted",
DISPLAYED: "Displayed",
CASH_QUOTE_DISPLAYED: "Cash quote displayed",
SERVICE_PACKAGE_PRICE: "Service package price",
MOBILE_AVAILABLE: "mobile_available",
SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
MORE_LOCATIONS_CLICKED: "more_locations_clicked",

View file

@ -69,6 +69,7 @@ const storeMutations = {
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
// EVENT BUS MUTATIONS
ADD_EVENT_TO_BUS: "addEventToBus",

View file

@ -113,12 +113,14 @@ export default {
},
onModalOpened() {
this.onModalOpenedCallback?.();
this.$emit("isModalOpened", true);
},
onModalClosed() {
this.resetButtonStyle();
this.onModalClosedCallback?.();
if (this.suppressPageScroll)
document.querySelector("body").classList.remove("prevent-modal-scroll");
this.$emit("isModalOpened", false);
},
openModal() {
if (this.suppressPageScroll)

View file

@ -65,77 +65,107 @@ export default {
}
}
.dockableContainer.showDockableContainer .sidebarBody {
button {
background: #0070d1;
color: #fff;
&:hover {
background: #06577c !important;
.dockableContainer {
&.showDockableContainer {
font-family: AvertaRegular;
color: $gray-600;
.sidebarBody {
button {
background: $blue;
color: $white;
&:hover {
background: $blue-700 !important;
}
}
.form-element__help {
color: $red;
}
.embeddedServiceSidebarFormField {
.uiInput {
.uiLabel-left {
color: $gray-600;
}
}
}
.embeddedServiceSidebarButton {
&.uiButton--inverse {
.label {
color: $white;
text-decoration: underline;
}
&:not(:disabled):focus {
background-color: $blue;
}
}
}
.embeddedServiceLiveAgentStateChatAvatar {
&.isLightningOutContext {
.agentIconColor0 {
background-color: $blue;
}
}
}
.embeddedServiceLiveAgentStateChatInputFooter {
.footerMenuWrapper {
.footer-menu-items {
.slds-dropdown__item {
> a {
padding-left: 0;
max-width: 100%;
display: flex;
justify-content: center;
}
}
}
}
}
.embeddedServiceLiveAgentStateChatInputFooter {
.footerMenuWrapper {
.footer-menu-items {
a {
margin: 0 auto;
}
}
}
}
}
.chatHeaderBranding {
background-color: $blue;
}
span {
&.required {
color: $red;
}
}
}
.embeddedServiceSidebarFormField .uiInput input {
border: 1px solid #8e9292;
}
.embeddedServiceHelpButton {
.helpButton {
.uiButton {
background-color: $blue !important;
}
}
.embeddedServiceSidebarButton.uiButton--inverse .label {
color: #fff;
text-decoration: underline;
}
.embeddedServiceLiveAgentStateChatAvatar.isLightningOutContext .agentIconColor0 {
background-color: #0070d1;
}
.embeddedServiceSidebarFormField .uiInput .uiLabel-left {
font-family: AvertaRegular;
color: #727676;
}
.form-element__help {
color: #db0020;
}
.embeddedServiceSidebarFormField .uiInput input {
border: 1px solid #06577c;
}
.embeddedServiceLiveAgentStateChatInputFooter
.footerMenuWrapper
.footer-menu-items
.slds-dropdown__item
> a {
padding-left: 0;
max-width: 100%;
display: flex;
justify-content: center;
}
.embeddedServiceLiveAgentStateChatInputFooter .footerMenuWrapper .footer-menu-items a {
margin: 0 auto;
}
.embeddedServiceSidebarButton.uiButton--inverse:not(:disabled):focus {
background-color: #0070d1;
.embeddedServiceIcon {
display: inline-block !important;
}
}
.embeddedServiceHelpButton .helpButton .uiButton {
background-color: #0070d1 !important;
}
.modalContainer.sidebarMinimized.layout-docked.embeddedServiceSidebar
.embeddedServiceSidebarMinimizedDefaultUI.helpButton,
.modalContainer.sidebarMinimized.layout-docked.embeddedServiceSidebar
.sidebarHeader.minimizedContainer.embeddedServiceSidebarMinimizedDefaultUI {
background-color: #0070d1;
border: 1px solid #0070d1;
}
.dockableContainer.showDockableContainer .chatHeaderBranding {
background-color: #0070d1;
}
.dockableContainer.showDockableContainer {
font-family: AvertaRegular;
color: #727676;
}
.dockableContainer.showDockableContainer span.required {
color: #db0020;
}
.dockableContainer.showDockableContainer .sidebarBody button:hover {
.modalContainer {
&.sidebarMinimized {
&.layout-docted {
&.embeddedServiceSidebar {
.embeddedServiceSidebarMinimizedDefaultUI {
.helpButton,
.sidebarHeader {
&.minimizedContainer {
.embeddedServiceSidebarMinimizedDefaultUI {
background-color: $blue;
border: 1px solid $blue;
}
}
}
}
}
}
}
}
</style>

View file

@ -73,6 +73,22 @@ export async function saveSession({
if (!store.getters.applicationUser.savedSessionId || shouldAwaitSaveSessionQueue) {
await saveSessionPromise;
}
const lineItems = store.getters.order?.lineItems ?? {};
const combinedLineItems = [
...(lineItems.glassParts || []),
...(lineItems.supportingItems || []),
...(lineItems.vaps || []),
...(lineItems.promos || []),
];
const cashPriceSubtotal = combinedLineItems.length
? baseMixin.methods
.getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false)
.toFixed(2)
: "";
store.commit(storeMutations.UPDATE_CASH_PRICE_SUBTOTAL, cashPriceSubtotal);
}
export async function submitWorkOrder({

View file

@ -800,8 +800,10 @@ export default {
});
this.$nextTick(() => {
const availablePackageNames = this.$refs.servicePackage?.servicePackageAnswers;
var eventLabel = "";
availablePackageNames?.forEach((tier) => {
var priceLabel = "";
if (availablePackageNames?.length > 1 || availablePackageNames?.length == 1) {
var tier = availablePackageNames[0];
if (tier) {
let lineItemsToPrice = this.availableLineItems;
if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) {
@ -826,14 +828,11 @@ export default {
baseMixin.methods.filterOutFees(lineItemsToPrice)
);
price += this.$refs.servicePackage.getVapsPrice(tier.value);
const formattedPrice = parseFloat(price).toFixed(2);
eventLabel += tier.value + "_" + formattedPrice;
priceLabel = price.toFixed(2);
}
eventLabel += ",";
});
eventLabel = eventLabel.slice(0, -1);
this.pushEventToGA("quote", this.GaActions.SERVICE_PACKAGE_PRICE, eventLabel, true);
}
const subTotalLabel = this.Variables?.CASH_SUBTOTAL;
this.pushVariableToDataLayer?.({ [subTotalLabel]: priceLabel });
});
},
async skip(insuranceSelection, packageSelection) {

View file

@ -21,6 +21,8 @@ import servicePackageRadioExperiment from "@/experiment-components/service-packa
import servicePackageRadioAfterpayExperiment from "@/experiment-components/service-package-radio-for-afterpay";
import { partTypeStrings } from "@/constants/part-type-strings";
import { packageNames } from "@/constants/package-names";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
import {
shouldFrontWipersBeAvailable,
shouldRearWipersBeAvailable,
@ -161,6 +163,7 @@ export default {
isInsuranceSelected: this.isInsuranceSelected,
},
}));
this.$emit("currentNumberOfPackages", modifiedAnswers.length);
return modifiedAnswers;
},

View file

@ -375,6 +375,10 @@ export default {
setTimeout(() => {
this.$refs.datePicker.showAnotherMonth().then((moreSelectableDatesData) => {
this.selectableDatesData = moreSelectableDatesData;
if (selectableDatesData?.days?.length > 0) {
this.selectedDate = selectableDatesData.days[0].date;
}
this.setDisplayWaitList();
});
}, 50);

View file

@ -34,6 +34,7 @@ export default {
isServiceableInshop: Boolean,
isServiceableDropoff: Boolean,
mobileFeeApplies: Boolean,
zipCode: String,
},
computed: {
questionText() {
@ -47,6 +48,7 @@ export default {
const shouldShowInshop = this.isServiceableInshop;
const shouldShowDropoff =
this.isServiceableDropoff && !this.$store.getters.damage.isRepair;
const zipCode = this.zipCode;
var answers = this.answersFromCms
? this.answersFromCms.filter((answer) => {
@ -87,12 +89,15 @@ export default {
watch: {
answersToDisplay: {
handler(newValue) {
// If there is only one option to display and that option is 'Mobile' then select it
if (
newValue.length == 1 &&
newValue.findIndex((answer) => answer.Name == "Mobile") != -1
) {
this.selectedValue = "Mobile";
// If there is only one option to display and that option is 'Mobile' or 'Inshop' then select it
if (newValue.length == 1) {
const name = newValue[0].Name;
if (
name === AppointmentTypeStrings.MOBILE ||
name === AppointmentTypeStrings.IN_SHOP
) {
this.selectedValue = name;
}
}
},
immediate: true,
@ -100,14 +105,14 @@ export default {
isMobileOnly: {
handler(newValue) {
if (newValue) {
this.selectedValue = "Mobile";
this.selectedValue = AppointmentTypeStrings.MOBILE;
}
},
},
isInshopOnly: {
handler(newValue) {
if (newValue) {
this.selectedValue = "Inshop";
this.selectedValue = AppointmentTypeStrings.IN_SHOP;
}
},
},

View file

@ -8,6 +8,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
}
const zipCodeData = await getZipCodeData(serviceZipCode);
const providerData = await getShopProviderData(serviceZipCode);
// Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging(
@ -15,6 +16,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
{
serviceZipCode: serviceZipCode,
serviceZipCodeCtu: zipCodeData.zipCodeCtu,
mobileProviderNumber: providerData.data.mobileProviderNumber,
},
pageNameToLog,
false

View file

@ -91,6 +91,7 @@ const mockStoreActionPriceOrderItemsAndSaveServerData =
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS;
const mockStoreActionGetProviders = storeActions.GET_PROVIDERS;
const mockGetShopTimeSlotsGoodAvailability = {
data: {
@ -236,6 +237,15 @@ jest.mock("@/mixins/base-mixin.js", () => ({
return Promise.resolve(mockGetShopTimeSlotsLowAvailability);
}
if (actionName === mockStoreActionGetProviders) {
return Promise.resolve({
data: {
mobileProviderNumber: "001820",
shopProviders: [{}],
},
});
}
}),
},
}));

View file

@ -97,6 +97,7 @@ export default {
"updated-contains-military-base",
"updated-bill-to-account-number",
"setMobileLocationAndProceed",
"updated-modal-opened",
],
data() {
return {
@ -228,6 +229,7 @@ export default {
},
setModalStatus(isOpened) {
this.isModalOpened = isOpened;
this.$emit("updated-modal-opened", isOpened);
},
closeModal() {
this.modal.closeModal();

View file

@ -343,7 +343,7 @@ describe("service-location.vue", () => {
zipCode: "43054",
},
isVehicleProtected: true,
isMobileSelected: true,
isMobileSelectedAndModalClosed: true,
};
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
@ -356,7 +356,7 @@ describe("service-location.vue", () => {
zipCode: "61606",
},
isVehicleProtected: null,
isMobileSelected: false,
isMobileSelectedAndModalClosed: false,
};
// Act
@ -459,7 +459,7 @@ describe("service-location.vue", () => {
zipCode: "",
},
isVehicleProtected: null,
isMobileSelected: false,
isMobileSelectedAndModalClosed: false,
};
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
const newMobileLocationQuestions = {
@ -471,7 +471,7 @@ describe("service-location.vue", () => {
zipCode: "43054",
},
isVehicleProtected: true,
isMobileSelected: true,
isMobileSelectedAndModalClosed: true,
};
wrapper.vm.closeModalAction = jest.fn();
@ -521,7 +521,7 @@ describe("service-location.vue", () => {
zipCode: "",
},
isVehicleProtected: null,
isMobileSelected: false,
isMobileSelectedAndModalClosed: false,
mobileFeePart: null,
};
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
@ -535,7 +535,7 @@ describe("service-location.vue", () => {
zipCode: "43081",
},
isVehicleProtected: true,
isMobileSelected: true,
isMobileSelectedAndModalClosed: true,
mobileFeePart: null,
};
@ -599,7 +599,7 @@ describe("service-location.vue", () => {
zipCode: "43081",
},
isVehicleProtected: "YesAnswer",
isMobileSelected: true,
isMobileSelectedAndModalClosed: true,
};
wrapper.vm.closeModalAction = jest.fn();

View file

@ -79,6 +79,7 @@
:isServiceableDropoff="isServiceableDropoff"
:isDisplayed="isAppointmentTypeDisplayed"
:mobileFeeApplies="mobileFeeApplies"
:zipCode="zipCode"
ref="appointmentTypeQuestion"
groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget"
@ -98,6 +99,7 @@
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile"
@updated-modal-opened="setMobileLocationModalOpened"
validationRules="mobile-location-required"
ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget"
@ -111,14 +113,17 @@
:selectedAppointmentType="selectedAppointmentType"
:shopProviderData="shopProviderData"
:isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget" />
cmsWidgetName="ShopQuestionWidget"
:isInshopOnly="isInshopOnly" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid || displayNoShopsAlert"
:isForwardActionDisabled="
!meta.valid || displayNoShopsAlert || mobileLocationModalOpened
"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
@ -172,7 +177,7 @@ const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
// DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => {
if (
value.isMobileSelected &&
value.isMobileSelectedAndModalClosed &&
(!value.addressQuestions.streetAddress ||
!value.addressQuestions.city ||
!value.addressQuestions.state ||
@ -211,6 +216,7 @@ export default {
billToAccountNumber: null,
shopProviderData: null,
navigatingForward: false,
mobileLocationModalOpened: false,
};
},
async beforeRouteEnter(to, from, next) {
@ -301,7 +307,9 @@ export default {
zipCode: this.zipCode,
},
isVehicleProtected: this.isVehicleProtected,
isMobileSelected: this.selectedAppointmentType === "Mobile",
isMobileSelectedAndModalClosed:
this.selectedAppointmentType === "Mobile" &&
!this.mobileLocationModalOpened,
};
},
},
@ -415,6 +423,11 @@ export default {
isNoComp() {
return store.getters.order.policy.isNoComp;
},
isInshopOnly() {
return (
this.isServiceableInshop && !this.isServiceableMobile && !this.isServiceableDropoff
);
},
},
methods: {
arePagePrerequisitesValid() {
@ -502,6 +515,9 @@ export default {
setRecycleFeePart(recycleFeePart) {
this.recycleFeePart = recycleFeePart;
},
setMobileLocationModalOpened(isModalOpened) {
this.mobileLocationModalOpened = isModalOpened;
},
//creating async function as the computed property can not directly handle asynchronous operations or promises.
async setMobileLocationQuestions(newValue) {
var newZipCode = newValue.addressQuestions.zipCode;

View file

@ -77,6 +77,7 @@ export default {
cmsWidgetName: String,
validationRules: String,
isDisplayed: Boolean,
isInshopOnly: Boolean,
},
computed: {
questionText() {
@ -233,9 +234,8 @@ export default {
// nothing to do with this component. We could mitigate this by showing / hiding this component with v-if but that messes
// up the component initialization on page load.
async handler() {
this.resetAnswers();
await nextTick();
this.resetAnswers();
this.getNextShopsFromList();
},
},

View file

@ -29,6 +29,7 @@ import { applicationConfig } from "../constants/application-config";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routeData } from "@/router/constants/routes";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { Variables } from "../constants/analytics";
export default {
methods: {
@ -119,6 +120,10 @@ export default {
}
},
async pushVariableToDataLayer(data) {
pushToDataLayerIfDefined(data);
},
async pushPageViewToGA() {
const currentPageName = getPageNameByQueryString();
const pageViewEvent = {
@ -305,6 +310,18 @@ export default {
} else {
payload.priceSubTotal = "";
}
// Cash Quote or Cash Price Sub Total
if (isPricingAvailable) {
const subtotal = baseMixin.methods
.getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false)
.toFixed(2);
payload.cashPriceSubTotal = parseFloat(subtotal);
} else {
payload.cashPriceSubTotal = "";
}
//unverified (in scenarios we dont display the price)
if (
order.payment.isInsurance &&
@ -769,6 +786,9 @@ export default {
ValueToLogTypes() {
return ValueToLogTypes;
},
Variables() {
return Variables;
},
},
};

View file

@ -341,6 +341,10 @@ export const mutations = {
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable;
},
updateCashPriceSubTotal(state, cashPriceSubTotal) {
state.order.cashPriceSubTotal =
cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString();
},
updateCCToken(state, ccToken) {
state.order.payment.ccToken.subscriptionId = ccToken.subscriptionId;
state.order.payment.ccToken.expMonth = ccToken.expMonth;
@ -1736,7 +1740,10 @@ export const actions = {
return response;
},
getMobileFeePart(context, { payload: { serviceZipCode, serviceZipCodeCtu }, pageNameToLog }) {
getMobileFeePart(
context,
{ payload: { serviceZipCode, serviceZipCodeCtu, mobileProviderNumber }, pageNameToLog }
) {
const serviceType = context.getters.damage.isRepair ? "Repair" : "Install";
const facilityType = "Mobile";
const parentAccountNumber = context.getters.payment.parentAccountNumber;
@ -1754,6 +1761,7 @@ export const actions = {
order.serviceLocation?.zipCode;
var providerNumber =
mobileProviderNumber ??
serviceZipCodeCtu ??
order.serviceLocation?.provider?.providerNumber ??
order.serviceLocation?.zipCodeCtu;
@ -2343,6 +2351,7 @@ export const actions = {
lockToken: order.lockToken,
isRecalAckOptIn: order.isRecalAckOptIn,
isMSRFeeApplicable: order.isMSRFeeApplicable,
cashPriceSubTotal: order.cashPriceSubTotal,
},
},
logApiCall: true,