Merge branch 'feature/uboppa/INSR-8668' of https://github.com/Safelite/DigitalConsumer.ISS into feature/uboppa/INSR-8668

This commit is contained in:
Udayboppasafelite 2026-06-24 13:09:17 -04:00
commit 78b01713e0
9 changed files with 883 additions and 822 deletions

1447
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -48,7 +48,7 @@
"@types/dotenv-safe": "^8.1.6",
"@types/form-data": "^2.2.1",
"@types/node": "^22.7.5",
"@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue": "6.0.7",
"@vitest/coverage-v8": "^4.1.8",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-router": "~5.0.0",
@ -73,7 +73,7 @@
"jest": "^27.0.5",
"jest-junit": "^13.0.0",
"jest-serializer-vue": "^3.1.0",
"jsdoc": "^4.0.2",
"jsdoc": "^4.0.5",
"jsdom": "^22.1.0",
"luxon": "^3.5.0",
"ortoni-report": "^2.0.8",
@ -82,7 +82,7 @@
"sass-loader": "^12.0.0",
"saucectl": "^0.188.0",
"typescript-eslint": "^8.11.0",
"vite": "^6.4.2",
"vite": "^8.0.16",
"vitest": "^4.1.8",
"vue-eslint-parser": "^10.2.0",
"wait-on": "^8.0.2"

View file

@ -52,7 +52,7 @@ export class Logger {
return [
`Application: ${applicationConfig.APPLICATION_NAME}`,
`Build Version: ${store.issConfig?.buildInfo?.versionString}`,
`Build Time: ${store.issConfig?.buildInfo?.buildTimeLocal}`,
`Build Time: ${store.issConfig?.buildInfo?.buildTime}`,
`ClientTag: ${store.issConfig?.clientTag}`,
`ParentAccountNumber: ${store.issConfig?.parentAccountNumber}`,
`ClientName: ${store.issConfig?.clientName}`,

View file

@ -120,5 +120,9 @@ export function anyPartWithRequiresRecalFlag(lineItems) {
}
export function hasMSRPart(lineItems) {
return lineItems?.feeItems?.some((item) => item.partNumber === partNumberStrings.RECAL_MOBILEDUAL || item.partNumber === partNumberStrings.RECAL_MOBILE) ?? false
return lineItems?.feeItems?.some((item) => isMSRPart(item)) ?? false;
}
export function isMSRPart(part) {
return part.partNumber === partNumberStrings.RECAL_MOBILEDUAL || part.partNumber === partNumberStrings.RECAL_MOBILE;
}

View file

@ -4,9 +4,11 @@ import { createTestingPinia } from '@pinia/testing';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import navigationScenarios from '@/router/router-constants/navigation-scenarios';
import routerParams from '@/router/router-constants/router-params';
import { getPricedMobileFeePart } from '@/helpers/service-location-helper';
import { useMainStore } from '@/store';
import serviceLocation from '@/layouts/schedule-page/service-location/service-location.vue';
import coverageTypes from '@/constants/coverage-type';
import coverageStatuses from '@/constants/coverage-statuses';
import { experimentSettings } from '@/constants/experiments';
const mockGetServiceabilityDetails = () => {
const serviceabilityDetails = {
@ -84,12 +86,23 @@ const mockProviders = () => Promise.resolve([
isSafeliteShop: true
}
]);
const defaultMobileFeePart = {
"partNumber": "RECAL MOBILEDUAL",
"description": null,
"partType": "MOBILE FEE",
"isInsurable": false,
"displayName": "Advanced mobile",
"type": null,
"laborAmount": 0,
"sellingPrice": 50,
"kitPrice": 0
}
jest.mock(
'@/helpers/service-location-helper',
() => ({
getZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)),
getMobileZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)),
getPricedMobileFeePart: jest.fn(() => Promise.resolve(null))
getPricedMobileFeePart: jest.fn(() => Promise.resolve(defaultMobileFeePart))
})
);
@ -135,13 +148,17 @@ const mountOptions = {
}
}
};
function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP, coverageStatus = coverageStatuses.PENDING, coverageType = coverageTypes.NONE }, splitPaySettingValue = 'false') {
mountOptions.global.plugins = [createTestingPinia({
initialState: {
main: {
order: {
serviceLocation: {
appointmentType
},
insuranceCoverage: {
coverageStatus,
coverageType
}
}
}
@ -149,6 +166,23 @@ function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
})];
useMainStore().getSafeliteProviders = jest.fn().mockImplementation(() => mockProviders());
useMainStore().getServiceabilityDetails = jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode));
const mockMixin = {
methods: {
getSettingValue: jest.fn((settingName) => {
if (settingName === experimentSettings.MSR_SPLIT_PAY_ENABLED) {
return splitPaySettingValue;
}
return 'false';
})
}
};
const cmsMixin = {
methods: {
getCmsContent: jest.fn(() => "")
}
}
mountOptions.mixins = [mockMixin, cmsMixin];
const wrapper = mount(serviceLocation, mountOptions);
wrapper.vm.$router.navigateWithSpinner = jest.fn();
@ -212,4 +246,91 @@ describe('service-location.vue', () => {
expect(wrapper.vm.zipContainsMilitaryBase).toBe(true);
});
});
describe('computed', () => {
describe('displayMSRFeeAlert', () => {
test('returns false with no mobile fee', () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.mobileFeePart = null;
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(false);
});
test('returns false with a mobile fee with no price', () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.mobileFeePart = { ...defaultMobileFeePart, sellingPrice: 0 };
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(false);
});
test('returns true for priced mobile fee for No Comp', () => {
// Arrange
const { wrapper } = setupMocks({ coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageTypes.NO_COMP });
wrapper.vm.mobileFeePart = defaultMobileFeePart;
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(true);
});
test('returns true for priced mobile fee for ITAC', () => {
// Arrange
const { wrapper } = setupMocks({ coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageTypes.ITAC });
wrapper.vm.mobileFeePart = defaultMobileFeePart;
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(true);
});
test('returns false for priced mobile fee for Deductible without Split Pay', () => {
// Arrange
const { wrapper } = setupMocks({ coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageTypes.Deductible }, 'false');
wrapper.vm.mobileFeePart = defaultMobileFeePart;
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(false);
});
test('returns false for priced mobile fee for Deductible with Split Pay and isInsurable=true', () => {
// Arrange
const { wrapper } = setupMocks({ coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageTypes.Deductible }, 'true');
wrapper.vm.mobileFeePart = { ...defaultMobileFeePart, isInsurable: true };
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(false);
});
test('returns true for priced mobile fee for Deductible with Split Pay and isInsurable=false', () => {
// Arrange
const { wrapper } = setupMocks({ coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageTypes.Deductible }, 'true');
wrapper.vm.mobileFeePart = { ...defaultMobileFeePart, isInsurable: false };
// Act
const result = wrapper.vm.displayMSRFeeAlert;
// Assert
expect(result).toBe(true);
});
});
});
});

