Merge pull request #1104 from Safelite/feature/digital/INSR-8549

INSR-8549 Fixed Zipcode 99546 causing all sorts of issues in NextGen
This commit is contained in:
Josh Dassinger 2026-02-23 11:11:41 -06:00 committed by GitHub
commit 883d1980dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 188 additions and 184 deletions

View file

@ -32,9 +32,16 @@ export class Logger {
}
formatLogEntry(message, details) {
return `Application: ${applicationConfig.APPLICATION_NAME}\n${message}\n${
details ? JSON.stringify(details, undefined, 2) : ''
}`;
let json = '';
if (details) {
if (details instanceof Error) {
json = JSON.stringify(details, Object.getOwnPropertyNames(details), 2);
} else {
json = JSON.stringify(details, undefined, 2)
}
}
return `Application: ${applicationConfig.APPLICATION_NAME} \n${message} \n${json}`;
}
writeLogEntry(endpoint, logEntry) {

View file

@ -1,4 +1,6 @@
import { useMainStore } from '@/store';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import settleAllPromises from '@/helpers/layout-helper';
function processZipCodeResults(request) {
return request.then((serviceZipValidationResponse) => ({
@ -29,7 +31,7 @@ export async function getPricedMobileFeePart(serviceZipCode) {
// Get the Mobile Fee Part
const mobileFeePart = await useMainStore().getMobileFeePart();
if (mobileFeePart.data == null || mobileFeePart.data === '') {
if (mobileFeePart?.data == null || mobileFeePart.data === '') {
return Promise.resolve(null);
}
@ -40,17 +42,6 @@ export async function getPricedMobileFeePart(serviceZipCode) {
return Promise.resolve(pricingResults[0]);
}
export async function getServiceabilityDetails(serviceZipCode, lineItems) {
const serviceabilityDetails = await useMainStore().getServiceabilityDetails(
{
serviceZipCode,
lineItems
},
false
);
return Promise.resolve(serviceabilityDetails);
}
export async function getAvailabilityRating(
startDate,
endDate,
@ -69,3 +60,45 @@ export async function getAvailabilityRating(
return Promise.resolve(shopStatus);
}
export function getProviderData() {
const mainStore = useMainStore();
const storeServiceLocation = mainStore.order.serviceLocation;
const storeSelectedAppointmentType = storeServiceLocation?.appointmentType;
const isMobileAppointment = storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE
|| storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const storeSelectedProvider = storeServiceLocation?.provider;
const serviceZipCode = storeServiceLocation?.zipCode || mainStore.order.customer.address.zipCode;
return { storeServiceLocation, isMobileAppointment, storeSelectedProvider, serviceZipCode }
}
export async function onProviderChanged() {
const { isMobileAppointment, storeSelectedProvider, serviceZipCode } = getProviderData();
if (!storeSelectedProvider) {
return;
}
const mainStore = useMainStore();
await mainStore.getBillToInfo(storeSelectedProvider.providerNumber);
const promiseResultMap = [
{
resultKey: 'glassFees',
promise: mainStore.getGlassFees()
.then(async (glassFees) => await mainStore.getCombinedQuote(glassFees.data))
.then(async (quotedGlassFees) => mainStore.updateGlassFees(quotedGlassFees))
},
];
if (isMobileAppointment && (mainStore.isNoComp || mainStore.isITAC)) {
promiseResultMap.push({
resultKey: 'priceMobileFee',
promise: getPricedMobileFeePart(serviceZipCode).then(mobileFee => mainStore.updateMobileFee(mobileFee))
});
} else {
mainStore.updateMobileFee(null);
}
await settleAllPromises(promiseResultMap);
}

View file

@ -89,9 +89,9 @@ import {
import settleAllPromises from '@/helpers/layout-helper';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import {
getPricedMobileFeePart,
getServiceabilityDetails,
getZipCodeData
getProviderData,
getZipCodeData,
onProviderChanged
} from '@/helpers/service-location-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
@ -225,16 +225,9 @@ export default {
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const storeServiceLocation = useMainStore().order.serviceLocation;
const storeSelectedAppointmentType = storeServiceLocation?.appointmentType;
const isMobileAppointment = storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE
|| storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const storeSelectedProvider = storeServiceLocation?.provider;
const serviceZipCode = storeServiceLocation?.zipCode || useMainStore().order.customer.address.zipCode;
const { isMobileAppointment, storeServiceLocation, storeSelectedProvider, serviceZipCode } = getProviderData();
const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const serviceabilityDetailsPromise = useMainStore().getServiceabilityDetails(serviceZipCode);
const providersPromise = useMainStore().getSafeliteProviders(serviceZipCode, 150);
const premiumFeePromise = useMainStore().getMobilePremiumFee();
@ -251,14 +244,6 @@ export default {
resultKey: 'cmsContent',
promise: cmsContentPromise
},
{
resultKey: 'mobileFeePart',
promise: mobileFeePartPromise
},
{
resultKey: 'glassFees',
promise: getGlassFeesPromise
},
{
resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise
@ -271,6 +256,10 @@ export default {
resultKey: 'providers',
promise: providersPromise
},
{
resultKey: 'providerChanged',
promise: onProviderChanged()
},
{
resultKey: 'premiumFeeWithPrice',
promise: premiumFeeWithPricePromise
@ -332,7 +321,6 @@ export default {
inShopDatesData: [],
isMobileView: false,
mobileDatesData: [],
mobileFeePart: null,
mobilePremiumAppointmentFee: null,
mobileProviderNumber: null,
selectableDatesData: [],
@ -659,7 +647,9 @@ export default {
}
},
mobileZipUpdatedFromServiceLocation(mobileZipServiceLocation) {
if (mobileZipServiceLocation) {
if (mobileZipServiceLocation
&& (this.mobileProviderNumber !== mobileZipServiceLocation.mobileProviderNumber
|| !this.hasNeededServiceLocationData)) {
this.mobileProviderNumber = mobileZipServiceLocation.mobileProviderNumber;
this.selectedMobileZipCode = mobileZipServiceLocation.zipCode;
this.selectedServiceLocation = mobileZipServiceLocation;
@ -671,18 +661,15 @@ export default {
},
async providerChangedFromServiceLocation(newProvider) {
this.selectedProvider = newProvider?.provider;
if (newProvider?.refreshDatePicker && newProvider.provider) {
this.mainStore.updateServiceLocationProvider(newProvider.provider);
if (newProvider?.provider) {
this.mainStore.updateServiceLocation(newProvider);
showIssLoadingModal(true);
await this.mainStore.getBillToInfo(newProvider.provider?.providerNumber)
.then(() => {
this.refreshDatePicker();
})
.catch(() => {
console.warn('Error fetching bill to info...');
showIssLoadingModal(false);
});
await onProviderChanged();
if (newProvider.refreshDatePicker) {
await this.refreshDatePicker();
} else {
showIssLoadingModal(false);
}
}
},
async refreshDatePicker() {
@ -713,8 +700,7 @@ export default {
appointmentTypeToUse = AppointmentTypeStrings.DROP_OFF;
}
// Save service location then save schedule then navigate forward
await this.$refs.serviceLocation.forwardButtonAction(appointmentTypeToUse);
this.mainStore.updateAppointmentType(appointmentTypeToUse);
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,

View file

@ -88,7 +88,6 @@ import { useMainStore } from '@/store';
import {
getMobileZipCodeData,
getPricedMobileFeePart,
getServiceabilityDetails
} from '@/helpers/service-location-helper';
import { deepClone } from '@/helpers/object-helper.js';
import vehicleProtectedQuestion from '@/layouts/schedule-page/service-location/mobile-location-modal-question/vehicle-protected-question/vehicle-protected-question.vue';
@ -273,7 +272,7 @@ export default {
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
const serviceabilityDetails = await useMainStore().getServiceabilityDetails(serviceZipCode);
// update content related to service zip code
this.$emit('updated-mobile-fee-part', mobileFeePart);

View file

@ -86,7 +86,6 @@ const mockProviders = () => Promise.resolve([
jest.mock(
'@/helpers/service-location-helper',
() => ({
getServiceabilityDetails: jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode)),
getZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)),
getMobileZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode))
})
@ -147,6 +146,7 @@ function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
}
})];
useMainStore().getSafeliteProviders = jest.fn().mockImplementation(() => mockProviders());
useMainStore().getServiceabilityDetails = jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode));
const wrapper = mount(serviceLocation, mountOptions);
wrapper.vm.$router.navigateWithSpinner = jest.fn();

View file

@ -129,7 +129,6 @@ import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import {
getServiceabilityDetails,
getZipCodeData,
getMobileZipCodeData
} from '@/helpers/service-location-helper';
@ -183,7 +182,6 @@ export default {
isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
mobileProviderNumber: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
@ -233,8 +231,8 @@ export default {
},
isInshop() {
return (
this.selectedAppointmentType === 'Inshop'
|| this.selectedAppointmentType === 'Dropoff'
this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP
|| this.selectedAppointmentType === AppointmentTypeStrings.DROP_OFF
);
},
isMobile() {
@ -317,7 +315,7 @@ export default {
},
selectedAppointmentType(newValue, oldValue) {
if (this.isInshop && this.selectedProvider && this.availabilityRating === null) {
const appointmenTypeServiceLocationObj = {
const appointmentTypeServiceLocationObj = {
appointmentType: newValue,
mobileProviderNumber: null,
provider: this.selectedProvider,
@ -326,9 +324,9 @@ export default {
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('appointment-type-changed', appointmenTypeServiceLocationObj);
this.$emit('appointment-type-changed', appointmentTypeServiceLocationObj);
} else {
const appointmenTypeServiceLocationObj = {
const appointmentTypeServiceLocationObj = {
appointmentType: newValue,
mobileProviderNumber: this.mobileProviderNumber,
provider: null,
@ -337,20 +335,31 @@ export default {
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu
};
this.$emit('appointment-type-changed', appointmenTypeServiceLocationObj);
this.$emit('appointment-type-changed', appointmentTypeServiceLocationObj);
}
},
selectedProvider(newProvider, oldProvider) {
if (newProvider?.providerNumber !== oldProvider?.providerNumber || this.availabilityRating === null) {
if (newProvider?.providerNumber !== oldProvider?.providerNumber) {
const appointmentIsInshop = this.isInshop;
const returnedProvider = {
provider: newProvider,
refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null
refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null,
state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType,
provider: !this.isMobile ? newProvider : {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
}
};
if (newProvider?.providerNumber !== oldProvider?.providerNumber) {
this.$emit('provider-changed', returnedProvider);
}
this.$emit('provider-changed', returnedProvider);
}
}
},
@ -360,34 +369,6 @@ export default {
await this.setData(initialData);
this.initializingData = false;
},
async forwardButtonAction(appointmentType) {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
provider = {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
};
}
this.mainStore.saveServiceLocation({
state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: appointmentType || this.selectedAppointmentType,
provider
});
},
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
@ -427,7 +408,7 @@ export default {
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.zipCode);
const serviceabilityDetailsPromise = this.mainStore.getServiceabilityDetails(this.zipCode);
await serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {
@ -465,10 +446,6 @@ export default {
this.setServiceabilityDetails(initialData.serviceabilityDetails);
}
if (initialData.mobileFeePart) {
this.mobileFeePart = initialData.mobileFeePart;
}
if (initialData.providers) {
this.setMobileProviderNumber(initialData.providers.mobileProviderNumber);
let foundMatch = false;
@ -485,22 +462,12 @@ export default {
this.selectedProvider = null;
}
}
if (initialData.glassFees) {
useMainStore().getCombinedQuote(initialData.glassFees)
.then((combinedQuote) => {
this.setGlassFeeItems(combinedQuote);
});
}
},
setContainsMilitaryBase(val) {
if (this.zipContainsMilitaryBase !== val) {
this.zipContainsMilitaryBase = val;
}
},
setGlassFeeItems(newGlassFeeItems) {
this.mainStore.updateGlassFees(newGlassFeeItems);
},
setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber;
},
@ -531,7 +498,7 @@ export default {
this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.mobileZipCode);
const serviceabilityDetailsPromise = this.mainStore.getServiceabilityDetails(this.mobileZipCode);
await serviceabilityDetailsPromise.then((result) => {
const details = result.data;
if (details) {

View file

@ -319,7 +319,6 @@ export default {
}
const promises = [
// Suppress error from API call we can try again later in the flow
this.mainStore.getBillToInfo()
];
// Skip duplicate check if loaded from cookie or already visited duplicate check page.

View file

@ -873,6 +873,10 @@ export const useMainStore = defineStore({
},
getMobileTimeSlots(startDate, endDate, zipCodeOverride = null) {
const { order } = this;
if (!this.billToAccountNumber) {
return null;
}
const { vehicle } = this.order;
let lineItems = [
...(order.lineItems.supportingItems ?? []),
@ -928,6 +932,9 @@ export const useMainStore = defineStore({
},
getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) {
const { order } = this;
if (!this.billToAccountNumber || !providerNumber) {
return null;
}
const { vehicle } = this.order;
let lineItems = [
@ -1125,6 +1132,10 @@ export const useMainStore = defineStore({
const providerToUse = provider?.providerNumber ?? defaultProviderNumber;
const partNumberListQueryString = getPartNumbersListForQueryString(glassParts, 'PartNumbers');
if (!this.billToAccountNumber || !providerToUse) {
return null
}
const params = new URLSearchParams({
serviceType: damageType,
facilityType,
@ -1242,62 +1253,74 @@ export const useMainStore = defineStore({
},
async getCombinedQuote(availableLineItems) {
if (availableLineItems && availableLineItems.length > 0) {
const { vehicle, serviceLocation, policy, eon, referralNumber, referralDate, referralSequenceNumber, lineItems } = this.order;
const availableLineItemsFiltered = availableLineItems.filter((lineItem) => !!(lineItem.partNumber));
const lineItemsWithOnlyPartNumbers = availableLineItemsFiltered.map((lineItem) => ({ partNumber: lineItem.partNumber }));
const payload = {
ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber,
CarId: vehicle.carId,
Make: vehicle.make,
Model: vehicle.model,
Year: vehicle.year,
EON: eon,
ZipCode: policy.policyZipCode,
State: this.order.customer.address.state,
ReferralNumber: referralNumber,
ReferralDate: referralDate,
ReferralSequenceNumber: referralSequenceNumber,
ServiceZipCode: serviceLocation.zipCode,
IsReplacement: !this.isRepair,
ServerData: lineItems.serverData,
LineItems: lineItemsWithOnlyPartNumbers,
FeatureToggles: getFeatureTogglesPayloadObject(this.experimentSettings)
};
const response = await globalMethods
.callHttpClient({
method: endpoints.GetCombinedQuote.method,
endpoint: endpoints.GetCombinedQuote.url,
payload
})
.catch((error) => {
console.error(error);
return [];
});
const { lineItems: pricedLinedItems, serverData } = response.data;
if (serverData) {
this.order.lineItems.serverData = serverData;
}
if (pricedLinedItems) {
return addPricesToLineItems(availableLineItems, pricedLinedItems);
}
return availableLineItems;
if (!availableLineItems || availableLineItems.length === 0) {
return [];
}
return [];
const {
vehicle,
serviceLocation,
policy,
eon,
referralNumber,
referralDate,
referralSequenceNumber,
lineItems
} = this.order;
const availableLineItemsFiltered = availableLineItems.filter((lineItem) => !!(lineItem.partNumber));
const lineItemsWithOnlyPartNumbers = availableLineItemsFiltered.map((lineItem) => ({ partNumber: lineItem.partNumber }));
const payload = {
ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber,
CarId: vehicle.carId,
Make: vehicle.make,
Model: vehicle.model,
Year: vehicle.year,
EON: eon,
ZipCode: policy.policyZipCode,
State: this.order.customer.address.state,
ReferralNumber: referralNumber,
ReferralDate: referralDate,
ReferralSequenceNumber: referralSequenceNumber,
ServiceZipCode: serviceLocation.zipCode,
IsReplacement: !this.isRepair,
ServerData: lineItems.serverData,
LineItems: lineItemsWithOnlyPartNumbers,
FeatureToggles: getFeatureTogglesPayloadObject(this.experimentSettings)
};
const response = await globalMethods
.callHttpClient({
method: endpoints.GetCombinedQuote.method,
endpoint: endpoints.GetCombinedQuote.url,
payload
})
.catch((error) => {
console.error(error);
return [];
});
const { lineItems: pricedLinedItems, serverData } = response.data;
if (serverData) {
this.order.lineItems.serverData = serverData;
}
if (pricedLinedItems) {
return addPricesToLineItems(availableLineItems, pricedLinedItems);
}
return availableLineItems;
},
getMobileFeePart() {
const { vehicle, insuranceCoverage } = this.order;
if (!this.billToAccountNumber || !this.providerNumber) {
return null;
}
const params = new URLSearchParams({
serviceType: this.damage.isRepair ? 'Repair' : 'Install',
@ -1317,7 +1340,7 @@ export const useMainStore = defineStore({
});
},
getServiceabilityDetails({ serviceZipCode }) {
getServiceabilityDetails(serviceZipCode) {
const { vehicle, damage, parentAccountNumber, referralSequenceNumber, lineItems } = this.order;
const { carId } = vehicle;
const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace);
@ -1819,23 +1842,12 @@ export const useMainStore = defineStore({
this.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu ?? this.order.serviceLocation.zipCodeCtu;
this.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType ?? this.order.serviceLocation.appointmentType;
this.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected ?? this.order.serviceLocation.isVehicleProtected;
if (serviceLocationInfo.provider) {
this.order.serviceLocation.provider = {
providerNumber: serviceLocationInfo.provider?.providerNumber,
address: {
streetAddress: serviceLocationInfo.provider?.address?.streetAddress,
city: serviceLocationInfo.provider?.address?.city,
state: serviceLocationInfo.provider?.address?.state,
zipCode: serviceLocationInfo.provider?.address?.zipCode,
zipCodeCtu: serviceLocationInfo.provider?.address?.zipCodeCtu
},
companyName: serviceLocationInfo.provider?.companyName,
phoneNumber: serviceLocationInfo.provider?.phoneNumber
};
}
this.updateServiceLocationProvider(serviceLocationInfo.provider)
this.order.serviceLocation.tpaSearchRadius = serviceLocationInfo.tpaSearchRadius;
},
updateAppointmentType(appointmentType) {
this.order.serviceLocation.appointmentType = appointmentType;
},
updateServiceLocationProvider(providerInfo) {
this.order.serviceLocation.provider = {
providerNumber: providerInfo?.providerNumber,
@ -2686,11 +2698,16 @@ export const useMainStore = defineStore({
}
},
async getBillToInfo(componentProviderNumber = null) {
async getBillToInfo(componentProviderNumber = null) {
const providerNumber = componentProviderNumber || this.providerNumber;
if (!providerNumber) {
return;
}
const { order, issConfig } = this;
const params = new URLSearchParams({
parentAccountNumber: order.parentAccountNumber.toString(),
providerNumber: componentProviderNumber || this.providerNumber,
providerNumber,
typeOfClaim: 'GLASS ONLY',
lineOfBusiness: 'PERSONAL',
isItac: this.isITAC
@ -2727,10 +2744,6 @@ export const useMainStore = defineStore({
});
},
saveServiceLocation(serviceLocationInfo) {
this.updateServiceLocation(serviceLocationInfo);
},
saveRegistrationAddressLookup({ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) {
// Reset dependent state when changing
if