Merge pull request #3265 from Safelite/nation/CASH-3036
CASH-3036 add additional policy fields
This commit is contained in:
commit
cd532dfbff
8 changed files with 385 additions and 6 deletions
|
|
@ -43,6 +43,7 @@ const errorMessages = {
|
|||
INSURANCE_COMPANY_NAME_REQUIRED: "Please enter your insurance company name",
|
||||
POLICY_NUMBER_REQUIRED: "Please enter your policy number",
|
||||
POLICY_NUMBER_FORMAT: "Please enter a valid policy number",
|
||||
CLAIM_NUMBER_REQUIRED: "Please enter your claim number",
|
||||
CLAIM_NUMBER_VALID: "Please enter a valid claim number",
|
||||
CLAIM_NUMBER_FORMAT: "Claim number must be 6 to 8 digits",
|
||||
DATE_OF_LOSS_REQUIRED: "Please enter your date of damage",
|
||||
|
|
|
|||
54
src/constants/more-policy-questions.js
Normal file
54
src/constants/more-policy-questions.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// CMS answer `Name` values that can appear in MorePolicyQuestionsWidget /
|
||||
// MorePolicyQuestionsOverrideWidget's Answers array (CMS content is a superset
|
||||
// across both widget variants).
|
||||
export const morePolicyQuestionAnswers = {
|
||||
ADDITIONAL_DAMAGE: "AdditionalDamage",
|
||||
RENTAL: "Rental",
|
||||
OTHER_RESPONSIBLE_PARTY: "OtherResponsibleParty",
|
||||
THIRD_PARTY_VEHICLE: "ThirdPartyVehicle",
|
||||
INJURIES: "Injuries",
|
||||
};
|
||||
|
||||
// Maps each possible CMS answer Name to the boolean flag persisted in the store
|
||||
// and sent to the save-session API, regardless of which widget variant (and
|
||||
// therefore which subset of answers) is shown to the user.
|
||||
export const morePolicyQuestionAnswerToFlag = {
|
||||
[morePolicyQuestionAnswers.ADDITIONAL_DAMAGE]: "additionalDamage",
|
||||
[morePolicyQuestionAnswers.RENTAL]: "rental",
|
||||
[morePolicyQuestionAnswers.OTHER_RESPONSIBLE_PARTY]: "otherResponsibleParty",
|
||||
[morePolicyQuestionAnswers.THIRD_PARTY_VEHICLE]: "thirdPartyVehicle",
|
||||
[morePolicyQuestionAnswers.INJURIES]: "injuries",
|
||||
};
|
||||
|
||||
export function getDefaultMorePolicyQuestions() {
|
||||
return Object.values(morePolicyQuestionAnswerToFlag).reduce((flags, flagName) => {
|
||||
flags[flagName] = false;
|
||||
return flags;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Converts the array of selected CMS answer Names (as emitted by the
|
||||
// checkbox-based buttonQuestion component) into the boolean-flag object shape
|
||||
// that is persisted to the store and sent to the backend.
|
||||
export function morePolicyQuestionAnswerNamesToFlags(selectedAnswerNames) {
|
||||
const flags = getDefaultMorePolicyQuestions();
|
||||
for (const answerName of Array.isArray(selectedAnswerNames) ? selectedAnswerNames : []) {
|
||||
const flagName = morePolicyQuestionAnswerToFlag[answerName];
|
||||
if (flagName) {
|
||||
flags[flagName] = true;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
// Reverses morePolicyQuestionAnswerNamesToFlags, so the checkbox-based
|
||||
// buttonQuestion component (which expects an array of selected values) can be
|
||||
// pre-populated from previously persisted flags.
|
||||
export function morePolicyQuestionFlagsToAnswerNames(flags) {
|
||||
if (!flags) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(morePolicyQuestionAnswerToFlag)
|
||||
.filter(([, flagName]) => flags[flagName])
|
||||
.map(([answerName]) => answerName);
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import morePolicyQuestions from "./more-policy-questions";
|
||||
|
||||
const answersFromCms = [
|
||||
{ Name: "AdditionalDamage", Text: "Additional damage occurred" },
|
||||
{ Name: "Rental", Text: "The vehicle was a rental" },
|
||||
{ Name: "OtherResponsibleParty", Text: "Another party is responsible" },
|
||||
];
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn().mockImplementation((widgetName, property) => {
|
||||
if (property === "Answers") {
|
||||
return answersFromCms;
|
||||
}
|
||||
return "Please select all that apply";
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
function mountComponent(modelValue) {
|
||||
return shallowMount(morePolicyQuestions, {
|
||||
props: {
|
||||
modelValue,
|
||||
groupName: "MorePolicyQuestionsQuestion",
|
||||
cmsWidgetName: "MorePolicyQuestionsWidget",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
}
|
||||
|
||||
describe("more-policy-questions.vue", () => {
|
||||
it("converts previously selected boolean flags into the array of CMS answer Names for the checkbox group", () => {
|
||||
// Act
|
||||
const wrapper = mountComponent({
|
||||
additionalDamage: true,
|
||||
rental: false,
|
||||
otherResponsibleParty: true,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.selectedValues).toEqual(["AdditionalDamage", "OtherResponsibleParty"]);
|
||||
});
|
||||
|
||||
it("treats a null/undefined modelValue as no answers selected", () => {
|
||||
// Act
|
||||
const wrapper = mountComponent(undefined);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.selectedValues).toEqual([]);
|
||||
});
|
||||
|
||||
it("emits a full set of boolean flags reflecting which CMS answer Names were checked", () => {
|
||||
// Arrange
|
||||
const wrapper = mountComponent({});
|
||||
|
||||
// Act
|
||||
wrapper.vm.selectedValues = ["Rental", "Injuries"];
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
|
||||
additionalDamage: false,
|
||||
rental: true,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("emits every flag as false when no answers are checked", () => {
|
||||
// Arrange
|
||||
const wrapper = mountComponent({ rental: true });
|
||||
|
||||
// Act
|
||||
wrapper.vm.selectedValues = [];
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
|
||||
additionalDamage: false,
|
||||
rental: false,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -13,11 +13,18 @@
|
|||
|
||||
<script>
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import {
|
||||
morePolicyQuestionAnswerNamesToFlags,
|
||||
morePolicyQuestionFlagsToAnswerNames,
|
||||
} from "@/constants/more-policy-questions";
|
||||
|
||||
export default {
|
||||
name: "morePolicyQuestions",
|
||||
props: {
|
||||
modelValue: Array,
|
||||
// Boolean flags keyed by the camelCase name of each possible policy
|
||||
// question (e.g. { additionalDamage, rental, otherResponsibleParty,
|
||||
// thirdPartyVehicle, injuries }), see @/constants/more-policy-questions.
|
||||
modelValue: Object,
|
||||
groupName: String,
|
||||
cmsWidgetName: String,
|
||||
},
|
||||
|
|
@ -33,12 +40,18 @@ export default {
|
|||
if (!Array.isArray(this.answersFromCms)) return [];
|
||||
return this.answersFromCms;
|
||||
},
|
||||
// The checkbox-based buttonQuestion component works with an array of
|
||||
// selected CMS answer Names; translate to/from the boolean-flag object
|
||||
// that this component's own v-model exposes to its parent.
|
||||
selectedValues: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
return morePolicyQuestionFlagsToAnswerNames(this.modelValue);
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
set(newSelectedAnswerNames) {
|
||||
this.$emit(
|
||||
"update:modelValue",
|
||||
morePolicyQuestionAnswerNamesToFlags(newSelectedAnswerNames)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -47,6 +47,82 @@ describe("policy-info.vue", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("displayClaimNumber", () => {
|
||||
test("is false when no morePolicyQuestions flags are set", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.morePolicyQuestions = {
|
||||
additionalDamage: false,
|
||||
rental: false,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
};
|
||||
|
||||
expect(wrapper.vm.displayClaimNumber).toBe(false);
|
||||
});
|
||||
|
||||
test("is true when additionalDamage is checked", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.morePolicyQuestions = {
|
||||
additionalDamage: true,
|
||||
rental: false,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
};
|
||||
|
||||
expect(wrapper.vm.displayClaimNumber).toBe(true);
|
||||
});
|
||||
|
||||
test("is true when rental is checked", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.morePolicyQuestions = {
|
||||
additionalDamage: false,
|
||||
rental: true,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
};
|
||||
|
||||
expect(wrapper.vm.displayClaimNumber).toBe(true);
|
||||
});
|
||||
|
||||
test("is false when only unrelated flags (e.g. otherResponsibleParty, thirdPartyVehicle, injuries) are checked", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.morePolicyQuestions = {
|
||||
additionalDamage: false,
|
||||
rental: false,
|
||||
otherResponsibleParty: true,
|
||||
thirdPartyVehicle: true,
|
||||
injuries: true,
|
||||
};
|
||||
|
||||
expect(wrapper.vm.displayClaimNumber).toBe(false);
|
||||
});
|
||||
|
||||
test("is false when morePolicyQuestions is not yet populated", async () => {
|
||||
const { wrapper } = setupMocks();
|
||||
wrapper.vm.morePolicyQuestions = null;
|
||||
|
||||
expect(wrapper.vm.displayClaimNumber).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("claim-number-required validation rule", () => {
|
||||
test("fails when value is empty", async () => {
|
||||
const result = await validate("", "claim-number-required");
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain(errorMessages.CLAIM_NUMBER_REQUIRED);
|
||||
});
|
||||
|
||||
test("passes when value is provided", async () => {
|
||||
const result = await validate("12345678", "claim-number-required");
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forwardButtonAction", () => {
|
||||
// Regression coverage: clicking Continue must explicitly (re)validate the form
|
||||
// rather than assuming meta.valid/isForwardActionDisabled already caught everything,
|
||||
|
|
|
|||
|
|
@ -180,10 +180,20 @@
|
|||
|
||||
<morePolicyQuestions
|
||||
ref="morePolicyQuestions"
|
||||
class="mb-4"
|
||||
:cmsWidgetName="morePolicyQuestionsWidgetName"
|
||||
v-model="morePolicyQuestions"
|
||||
groupName="MorePolicyQuestionsQuestion" />
|
||||
|
||||
<textboxQuestion
|
||||
v-if="displayClaimNumber"
|
||||
isRequired
|
||||
class="mb-4"
|
||||
cmsWidgetName="ClaimNumberWidget"
|
||||
v-model="claimNumber"
|
||||
inputId="claimNumber"
|
||||
validationRules="claim-number-required" />
|
||||
|
||||
<insuranceNavBar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
|
|
@ -212,6 +222,10 @@ import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-qu
|
|||
import morePolicyQuestions from "@/layouts/policy-info/more-policy-questions/more-policy-questions";
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import { stateOptions } from "@/constants/state-options";
|
||||
import {
|
||||
morePolicyQuestionAnswers,
|
||||
morePolicyQuestionAnswerToFlag,
|
||||
} from "@/constants/more-policy-questions";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
|
@ -317,6 +331,7 @@ defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQU
|
|||
defineRule("loss-time-required", required(errorMessages.LOSS_TIME_REQUIRED));
|
||||
defineRule("loss-location-required", required(errorMessages.LOSS_LOCATION_REQUIRED));
|
||||
defineRule("subrogation-required", required(errorMessages.SUBROGATION_REQUIRED));
|
||||
defineRule("claim-number-required", required(errorMessages.CLAIM_NUMBER_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "policy-info",
|
||||
|
|
@ -339,6 +354,7 @@ export default {
|
|||
morePolicyQuestions: this.getMorePolicyQuestionsFromStore(),
|
||||
subrogationSelectedValue: this.getSubrogationSelectedValueFromStore(),
|
||||
lossLocation: this.getLossLocationFromStore(),
|
||||
claimNumber: this.getClaimNumberFromStore(),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
@ -471,6 +487,17 @@ export default {
|
|||
? "MorePolicyQuestionsOverrideWidget"
|
||||
: "MorePolicyQuestionsWidget";
|
||||
},
|
||||
// Only ask for a claim number if the user indicated additional damage
|
||||
// occurred or the vehicle was a rental, per the morePolicyQuestions answers.
|
||||
displayClaimNumber() {
|
||||
const additionalDamageFlag =
|
||||
morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.ADDITIONAL_DAMAGE];
|
||||
const rentalFlag = morePolicyQuestionAnswerToFlag[morePolicyQuestionAnswers.RENTAL];
|
||||
return Boolean(
|
||||
this.morePolicyQuestions?.[additionalDamageFlag] ||
|
||||
this.morePolicyQuestions?.[rentalFlag]
|
||||
);
|
||||
},
|
||||
},
|
||||
// Ensure CMS content is fetched during route navigation without depending on `to`/`from`
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -560,11 +587,14 @@ export default {
|
|||
return store.getters.order.policy.subrogation;
|
||||
},
|
||||
getMorePolicyQuestionsFromStore() {
|
||||
return store.getters.order.policy.morePolicyQuestions ?? [];
|
||||
return store.getters.order.policy.morePolicyQuestions;
|
||||
},
|
||||
getLossLocationFromStore() {
|
||||
return store.getters.order.policy.lossLocation;
|
||||
},
|
||||
getClaimNumberFromStore() {
|
||||
return store.getters.order.payment.insuranceCoverage.claimNumber;
|
||||
},
|
||||
cancelButtonAction() {
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_CANCEL_VERIFICATION,
|
||||
|
|
@ -599,6 +629,7 @@ export default {
|
|||
dateOfBirth: this.dateOfBirth,
|
||||
subrogation: this.subrogationSelectedValue,
|
||||
streetAddress: this.streetAddress,
|
||||
claimNumber: this.claimNumber,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ import {
|
|||
import { externalParameterStatus } from "@/constants/external-parameters";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { addPricesToLineItems, getAmountDue } from "@/helpers/pricing-helper";
|
||||
|
||||
// Export State
|
||||
const getDefaultState = () => {
|
||||
return {
|
||||
|
|
@ -190,6 +189,13 @@ const getDefaultState = () => {
|
|||
lossLocation: null,
|
||||
lastName: null,
|
||||
subrogation: null,
|
||||
morePolicyQuestions: {
|
||||
additionalDamage: false,
|
||||
rental: false,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
},
|
||||
},
|
||||
schedule: {
|
||||
date: null,
|
||||
|
|
@ -1009,6 +1015,9 @@ export const mutations = {
|
|||
state.order.policy.insuranceCompanyName =
|
||||
sessionInformation.order.policy?.insuranceCompanyName;
|
||||
|
||||
state.order.policy.morePolicyQuestions =
|
||||
sessionInformation.order.policy?.morePolicyQuestions;
|
||||
|
||||
state.order.policy.isNoComp =
|
||||
coverageTypeValue(sessionInformation?.order.insuranceCoverage.coverageType) ===
|
||||
coverageType.NOCOMP
|
||||
|
|
@ -2895,6 +2904,7 @@ export const actions = {
|
|||
currentDeductible: order.policy?.currentDeductible,
|
||||
originalDeductible: order.policy?.originalDeductible,
|
||||
policyNumber: order.policy?.policyNumber,
|
||||
morePolicyQuestions: order.policy?.morePolicyQuestions,
|
||||
},
|
||||
serviceLocation: {
|
||||
streetAddress: order.serviceLocation.address,
|
||||
|
|
|
|||
|
|
@ -795,6 +795,112 @@ describe("Actions", () => {
|
|||
expect(response.data).toEqual({ referralNumber: 123 });
|
||||
});
|
||||
|
||||
it("saveSession action, includes the selected morePolicyQuestions answers in the policy payload", async () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
|
||||
context.getters = {
|
||||
vehicle: {
|
||||
registration: {},
|
||||
},
|
||||
order: {
|
||||
damage: {
|
||||
numberOfChips: "2",
|
||||
partQuestionAnswers: {},
|
||||
moldingQuestionAnswers: {},
|
||||
capabilityQuestionAnswers: {},
|
||||
},
|
||||
},
|
||||
damage: {},
|
||||
applicationUser: {
|
||||
lastPageVisited: "test-page",
|
||||
crmCustomerId: "xxx-xxx-xxx",
|
||||
savedSessionId: "xxx-xxx-xxx",
|
||||
},
|
||||
};
|
||||
context.state = {
|
||||
order: {
|
||||
damage: {
|
||||
numberOfChips: "2",
|
||||
partQuestionAnswers: {},
|
||||
moldingQuestionAnswers: {},
|
||||
capabilityQuestionAnswers: {},
|
||||
},
|
||||
payment: {
|
||||
insuranceCoverage: {
|
||||
isVerified: false,
|
||||
},
|
||||
isInsurance: false,
|
||||
ccToken: {
|
||||
subscriptionId: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
cardType: null,
|
||||
billToPostalCode: null,
|
||||
billToFirstName: null,
|
||||
billToLastName: null,
|
||||
referenceNumber: null,
|
||||
authCode: null,
|
||||
transactionId: null,
|
||||
transReferenceNumber: null,
|
||||
lastFour: null,
|
||||
},
|
||||
},
|
||||
policy: {
|
||||
morePolicyQuestions: {
|
||||
additionalDamage: true,
|
||||
rental: true,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
},
|
||||
},
|
||||
serviceLocation: {},
|
||||
customer: {
|
||||
emailAddress: "test@safelite.com",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
isSmsOptIn: true,
|
||||
phoneNumber: "1234567890",
|
||||
address: {
|
||||
streetAddress: "123 Main St",
|
||||
streetAddress2: "Apt 1",
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "12345",
|
||||
},
|
||||
},
|
||||
lineItems: {},
|
||||
},
|
||||
};
|
||||
|
||||
globalMethods.callHttpClient.mockImplementation(() => {
|
||||
return Promise.resolve({ data: { referralNumber: 123 } });
|
||||
});
|
||||
|
||||
// Act
|
||||
await actions.saveSession(context, { pageNameToLog: "test" });
|
||||
|
||||
// Assert
|
||||
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
order: expect.objectContaining({
|
||||
policy: expect.objectContaining({
|
||||
morePolicyQuestions: {
|
||||
additionalDamage: true,
|
||||
rental: true,
|
||||
otherResponsibleParty: false,
|
||||
thirdPartyVehicle: false,
|
||||
injuries: false,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("loadSession action, returns order information, calls mutation", async () => {
|
||||
// Arrange
|
||||
const context = state;
|
||||
|
|
|
|||
Loading…
Reference in a new issue