From 2eb985cd7d1ece210ec3fffa47737a7732a500af Mon Sep 17 00:00:00 2001 From: scottkiener-at-safelite Date: Fri, 6 Feb 2026 10:17:18 -0500 Subject: [PATCH] INSR-7771 | Use new endpoint, update error handling --- src/constants/endpoints.js | 4 + src/constants/error-messages.js | 5 +- src/constants/global-rules.js | 3 +- src/helpers/global-rule-definer.js | 24 +++- src/layouts/tpa-search/tpa-search.vue | 155 +++++++++----------------- src/store/index.js | 26 ++--- 6 files changed, 96 insertions(+), 121 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index c55410da..63ca9e2d 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -84,6 +84,10 @@ const endpoints = Object.freeze({ url: `${LOCATION_BASE_URL}/providers`, method: 'GET' }, + GetTpaAndSafeliteProviders: { + url: `${LOCATION_BASE_URL}/tpa-and-safelite-providers`, + method: 'GET' + }, GetCapabilityQuestions: { url: `${PARTS_BASE_URL}/capability-questions`, method: 'GET' diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js index 71e7d515..868b8e13 100644 --- a/src/constants/error-messages.js +++ b/src/constants/error-messages.js @@ -60,7 +60,10 @@ const errorMessages = Object.freeze({ STYLE_REQUIRED: 'Vehicle style is required.', MOBILE_LOCATION_REQUIRED: 'Please enter your service address', DATE_REQUIRED: 'Please select a date', - TIME_REQUIRED: 'Please select an appointment time.' + TIME_REQUIRED: 'Please select an appointment time.', + TPA_SEARCH_REQUIRED: 'Please enter a shop name or ZIP code.', + TPA_SEARCH_SHOP_FORMAT: 'Please enter a valid shop name.', + TPA_SEARCH_ZIP_FORMAT: 'Please enter a valid Zip code.' }); export default errorMessages; diff --git a/src/constants/global-rules.js b/src/constants/global-rules.js index e82b118a..6dc75766 100644 --- a/src/constants/global-rules.js +++ b/src/constants/global-rules.js @@ -26,7 +26,8 @@ const globalRules = Object.freeze({ EXTENSION_FORMAT: 'extension-format', ZIP_CODE_REQUIRED: 'zip-code-required', ZIP_CODE_SEARCH_FORMAT: 'zip-code-search-format', - OPTION_REQUIRED: 'option-required' + OPTION_REQUIRED: 'option-required', + TPA_SEARCH_FORMAT: 'tpa-search-format' }); export default globalRules; diff --git a/src/helpers/global-rule-definer.js b/src/helpers/global-rule-definer.js index f5a2335b..227fc0bb 100644 --- a/src/helpers/global-rule-definer.js +++ b/src/helpers/global-rule-definer.js @@ -123,6 +123,28 @@ function defineGlobalZipCodeRules() { defineRule(globalRules.ZIP_CODE_SEARCH_FORMAT, regex(/^\d{5}$/, errorMessages.ZIP_FORMAT)); } +/** + * @summary Define global rules related to the zip code/shop name search on TPA Search page + */ +function defineGlobalTpaSearchRules() { + defineRule(globalRules.TPA_SEARCH_FORMAT, (value) => { + if (value.length === 0) { + return errorMessages.TPA_SEARCH_REQUIRED; + } + // Non Numeric + if (isNaN(value)) { + if (value.length < 3) { + return errorMessages.TPA_SEARCH_SHOP_FORMAT; + } + } else { + if (value.length != 5) { + return errorMessages.TPA_SEARCH_ZIP_FORMAT; + } + } + return true; + }); +} + /** * @function defineGlobalRules * @summary Define all global rules @@ -136,6 +158,6 @@ export default function defineGlobalRules() { defineGlobalPolicyNumberRules(); defineGlobalPolicyZipCodeRules(); defineGlobalZipCodeRules(); - + defineGlobalTpaSearchRules(); defineRule(globalRules.OPTION_REQUIRED, required(errorMessages.OPTION_REQUIRED)); } diff --git a/src/layouts/tpa-search/tpa-search.vue b/src/layouts/tpa-search/tpa-search.vue index 9476a7ad..2eed76f9 100644 --- a/src/layouts/tpa-search/tpa-search.vue +++ b/src/layouts/tpa-search/tpa-search.vue @@ -27,14 +27,14 @@
@@ -52,10 +52,10 @@ @invalidSubmit="onInvalidSubmit"> @@ -135,53 +135,8 @@ import widgetFields from '@/constants/cms-widget-fields.js'; import { shallowRef } from 'vue'; import { toTitleCase, toDisplayPhoneNumber } from '@/helpers/text-helper.js'; import bailoutMessage from '@/constants/bailoutMessage'; +import settleAllPromises from '@/helpers/layout-helper'; -const radiusFilterPairs = [ - { radius: 15, filter: '15 miles' }, - { radius: 25, filter: '25 miles' }, - { radius: 50, filter: '50 miles' }, - { radius: 100, filter: '100 miles' } -]; - -function convertRadiusFilterToInteger(filter) { - const pair = radiusFilterPairs.find((p) => p.filter === filter); - return pair?.radius ?? 0; -} - -async function getInitialSearchData() { - const customerZipCode = useMainStore().order.customer.address.zipCode; - const serviceLocationZipCode = useMainStore().order.serviceLocation.zipCode; - const zipCode = serviceLocationZipCode ?? customerZipCode; - - const { searchFilter } = useMainStore().order.serviceLocation; - let { providerNumber } = useMainStore().order.serviceLocation.provider; - - let providers = []; - let radius = 0; - if (searchFilter) { - radius = convertRadiusFilterToInteger(searchFilter); - const result = await useMainStore().getTpaProviders(zipCode, radius); - providers = result?.data?.shopProviders ?? []; - - return { zipCode, filter: searchFilter, providers, providerNumber }; - } - - providerNumber = null; - let pairIndex = 0; - let filter = ''; - while (providers.length === 0 && pairIndex < radiusFilterPairs.length) { - const pair = radiusFilterPairs[pairIndex]; - radius = pair.radius; - filter = pair.filter; - // eslint-disable-next-line no-await-in-loop - const result = await useMainStore().getTpaProviders(zipCode, radius); - providers = result?.data?.shopProviders ?? []; - - pairIndex += 1; - } - - return { zipCode, filter, providers, providerNumber }; -} export default { name: 'tpa-search', @@ -200,23 +155,35 @@ export default { }, mixins: [BaseFormMixin], async beforeRouteEnter(to, from, next) { - const { zipCode, filter, providers, providerNumber } = - await getInitialSearchData(); - const cmsContent = await fetchCmsContentForPage(to.query.issPage); - + const zipCode = useMainStore().order.customer.address.zipCode; + const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); + const providersPromise = useMainStore().getTpaAndSafeliteProviders(zipCode); + const promiseResultMap = [ + { + resultKey: 'cmsContent', + promise: cmsContentPromise + }, + { + resultKey: 'providers', + promise: providersPromise + } + ]; + const resultMap = await settleAllPromises(promiseResultMap); next(async (vm) => { - vm.setCmsContent(cmsContent); - vm.setInitialSearchData(zipCode, filter, providers, providerNumber); + vm.setCmsContent(resultMap.cmsContent); + vm.tpaSearchValue = ""; // TODO: use page data to set the value + vm.mapZipCode = zipCode; + vm.providers = resultMap.providers; }); }, data() { return { dataLoaded: false, - zipCode: null, + tpaSearchValue: null, mapZipCode: null, - filter: '', + radiusFilter: useMainStore().order.serviceLocation.tpaSearchRadius ?? 25, // TODO: use page data to set the value providers: [], - selectedProviderNumber: '', + selectedProviderNumber: useMainStore().order.serviceLocation.provider.providerNumber, reloadingProviders: false, additionalButtonData: { displayAvailabilityIndicators: false @@ -231,7 +198,7 @@ export default { siteFooter: 'SiteFooterWidget' }, rules: { - zipCode: `${globalRules.ZIP_CODE_REQUIRED}|${globalRules.ZIP_CODE_SEARCH_FORMAT}`, + tpaSearch: globalRules.TPA_SEARCH_FORMAT, filter: globalRules.OPTION_REQUIRED, // TODO do we even need this? provider: globalRules.OPTION_REQUIRED }, @@ -239,17 +206,16 @@ export default { }; }, computed: { - filterOptions() { - const filterByAnswers = - this.getCmsContent( + radiusFilterOptions() { + const radiusFilterOptions = this.getCmsContent( this.widget.filterByQuestion, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS ) ?? []; - const filterByAnswersObj = {}; - [...filterByAnswers].forEach((answer) => { - filterByAnswersObj[answer.Name] = answer.Name; - }); - return filterByAnswersObj; + const options = {}; + for (const option of radiusFilterOptions) { + options[option.Name] = option.Text; + } + return options; }, tpaSearchQuestionLabel() { return this.getCmsContent( @@ -269,17 +235,17 @@ export default { widgetFields.TEXT_BLOCK_WIDGET.TEXT ); }, - radiusInMiles() { - return convertRadiusFilterToInteger(this.filter); - }, noNetworkShopsAlertHeaderText() { return this.getCmsContent( this.widget.noNetworkShopsAlert, widgetFields.ALERT_WIDGET.HEADLINE_TEXT )?.replaceAll('{custom:radiusInMiles}', this.radiusInMiles); }, + radiusFilteredAndCappedProviders() { + return this.providers.filter((provider) => provider.distanceInMiles <= this.radiusFilter).slice(0, 5); + }, providerAddresses() { - return this.providers?.map((provider) => ({ + return this.radiusFilteredAndCappedProviders?.map((provider) => ({ title: provider.companyName, fullAddress: this.getFullProviderAddress(provider), addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)] @@ -287,7 +253,7 @@ export default { }, providerButtonData() { return ( - this.providers?.map((provider) => + this.radiusFilteredAndCappedProviders?.map((provider) => this.getShopButtonDataFromProvider(provider)) ?? [] ); }, @@ -298,14 +264,6 @@ export default { } }, watch: { - async filter() { - if (this.dataLoaded) { - await this.getProviderButtonData().then((providers) => { - this.providers = providers; - }); - this.mapZipCode = this.zipCode; - } - }, providers(newProviders) { if (this.dataLoaded) { this.selectedProviderNumber = @@ -318,7 +276,7 @@ export default { const provider = this.providers?.find((p) => p.providerNumber === newNumber); if (provider && this.dataLoaded) { useMainStore().updateServiceLocation({ - searchFilter: this.filter, + tpaSearchRadius: this.radiusFilter, zipCode: this.zipCode, provider: { providerNumber: provider?.providerNumber, @@ -342,13 +300,6 @@ export default { } }, methods: { - setInitialSearchData(zipCode, filter, providers, providerNumber) { - this.zipCode = zipCode; - this.mapZipCode = zipCode; - this.filter = filter; - this.providers = providers; - this.selectedProviderNumber = providerNumber; - }, getFullProviderAddress(provider) { const addressLine1 = this.getProviderAddress(provider); const addressLine2 = this.getProviderCityZipState(provider); @@ -379,14 +330,14 @@ export default { } return addressLine2; }, - async getProviderButtonData() { + async getProviderButtonData(shopName = "") { this.reloadingProviders = true; - const getTpaProvidersResult = await useMainStore().getTpaProviders( - this.zipCode, - this.radiusInMiles + const getTpaProvidersResult = await useMainStore().getTpaAndSafeliteProviders( + this.mapZipCode, + shopName ); this.reloadingProviders = false; - return getTpaProvidersResult?.data?.shopProviders ?? []; + return getTpaProvidersResult?.data ?? []; }, doNotSeeMyShopLinkClick() { this.mainStore.setBailout(bailoutMessage.doNotSeeMyShop()); @@ -396,8 +347,12 @@ export default { ); }, async searchClick() { - this.providers = await this.getProviderButtonData(); - this.mapZipCode = this.zipCode; + if (isNaN(this.tpaSearchValue)) { + this.providers = await this.getProviderButtonData(this.tpaSearchValue); + } else { + this.mapZipCode = this.tpaSearchValue; + this.providers = await this.getProviderButtonData(); + } }, forwardButtonAction() { if (this.selectedProviderIsSafeliteShop) { @@ -409,14 +364,6 @@ export default { .CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP; this.$router.navigate(scenario, this.$route); }, - getCustomValueFromString(str) { - switch (str) { - case 'radiusInMiles': - return this.radiusInMiles; - default: - return null; - } - }, getShopButtonDataFromProvider(provider) { const cellNumber = toDisplayPhoneNumber(provider?.phoneNumber); const distance = diff --git a/src/store/index.js b/src/store/index.js index 5fb37e57..48b73e89 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -142,7 +142,7 @@ export const getDefaultState = () => ({ appointmentType: null, isVehicleProtected: null, IsSafeliteProvider: null, - searchFilter: null, + tpaSearchRadius: null, provider: { providerNumber: null, address: { @@ -1032,23 +1032,21 @@ export const useMainStore = defineStore({ }); }, - getTpaProviders(zipCode, radius) { - const { carId, isBigTruck } = this.order.vehicle; - const damageType = this.damage.isRepair ? 'Repair' : 'Replace'; + getTpaAndSafeliteProviders(zipCode, provderNameSearchString = "") { const { parentAccountNumber } = this.order; - const partsWithRecal = getTopLevelGlassPartsWithRecal(this.order.lineItems.glassParts); - const windshieldPartWithRecal = partsWithRecal?.length > 0 ? partsWithRecal[0] : null; - const safeliteOnly = false; + const carId = this.order.vehicle.carId; + const damageType = this.damage.isRepair ? 'Repair' : 'Install'; + + const parts = getLineItemsFlattened(this.order.lineItems.glassParts); + const partNumbers = parts.map((part) => part.partNumber).join(','); - let url = `${endpoints.GetProviders.url}/${zipCode}/${damageType}/${radius}/${parentAccountNumber}/${safeliteOnly}/${carId}`; - if (windshieldPartWithRecal) { - url += `/${windshieldPartWithRecal.partNumber}`; + let url = `${endpoints.GetTpaAndSafeliteProviders.url}/${zipCode}?accountNumber=${parentAccountNumber}&carId=${carId}&damageType=${damageType}&partNumbers=${partNumbers}`; + if (provderNameSearchString) { + url += `&providerNameSearchString=${provderNameSearchString}`; } - url += `/${isBigTruck}`; - return new Promise((resolve, reject) => { globalMethods.callHttpClient({ - method: endpoints.GetProviders.method, + method: endpoints.GetTpaAndSafeliteProviders.method, endpoint: url }).then((response) => resolve(response), (error) => reject(error)); }); @@ -1822,7 +1820,7 @@ export const useMainStore = defineStore({ companyName: serviceLocationInfo.provider?.companyName, phoneNumber: serviceLocationInfo.provider?.phoneNumber }; - this.order.serviceLocation.searchFilter = serviceLocationInfo.searchFilter; + this.order.serviceLocation.tpaSearchRadius = serviceLocationInfo.tpaSearchRadius; }, resetState() {