INSR-10109 Initial Setup for Vin Required Parts Lookup

This commit is contained in:
Josh Dassinger 2026-07-20 10:31:14 -05:00 committed by Bill Richardson
parent d94bd6ebe0
commit 457c112243
15 changed files with 159 additions and 84 deletions

View file

@ -102,9 +102,9 @@ const bailoutMessage = Object.freeze({
code: bailoutCode.NeedHelp,
message: 'User clicked the Need Help link in the app.'
}),
YMMNotFound: () => ({
YMMNotFound: (message = null) => ({
code: bailoutCode.YMMNotFound,
message: 'Vin Required vehicle'
message: `Vin Required vehicle` + message ? ` ${message}` : ''
})
});

View file

@ -70,7 +70,7 @@ const endpoints = Object.freeze({
url: `${PARTS_V1_BASE_URL}/parts-or-questions`,
method: 'POST'
},
GetPartsOrQuestionsVinRequired: {
GetPartsOrQuestionsV2: {
url: `${PARTS_V2_BASE_URL}/parts-or-questions`,
method: 'POST'
},

View file

@ -43,6 +43,7 @@ export function getMountOptions(mockData) {
mocks.pushEventToGA = jest.fn();
mocks.$loadScript = mockData?.loadScript;
mocks.prependActionToMethod = jest.fn();
mocks.navigateBailout = jest.fn();
const global = {
mocks,

View file

@ -442,7 +442,7 @@ describe('address-lookup.vue', () => {
useMainStore().order.vehicle.carId = 'C0000';
// Act
wrapper.vm.navigateForward(carsFound);
await wrapper.vm.navigateForward(carsFound);
// Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
@ -482,9 +482,10 @@ describe('address-lookup.vue', () => {
});
useMainStore().order.vehicle.carId = 'CARID3';
useMainStore().order.vehicle.vinRequired = false;
// Act
wrapper.vm.navigateForward(carsFound);
await wrapper.vm.navigateForward(carsFound);
// Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);

View file

@ -81,7 +81,6 @@ import alert from '@/ux-components/alert/alert.vue';
import { Form } from 'vee-validate';
// Supporting files
import { experimentSettings } from '@/constants/experiments';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import settleAllPromises from '@/helpers/layout-helper';
import routerParams from '@/router/router-constants/router-params';
@ -345,13 +344,8 @@ export default {
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true }
);
} else if (matchingCars.length === 1) {
const ymmsBailoutEnabled = this.getSettingValue(experimentSettings.ISS_YMMS_VIN_REQUIRED_BAILOUT_ENABLED) === 'true';
if (ymmsBailoutEnabled && this.mainStore.order.vehicle.vinRequired) {
this.mainStore.setBailout(bailoutMessage.YMMNotFound());
return this.$router.navigate(
this.navigationScenarios.BAILOUT,
this.$route
);
if (await this.handleVinRequiredVehicle(this.vin)) {
return;
}
await this.navigateForwardWithSingleCarMatch();
} else {

View file

@ -6,6 +6,7 @@ import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import vinPagesMixin from '@/mixins/vin-pages-mixin';
jest.mock('@/helpers/damage-helper', () => ({
isGlassAvailableForCarId: jest.fn().mockImplementation(() => true),
@ -24,6 +25,11 @@ jest.mock('@/helpers/cms-content-helper', () => ({
// Mock our module for promises.
jest.mock('@/helpers/layout-helper.js', () => jest.fn());
jest.mock('@/mixins/vin-pages-mixin', () => ({
methods: {
handleVinRequiredVehicle: jest.fn().mockReturnValue(false)
}
}));
function setupMocks({
route = null,
@ -121,7 +127,7 @@ function setupMocks({
}
};
mountOptions.mixins = [mockMixin];
mountOptions.mixins = [mockMixin, vinPagesMixin];
const wrapper = shallowMount(addressVehicles, mountOptions);

View file

@ -72,7 +72,6 @@ import settleAllPromises from '@/helpers/layout-helper';
import { useMainStore } from '@/store';
import issPageValues from '@/router/router-constants/issPage-values';
import errorMessages from '@/constants/error-messages';
import { experimentSettings } from '@/constants/experiments';
import { required } from '@/helpers/validation-rules';
import { Form, defineRule } from 'vee-validate';
import { isGlassAvailableForCarId } from '@/helpers/damage-helper';
@ -233,6 +232,10 @@ export default {
},
async forwardButtonAction() {
this.mainStore.resetBailout();
if (await this.handleVinRequiredVehicle(this.vin)) {
return;
}
const vinLookup = await useMainStore().lookupVehicleByVin(this.selectedVehicle.vin);
if (!vinLookup) {
return;
@ -257,15 +260,6 @@ export default {
false
);
const ymmsBailoutEnabled = this.getSettingValue(experimentSettings.ISS_YMMS_VIN_REQUIRED_BAILOUT_ENABLED) === 'true';
if (ymmsBailoutEnabled && this.mainStore.order.vehicle.vinRequired) {
this.mainStore.setBailout(bailoutMessage.YMMNotFound());
return this.$router.navigate(
this.navigationScenarios.BAILOUT,
this.$route
);
}
await this.navigateForward();
},
async navigateForward() {
@ -282,6 +276,9 @@ export default {
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true }
);
} else {
if (await this.handleVinRequiredVehicle(this.vin)) {
return;
}
await this.navigateForwardWithSingleCarMatch();
}
},

View file

@ -307,11 +307,7 @@ export default {
const ymmsBailoutEnabled = this.getSettingValue(experimentSettings.ISS_YMMS_VIN_REQUIRED_BAILOUT_ENABLED) === 'true';
if (ymmsBailoutEnabled && this.mainStore.order.vehicle.vinRequired) {
this.mainStore.setBailout(bailoutMessage.YMMNotFound());
return this.$router.navigate(
this.navigationScenarios.BAILOUT,
this.$route
);
return this.$router.navigateBailout(bailoutMessage.YMMNotFound());
}
return this.navigateForward();

View file

@ -51,8 +51,8 @@ const getPartsOrQuestions = {
methodName: 'getPartsOrQuestions',
mockResponse: null
};
const getPartsOrQuestionsVinRequired = {
methodName: 'getPartsOrQuestionsVinRequired',
const getPartsOrQuestionsV2 = {
methodName: 'getPartsOrQuestionsV2',
mockResponse: null
};
@ -384,8 +384,8 @@ describe('vin-lookup.vue', () => {
.mockResolvedValue(lookupVehicleByVin.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName)
.mockResolvedValue(getPartsOrQuestions.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName)
.mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsV2.methodName)
.mockResolvedValue(getPartsOrQuestionsV2.mockResponse);
const { container } = render(VinLookupComponent, mountOptions);
@ -417,8 +417,8 @@ describe('vin-lookup.vue', () => {
.mockResolvedValue(lookupVehicleByVin.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName)
.mockResolvedValue(getPartsOrQuestions.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName)
.mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsV2.methodName)
.mockResolvedValue(getPartsOrQuestionsV2.mockResponse);
const { container } = render(VinLookupComponent, mountOptions);
@ -452,8 +452,8 @@ describe('vin-lookup.vue', () => {
.mockResolvedValue(lookupVehicleByVin.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName)
.mockResolvedValue(getPartsOrQuestions.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName)
.mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsV2.methodName)
.mockResolvedValue(getPartsOrQuestionsV2.mockResponse);
store.getCapabilityQuestions.mockResolvedValueOnce({ data: [] });
const { container } = render(VinLookupComponent, mountOptions);
@ -487,8 +487,8 @@ describe('vin-lookup.vue', () => {
.mockResolvedValue(lookupVehicleByVin.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName)
.mockResolvedValue(getPartsOrQuestions.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName)
.mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsV2.methodName)
.mockResolvedValue(getPartsOrQuestionsV2.mockResponse);
const { container } = render(VinLookupComponent, mountOptions);
@ -525,8 +525,8 @@ describe('vin-lookup.vue', () => {
.mockResolvedValue(lookupVehicleByVin.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName)
.mockResolvedValue(getPartsOrQuestions.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsVinRequired.methodName)
.mockResolvedValue(getPartsOrQuestionsVinRequired.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestionsV2.methodName)
.mockResolvedValue(getPartsOrQuestionsV2.mockResponse);
const { container } = render(VinLookupComponent, mountOptions);

View file

@ -42,7 +42,6 @@
<script>
// Import Supporting Files
import { computed } from 'vue';
import { experimentSettings } from '@/constants/experiments';
import vehicleLookupAlertTypes from '@/constants/vehicle-lookup-alert-types';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { isGlassAvailableForCarId } from '@/helpers/damage-helper';
@ -62,6 +61,7 @@ import vinLocationInformation from '@/layouts/vin-lookup/vin-location-informatio
import vinLookupAlerts from '@/layouts/vin-lookup/vin-lookup-alerts/vin-lookup-alerts.vue';
import vinQuestion from '@/layouts/vin-lookup/vin-question/vin-question.vue';
import bailoutMessage from '@/constants/bailoutMessage';
import vinPagesMixin from '@/mixins/vin-pages-mixin';
export default {
name: 'vin-lookup',
@ -74,7 +74,7 @@ export default {
vinLookupAlerts,
vinQuestion
},
mixins: [baseFormMixin, vehicleQuestionsMixin],
mixins: [baseFormMixin, vehicleQuestionsMixin, vinPagesMixin],
provide() {
return {
vehicleFromLookup: computed(() => this.vehicleFromLookup)
@ -177,14 +177,8 @@ export default {
this.$refs.siteFooter.enableForwardAction();
showIssLoadingModal(true);
const ymmsBailoutEnabled = this.getSettingValue(experimentSettings.ISS_YMMS_VIN_REQUIRED_BAILOUT_ENABLED) === 'true';
if (ymmsBailoutEnabled && this.mainStore.order.vehicle.vinRequired) {
this.mainStore.order.vehicle.vin = this.vin;
this.mainStore.setBailout(bailoutMessage.YMMNotFound());
return this.$router.navigate(
this.navigationScenarios.BAILOUT,
this.$route
);
if (await this.handleVinRequiredVehicle(this.vin)) {
return;
}
if (this.needToLookupVehicle) {
@ -273,30 +267,6 @@ export default {
return;
}
/*
* If the vehicle is vinRequired, we need to call getPartsOrQuestionsVinRequired() to get the parts or questions.
* If the vehicle is not vinRequired, we can call getPartsOrQuestions() to get the parts or questions.
*/
/*
if (this.mainStore.order.vehicle.vinRequired) {
try {
const partsOrQuestionsResponse = await this.getPartsOrQuestionsVinRequired();
await this.navigateForward(
partsOrQuestionsResponse.data.partsOrQuestions,
this
);
} catch (e) {
this.mainStore.setBailout(bailoutMessage.HeavyTruckVehicle(this.vehicleFromLookup.carId));
this.$router.navigate(
this.navigationScenarios.BAILOUT,
this.$route
);
throw e;
}
return;
}
*/
const partsOrQuestionsResponse = await this.getPartsOrQuestions();
// Comes from vehicleQuestionsMixin.navigateForward()

View file

@ -33,6 +33,9 @@ export default {
self.$router.navigateWithSpinner(scenario, self.$route);
},
navigateBailout(bailoutData = null) {
this.$router.navigateBailout(bailoutData);
},
savePageDataToStore(page, data) {
useMainStore().updatePageData({ page, data });
},

View file

@ -355,8 +355,8 @@ export default {
async getPartsOrQuestions() {
return useMainStore().getPartsOrQuestions();
},
async getPartsOrQuestionsVinRequired() {
return useMainStore().getPartsOrQuestionsVinRequired();
async getPartsOrQuestionsV2() {
return useMainStore().getPartsOrQuestionsV2();
},
// Can't use `this` because navigateForward is also called from vin-pages-mixin

View file

@ -1,5 +1,7 @@
import { useMainStore } from '@/store';
import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin';
import { experimentSettings } from '@/constants/experiments';
import bailoutMessage from '@/constants/bailoutMessage';
export default {
methods: {
@ -8,6 +10,26 @@ export default {
const { partsOrQuestions } = result.data;
vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this);
},
async handleVinRequiredVehicle(vin) {
if (!this.mainStore.order.vehicle.vinRequired) {
return false;
}
this.mainStore.order.vehicle.vin = vin;
if (this.hasSettingEqualTo(experimentSettings.ISS_YMMS_VIN_REQUIRED_BAILOUT_ENABLED, 'true')) {
this.navigateBailout(bailoutMessage.YMMNotFound());
return true;
}
const partsOrQuestionsResponse = await this.getPartsOrQuestionsV2();
if (partsOrQuestionsResponse.data.partsOrQuestions.length === 0) {
this.navigateBailout(bailoutMessage.YMMNotFound('No Parts Returned'));
return true;
}
await this.navigateForward(partsOrQuestionsResponse.data.partsOrQuestions, this);
return true;
}
}
};

View file

@ -3,17 +3,30 @@ import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin';
import { useMainStore } from '@/store';
import experimentMixin from '@/mixins/experiment-mixin';
jest.mock('@/mixins/vehicle-questions-mixin', () => ({
methods: {
navigateForward: jest.fn(),
getPartsOrQuestionsV2: jest.fn()
}
}));
jest.mock('@/mixins/experiment-mixin', () => ({
methods: {
hasSettingEqualTo: jest.fn(),
}
}));
/** @ignore */
function setupMocks() {
const mocks = getMountOptions({
router: {
navigate: jest.fn()
navigate: jest.fn(),
navigateBailout: jest.fn()
}
});
const mockVinComponent = {
mixins: [vinPagesMixin]
mixins: [vinPagesMixin, experimentMixin, vehicleQuestionsMixin]
};
const wrapper = shallowMount(mockVinComponent, mocks);
@ -40,4 +53,64 @@ describe('vin-pages-mixin', () => {
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalled();
});
});
describe('handleVinRequiredVehicle', () => {
const vin = '12345678901234567';
test('should return false for non vin required vehicle', async () => {
// Arrange
useMainStore().order.vehicle.vinRequired = false;
const { wrapper } = setupMocks({});
// Act
const handled = await wrapper.vm.handleVinRequiredVehicle(vin);
// Assert
expect(handled).toBeFalsy();
});
test('should bailout if bailout experiment setting is true', async () => {
// Arrange
useMainStore().order.vehicle.vinRequired = true;
experimentMixin.methods.hasSettingEqualTo = jest.fn().mockReturnValue(true);
const { wrapper } = setupMocks({});
// Act
const handled = await wrapper.vm.handleVinRequiredVehicle(vin);
// Assert
expect(handled).toBeTruthy();
expect(wrapper.vm.navigateBailout).toHaveBeenCalled();
});
test('should navigateBailout when parts not found', async () => {
// Arrange
vehicleQuestionsMixin.methods.getPartsOrQuestionsV2 = () => ({ data: { partsOrQuestions: [] } });
useMainStore().order.vehicle.vinRequired = true;
const { wrapper } = setupMocks({});
// Act
const handled = await wrapper.vm.handleVinRequiredVehicle(vin);
// Assert
expect(handled).toBeTruthy();
expect(wrapper.vm.navigateBailout).toHaveBeenCalled();
});
test('should navigate next when parts found', async () => {
// Arrange
useMainStore().order.vehicle.vinRequired = true;
experimentMixin.methods.hasSettingEqualTo = jest.fn().mockReturnValue(false);
vehicleQuestionsMixin.methods.getPartsOrQuestionsV2 = () => ({ data: { partsOrQuestions: [{}] } });
vehicleQuestionsMixin.methods.navigateForward = jest.fn();
const { wrapper } = setupMocks({});
// Act
const handled = await wrapper.vm.handleVinRequiredVehicle(vin);
// Assert
expect(handled).toBeTruthy();
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalled();
});
});
});

View file

@ -864,7 +864,8 @@ export const useMainStore = defineStore({
glassPieces: glassArrayForPayload,
zip: zipCode,
vin,
oemEndorsementFlag: this.hasOemEndorsement
oemEndorsementFlag: this.hasOemEndorsement,
parentAccountNumber: this.order.parentAccountNumber
};
},
async getPartsOrQuestions() {
@ -882,19 +883,30 @@ export const useMainStore = defineStore({
return response;
},
async getPartsOrQuestionsVinRequired() {
async getPartsOrQuestionsV2() {
this.resetPartsAndDependencies();
const payload = this.getPartsOrQuestionsPayload();
const response = await globalMethods.callHttpClient({
method: endpoints.GetPartsOrQuestionsVinRequired.method,
endpoint: endpoints.GetPartsOrQuestionsVinRequired.url,
let response = await globalMethods.callHttpClient({
method: endpoints.GetPartsOrQuestionsV2.method,
endpoint: endpoints.GetPartsOrQuestionsV2.url,
payload: payload
});
// Flatten location and name properties
response.data.partsOrQuestions = convertGlassPieceNamingFromApi(response.data.partsOrQuestions);
// If no parts are returned for non oem check for OEM parts
if (!payload.oemEndorsementFlag && response.data.partsOrQuestions.length === 0) {
payload.oemEndorsementFlag = true;
response = await globalMethods.callHttpClient({
method: endpoints.GetPartsOrQuestionsV2.method,
endpoint: endpoints.GetPartsOrQuestionsV2.url,
payload: payload
});
response.data.partsOrQuestions = convertGlassPieceNamingFromApi(response.data.partsOrQuestions);
}
return response;
},
async getParts() {