View file

@ -94,6 +94,12 @@
{{ militaryBaseWarningText }}
</div>
</div>
<alert
v-if="displayMSRFeeAlert"
:manualCopy="msrFeeAlertCopy"
cmsWidgetName="AlertMSRFeeWidget"
:isCollapsible="true"
alertClass="alert-warning" />
<serviceZipQuestion
ref="mobileServiceZipCodeQuestion"
customInputId="mobileServiceZipCode"
@ -133,11 +139,10 @@ import {
getMobileZipCodeData
} from '@/helpers/service-location-helper';
import { toTitleCase } from '@/helpers/text-helper.js';
import { isRecalOrder } from '@/helpers/recal-helper';
import { isMSRPart, isRecalOrder } from '@/helpers/recal-helper';
// Import Component
import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import appointmentTypeQuestion from '@/layouts/schedule-page/service-location/appointment-type-question/appointment-type-question.vue';
import baseFormMixin from '@/mixins/base-form-mixin';
import shopAddress from '@/layouts/schedule-page/service-location/shop-address/shop-address.vue';
@ -146,6 +151,7 @@ import widgetFields from '@/constants/cms-widget-fields';
import recalModal from '@/iss-components/recal-modal/recal-modal.vue';
import { getPricedMobileFeePart } from '@/helpers/service-location-helper';
import { processIfStatements } from '@/helpers/cms-content-helper';
import { experimentSettings } from '@/constants/experiments';
const RECAL_MODAL_REF_NAME = 'RecalModal';
@ -153,7 +159,6 @@ export default {
name: 'service-location',
components: {
alert,
textBlock,
appointmentTypeQuestion,
shopAddress,
serviceZipQuestion,
@ -294,6 +299,38 @@ export default {
}
const bodyText = this.getCmsContent('WhereWouldYouLikeServiceWidget', widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
return this.processIfStatements(bodyText, 'custom', this.getCustomValueFromString);
},
displayMSRFeeAlert() {
if (!this.mobileFeePart || !isMSRPart(this.mobileFeePart)) {
return false;
}
const mobileFeeAmount = (this.mobileFeePart.kitPrice ?? 0)
+ (this.mobileFeePart.laborAmount ?? 0)
+ (this.mobileFeePart.sellingPrice ?? 0);
if (mobileFeeAmount <= 0) {
return false;
}
if (this.mainStore.isITAC || this.mainStore.isNoComp) {
return true;
}
const isSplitPayEnabled = this.getSettingValue(experimentSettings.MSR_SPLIT_PAY_ENABLED) === 'true';
if (!isSplitPayEnabled) {
return false;
}
return !this.mobileFeePart.isInsurable;
},
msrFeeAlertCopy() {
const mobileFeeAmount = (this.mobileFeePart?.kitPrice ?? 0)
+ (this.mobileFeePart?.laborAmount ?? 0)
+ (this.mobileFeePart?.sellingPrice ?? 0);
let content = this.getCmsContent('AlertMSRFeeWidget', widgetFields.ALERT_WIDGET.BODY_TEXT);
content = content.replace('{custom:mobileFee}', `${mobileFeeAmount.toFixed(2)}`);
return content;
}
},
watch: {

View file

@ -82,6 +82,7 @@ export default {
sessionKey: getSessionKeyValue(),
pageName: currentPageName,
referralSequenceNumber: order.referralSequenceNumber,
referralNumber: order.referralNumber,
parentAccountNumber: parentAccountNumber,
sessionId: getSessionIdValue(),
action: '',
@ -111,6 +112,7 @@ export default {
sessionKey: getSessionKeyValue(),
pageName: currentPageName,
referralSequenceNumber: order.referralSequenceNumber,
referralNumber: order.referralNumber,
parentAccountNumber: parentAccountNumber,
sessionId: getSessionIdValue(),
category,

View file

@ -125,7 +125,8 @@ export const getDefaultState = () => ({
},
firstName: null,
lastName: null,
emailAddress: null
emailAddress: null,
companyCode: null
},
serviceLocation: {
address: null,
@ -596,6 +597,7 @@ export const useMainStore = defineStore({
order.customer.address.zipCode = insured?.zipCode?.toString();
order.customer.firstName = insured?.firstName;
order.customer.lastName = insured?.lastName;
order.customer.companyCode = insured?.companyCode;
// populate additional fields
order.policy.policyData = responsePolicy.policyData;
@ -646,6 +648,7 @@ export const useMainStore = defineStore({
// This is here in case we have to deploy site for other fixes before coverage service gets updated.
isRepair: this.order.damage.isRepair,
isItac: isITAC,
sourceSystem: applicationConfig.APPLICATION_NAME,
insured: {
firstName: this.order.customer.firstName,
lastName: this.order.customer.lastName,
@ -676,6 +679,7 @@ export const useMainStore = defineStore({
actualDeductible: this.currentDeductible.toString() ?? '',
currentRepairDeductible: this.order.currentDeductible.repair,
currentReplaceDeductible: this.order.currentDeductible.replace,
company: this.order.customer.companyCode
},
lossInfo: {
dateOfLoss: this.order.policy.dateOfLoss,
@ -1613,12 +1617,15 @@ export const useMainStore = defineStore({
}
});
},
async loadSession(duplicate) {
const { order } = this;
async loadSession(duplicate) {
const { applicationUser, order } = this;
// NOTE: We are re-using the current user's saved session id (this is required on the load-session call).
// This should be ok, but should probably validate.
const response = await globalMethods.callHttpClient({
method: endpoints.LoadSession.method,
endpoint: endpoints.LoadSession.url,
payload: {
savedSessionId: applicationUser.savedSessionId,
referralNumber: duplicate.referralNumber,
referralDate: duplicate.referralDate,
parentAccountNumber: order.parentAccountNumber,
@ -2175,6 +2182,7 @@ export const useMainStore = defineStore({
this.order.customer.address.zipCode = null;
this.order.customer.address.streetAddress = null;
this.order.customer.address.streetAddress2 = null;
this.order.customer.companyCode = null;
},
resetContactInfo() {
@ -2574,13 +2582,14 @@ export const useMainStore = defineStore({
});
}
},
async logPageView({ userId, sessionKey, pageName, referralSequenceNumber, parentAccountNumber, sessionId, action, event, shouldUseSessionId, experimentsForUser }) {
async logPageView({ userId, sessionKey, pageName, referralSequenceNumber, referralNumber, parentAccountNumber, sessionId, action, event, shouldUseSessionId, experimentsForUser }) {
const payload = {
userId,
sessionKey,
sessionId,
pageName,
referralSequenceNumber,
referralNumber,
parentAccountNumber,
applicationName: applicationConfig.APPLICATION_NAME,
action,
@ -2602,7 +2611,7 @@ export const useMainStore = defineStore({
}
);
},
async logCustomEvent({ userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser, referralSequenceNumber, parentAccountNumber }) {
async logCustomEvent({ userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser, referralSequenceNumber, referralNumber, parentAccountNumber }) {
if (pageName == null || pageName.length === 0) { pageName = 'none'; }
const payload = {
@ -2618,6 +2627,7 @@ export const useMainStore = defineStore({
shouldUseSessionId,
experimentsForUser,
referralSequenceNumber,
referralNumber,
parentAccountNumber
};

View file

@ -16,6 +16,7 @@ import coverageType from '@/constants/coverage-type';
import { getEnumName } from '@/helpers/unit-test-helper';
import { strictEqual } from 'assert';
import partNumberStrings from '@/constants/part-number-strings';
import applicationConfig from '@/constants/application-config';
describe('Store', () => {
let store;
@ -481,6 +482,42 @@ describe('Store', () => {
expect(store.order.insuranceCoverage.coverageStatus).toBe(coverageStatuses.NO_COVERAGE);
expect(store.order.insuranceCoverage.claimNumber).toBe(null);
});
it('calls api with expected data', async () => {
// Arrange
const companyCode = getRandomString(6, 6);
const originalDeductible = {
replace: 500,
repair: 0
};
const currentDeductible = {
replace: 500,
repair: 0
};
store.order.originalDeductible = {
replace: originalDeductible.replace,
repair: originalDeductible.repair
};
store.order.currentDeductible = {
replace: currentDeductible.replace,
repair: currentDeductible.repair
};
store.order.customer.companyCode = companyCode;
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
// Act
await store.registerClaim();
// Asserts
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({
payload: expect.objectContaining({
sourceSystem: applicationConfig.APPLICATION_NAME,
policyInfo: expect.objectContaining({
company: companyCode
})
})
}));
});
});
describe('updateContactInfo method', () => {
@ -1299,6 +1336,7 @@ describe('Store', () => {
const zipCode = getRandomString(6, 6);
const firstName = getRandomString(6, 6);
const lastName = getRandomString(6, 6);
const companyCode = getRandomString(6, 6);
const vehicle = { id: '123' };
@ -1313,7 +1351,8 @@ describe('Store', () => {
state,
zipCode,
firstName,
lastName
lastName,
companyCode
}
],
vehicles: [
@ -1335,6 +1374,7 @@ describe('Store', () => {
await expect(store.order.customer.address.zipCode).toBe(zipCode);
await expect(store.order.customer.firstName).toBe(firstName);
await expect(store.order.customer.lastName).toBe(lastName);
await expect(store.order.customer.companyCode).toBe(companyCode)
await expect(store.order.policy.vehicles).toStrictEqual([vehicle]);
});
it('calls api with expected data', async () => {
@ -1404,7 +1444,8 @@ describe('Store', () => {
lastName: getRandomString(6, 6),
city: getRandomString(6, 6),
state: getRandomString(6, 6),
zipCode: getRandomString(6, 6)
zipCode: getRandomString(6, 6),
companyCode: getRandomString(6, 6)
};
const vehicles = [
{ name: getRandomString(6, 6) },
@ -1435,6 +1476,7 @@ describe('Store', () => {
expect(store.order.customer.address.zipCode).toBe(insured.zipCode);
expect(store.order.customer.firstName).toBe(insured.firstName);
expect(store.order.customer.lastName).toBe(insured.lastName);
expect(store.order.customer.companyCode).toBe(insured.companyCode);
expect(store.order.policy.vehicles).toEqual(vehicles);
});
@ -2194,12 +2236,14 @@ describe('Store', () => {
describe('loadSession method', () => {
let mockDuplicate;
let mockResponse;
let savedSessionId = getRandomGuid();
beforeEach(() => {
mockDuplicate = {
referralNumber: getRandomString(10, 10),
referralDate: new Date().toISOString(),
correlationId: getRandomGuid()
correlationId: getRandomGuid(),
savedSessionId: savedSessionId
};
mockResponse = {
@ -2249,6 +2293,7 @@ describe('Store', () => {
it('should call callHttpClient with correct parameters', async () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(mockResponse));
store.applicationUser.savedSessionId = savedSessionId;
// Act
await store.loadSession(mockDuplicate);
@ -2258,6 +2303,7 @@ describe('Store', () => {
method: endpoints.LoadSession.method,
endpoint: endpoints.LoadSession.url,
payload: {
savedSessionId: mockDuplicate.savedSessionId,
referralNumber: mockDuplicate.referralNumber,
referralDate: mockDuplicate.referralDate,
parentAccountNumber: store.order.parentAccountNumber,