diff --git a/src/constants/error-messages.js b/src/constants/error-messages.js
index 58f22ebe2..262979ed4 100644
--- a/src/constants/error-messages.js
+++ b/src/constants/error-messages.js
@@ -32,6 +32,7 @@ const errorMessages = {
DATE_REQUIRED: "Please select a date",
PHONE_REQUIRED: "Please enter your phone number",
PHONE_FORMAT: "Phone number must be 10 digits",
+ PHONE_EXTENSION_FORMAT: "The specified extension is invalid",
SMS_CONSENT_REQUIRED_1: "Please select checkbox to receive text messages",
SMS_CONSENT_REQUIRED_2: "Please select at least one consent option",
YEAR_REQUIRED: "Please select your vehicle year",
diff --git a/src/constants/experiments.js b/src/constants/experiments.js
index dcbaf1912..e2b15829a 100644
--- a/src/constants/experiments.js
+++ b/src/constants/experiments.js
@@ -61,6 +61,12 @@ const experimentSettings = {
// Consent Management
SHOW_CONSENT_MANAGEMENT: "ShowConsentManagement",
+
+ // YMM
+ BAILOUT_VIN_REQUIRED_VEHICLES: "BailoutVINRequiredVehicles",
+
+ // Promo Banner
+ SHOW_PROMO_BANNER: "ShowQuotePageRegionalPromoBanner",
};
const experimentTriggers = {
diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js
index cb4c6ff54..742e1c461 100644
--- a/src/constants/store-actions.js
+++ b/src/constants/store-actions.js
@@ -88,6 +88,7 @@ const storeActions = {
SAVE_EMAIL: "saveEmail",
SAVE_PHONE_NUMBER: "savePhoneNumber",
SAVE_IS_SMS_OPT_IN: "saveIsSmsOptIn",
+ SAVE_IS_SMS_MARKETING_OPT_IN: "saveIsSmsMarketingOptIn",
SAVE_WAITLIST_REQUESTED: "saveWaitListRequested",
SAVE_REGISTRATION_LICENSE_PLATE_LOOKUP: "saveRegistrationLicensePlateLookup",
SAVE_VIN: "saveVin",
diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js
index 71816b499..8437a395e 100644
--- a/src/constants/store-mutations.js
+++ b/src/constants/store-mutations.js
@@ -49,6 +49,7 @@ const storeMutations = {
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
UPDATE_CUSTOMER_PHONE_NUMBER: "updateCustomerPhoneNumber",
UPDATE_CUSTOMER_IS_SMS_OPT_IN: "updateCustomerIsSmsOptin",
+ UPDATE_CUSTOMER_IS_SMS_MARKETING_OPT_IN: "updateCustomerIsSmsMarketingOptin",
UPDATE_CUSTOMER_DETAILS: "updateCustomerDetails",
UPDATE_CUSTOMER_WAITLIST_REQUESTED: "updateCustomerWaitListRequested",
diff --git a/src/digital-components/question-chain/question-chain.spec.js b/src/digital-components/question-chain/question-chain.spec.js
index 7435feefd..d0e54a895 100644
--- a/src/digital-components/question-chain/question-chain.spec.js
+++ b/src/digital-components/question-chain/question-chain.spec.js
@@ -109,11 +109,11 @@ describe("Question Chain component", () => {
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
});
- test("should NOT emit update:modelValue if returnedAnswer contains 'nextQuestion'", () => {
+ test("should not emit update:modelValue for nextQuestion during normal forward progress", () => {
//Arrange
const { wrapper } = setupMocks({});
const testQuestion = {};
- const testReturnedAnswer = "1|nextQuestion|DB10840|No";
+ const testReturnedAnswer = "1|nextQuestion|3|No";
//Act
wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
@@ -121,6 +121,24 @@ describe("Question Chain component", () => {
//Assert
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
});
+
+ test("should emit update:modelValue for nextQuestion when revising a saved answer", async () => {
+ //Arrange
+ const { wrapper } = setupMocks({});
+ await wrapper.setProps({ hasSavedAnswer: true });
+ const testQuestion = {};
+ const testReturnedAnswer = "1|nextQuestion|3|No";
+
+ //Act
+ wrapper.vm.handleAnswer(testQuestion, testReturnedAnswer);
+
+ //Assert
+ expect(wrapper.emitted()).toHaveProperty("update:modelValue");
+ expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({
+ incomplete: true,
+ index: 0,
+ });
+ });
});
describe("method getQuestionChainAnswerIfComplete...", () => {
@@ -213,25 +231,103 @@ describe("Question Chain component", () => {
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
//Assert
- expect(wrapper.vm.questions[1].answerSelected).toBeUndefined;
+ expect(wrapper.vm.questions[1].answerSelected).toBeUndefined();
});
- test("should return false if returnedAnswer is a nextQuestion (not a final answer)", async () => {
+ test("should follow the No path and show the next question when changing Q1 from Yes to No", async () => {
+ const { wrapper } = setupMocks({
+ questionDataProp: [
+ {
+ questionSequence: 1,
+ questionText: "Is this the Overland edition?",
+ answers: [
+ {
+ answerResult: null,
+ answerText: "Yes",
+ nextQuestionSequence: 2,
+ },
+ {
+ answerResult: null,
+ answerText: "No",
+ nextQuestionSequence: 5,
+ },
+ ],
+ },
+ {
+ questionSequence: 2,
+ questionText: "Rain sensing wipers?",
+ answers: [
+ { answerResult: "PART-A", answerText: "Yes", nextQuestionSequence: 3 },
+ { answerResult: "PART-B", answerText: "No", nextQuestionSequence: 3 },
+ ],
+ answerSelected: "2|answer|PART-A|Yes",
+ },
+ {
+ questionSequence: 3,
+ questionText: "Laminated door glass?",
+ answers: [
+ {
+ answerResult: "PART-C",
+ answerText: "Yes",
+ nextQuestionSequence: null,
+ },
+ {
+ answerResult: "PART-D",
+ answerText: "No",
+ nextQuestionSequence: null,
+ },
+ ],
+ answerSelected: "3|answer|PART-C|Yes",
+ },
+ {
+ questionSequence: 5,
+ questionText: "Factory installed antenna?",
+ answers: [
+ {
+ answerResult: "PART-E",
+ answerText: "Yes",
+ nextQuestionSequence: null,
+ },
+ {
+ answerResult: "PART-F",
+ answerText: "No",
+ nextQuestionSequence: null,
+ },
+ ],
+ },
+ ],
+ });
+ await wrapper.setProps({ index: 0 });
+
+ const result = wrapper.vm.getQuestionChainAnswerIfComplete("1|nextQuestion|5|No");
+
+ expect(result.incomplete).toBe(true);
+ expect(wrapper.vm.questions[1].answerSelected).toBeUndefined();
+ expect(wrapper.vm.questions[2].answerSelected).toBeUndefined();
+ expect(wrapper.vm.currentQuestionNum).toBe(5);
+ expect(result.answeredQuestions).toHaveLength(1);
+ expect(result.answeredQuestions[0].selectedAnswerText).toBe("No");
+ });
+
+ test("should return incomplete answer if returnedAnswer is a nextQuestion (not a final answer)", async () => {
//Arrange
const { wrapper } = setupMocks({});
- const testReturnedAnswer = "1|nextQuestion|DB10840|No";
+ const testReturnedAnswer = "1|nextQuestion|3|No";
//Act
const result = wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
//Assert
- expect(result).toBeFalsy();
+ expect(result).toMatchObject({
+ incomplete: true,
+ index: 0,
+ });
});
test("should trigger scroll to .current-question if returnedAnswer is a nextQuestion (not a final answer)", async () => {
//Arrange
const { wrapper } = setupMocks({});
- const testReturnedAnswer = "1|nextQuestion|DB10840|No";
+ const testReturnedAnswer = "1|nextQuestion|3|No";
//Act
wrapper.vm.getQuestionChainAnswerIfComplete(testReturnedAnswer);
@@ -323,9 +419,206 @@ describe("Question Chain component", () => {
expect(wrapper.vm.questions[0].answers[0].problemQuestionId).toBe(9531);
expect(wrapper.vm.questions[0].answers[1].problemQuestionId).toBe(9529);
});
+
+ test("should normalize uppercase answerSelected to match button value on created", async () => {
+ const { wrapper } = setupMocks({
+ questionDataProp: [
+ {
+ questionSequence: 1,
+ questionText: "Is your vehicle equipped with rain sensing wipers?",
+ answerSelected: "1|ANSWER|DW01705|YES",
+ answers: [
+ {
+ answerResult: "DW01705",
+ answerText: "Yes",
+ nextQuestionSequence: null,
+ },
+ {
+ answerResult: "DW01591",
+ answerText: "No",
+ nextQuestionSequence: null,
+ },
+ ],
+ },
+ ],
+ });
+
+ await nextTick();
+
+ expect(wrapper.vm.questions[0].answerSelected).toBe("1|answer|DW01705|Yes");
+ });
+ test("should show question 3 after answering No on Cherokee Overland question", async () => {
+ const { wrapper } = setupMocks({
+ questionDataProp: getCherokeeWindshieldQuestions(),
+ });
+ await nextTick();
+
+ const q1 = wrapper.vm.questions[0];
+ wrapper.vm.handleAnswer(q1, "1|nextQuestion|3|No");
+
+ expect(wrapper.vm.currentQuestionNum).toBe(3);
+ expect(
+ wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question))
+ ).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ questionSequence: 1 }),
+ expect.objectContaining({ questionSequence: 3 }),
+ ])
+ );
+ expect(
+ wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question))
+ ).not.toEqual(
+ expect.arrayContaining([expect.objectContaining({ questionSequence: 2 })])
+ );
+ });
+
+ test("should resume at question 3 after remount when Q1 No was already saved", async () => {
+ const questionDataProp = getCherokeeWindshieldQuestions();
+ questionDataProp[0].answerSelected = "1|nextQuestion|3|No";
+
+ const { wrapper } = setupMocks({ questionDataProp });
+ await nextTick();
+
+ expect(wrapper.vm.currentQuestionNum).toBe(3);
+ expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true);
+ });
+
+ test("should resolve on Q1 Yes and not show Q2 for Honda HUD question", async () => {
+ const { wrapper } = setupMocks({
+ questionDataProp: getHondaAccordWindshieldQuestions(),
+ });
+ await nextTick();
+
+ const q1 = wrapper.vm.questions[0];
+ wrapper.vm.handleAnswer(q1, "1|answer|FW04796|Yes");
+
+ expect(wrapper.emitted()["update:modelValue"][0][0]).toMatchObject({
+ answerResult: "FW04796",
+ index: 0,
+ });
+ expect(wrapper.vm.currentQuestionNum).toBe(0);
+ expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(false);
+ });
+
+ test("should resolve on Q3 No and not continue after remount", async () => {
+ const questionDataProp = getHondaAccordWindshieldQuestions();
+ questionDataProp[0].answerSelected = "1|nextQuestion|2|No";
+ questionDataProp[1].answerSelected = "2|nextQuestion|3|No";
+ questionDataProp[2].answerSelected = "3|answer|FW04793|No";
+
+ const { wrapper } = setupMocks({ questionDataProp });
+ await nextTick();
+
+ expect(wrapper.vm.currentQuestionNum).toBe(0);
+ expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[2])).toBe(true);
+ expect(
+ wrapper.vm.questions.filter((question) => wrapper.vm.isQuestionVisible(question))
+ ).toHaveLength(3);
+ });
+
+ test("should show Q2 after answering No on Honda HUD question", async () => {
+ const { wrapper } = setupMocks({
+ questionDataProp: getHondaAccordWindshieldQuestions(),
+ });
+ await nextTick();
+
+ const q1 = wrapper.vm.questions[0];
+ wrapper.vm.handleAnswer(q1, "1|nextQuestion|2|No");
+
+ expect(wrapper.vm.currentQuestionNum).toBe(2);
+ expect(wrapper.vm.isQuestionVisible(wrapper.vm.questions[1])).toBe(true);
+ });
});
});
+function getHondaAccordWindshieldQuestions() {
+ return [
+ {
+ questionSequence: 1,
+ questionText:
+ "Is your vehicle equipped with a Heads-up Display which projects vehicle information, such as speed, onto the windshield?",
+ answers: [
+ { answerResult: "FW04796", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "", answerText: "No", nextQuestionSequence: 2 },
+ ],
+ },
+ {
+ questionSequence: 2,
+ questionText:
+ "Is your vehicle equipped with an auto-dimming rearview mirror which will darken automatically when a vehicles approaches from the rear at night?",
+ answers: [
+ { answerResult: "FW04795", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "", answerText: "No", nextQuestionSequence: 3 },
+ ],
+ },
+ {
+ questionSequence: 3,
+ questionText: "Is your vehicle equipped with a power moonroof?",
+ answers: [
+ { answerResult: "FW04794", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "FW04793", answerText: "No", nextQuestionSequence: null },
+ ],
+ },
+ ];
+}
+
+function getCherokeeWindshieldQuestions() {
+ return [
+ {
+ questionSequence: 1,
+ questionText:
+ "Is your Cherokee the Overland edition which can be identified by having a wood and leather wrapped steering wheel?",
+ answers: [
+ { answerResult: "", answerText: "Yes", nextQuestionSequence: 2 },
+ { answerResult: "", answerText: "No", nextQuestionSequence: 3 },
+ ],
+ },
+ {
+ questionSequence: 2,
+ questionText:
+ "Is your vehicle equipped with rain sensing wipers that adjust their speed automatically when it rains?",
+ answers: [
+ { answerResult: "DW02270", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "DW02264", answerText: "No", nextQuestionSequence: null },
+ ],
+ },
+ {
+ questionSequence: 3,
+ questionText:
+ "Is your vehicle equipped with rain sensing wipers that adjust their speed automatically when it rains?",
+ answers: [
+ { answerResult: "DW02268", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "", answerText: "No", nextQuestionSequence: 4 },
+ ],
+ },
+ {
+ questionSequence: 4,
+ questionText:
+ "Is your vehicle equipped with automatic climate control which will change the fan speed automatically in order to maintain a set temperature?",
+ answers: [
+ { answerResult: "", answerText: "Yes", nextQuestionSequence: 5 },
+ { answerResult: "", answerText: "No", nextQuestionSequence: 6 },
+ ],
+ },
+ {
+ questionSequence: 5,
+ questionText: "Is your vehicle equipped with heated seats?",
+ answers: [
+ { answerResult: "DW02104", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "DW02103", answerText: "No", nextQuestionSequence: null },
+ ],
+ },
+ {
+ questionSequence: 6,
+ questionText: "Is your vehicle equipped with heated seats?",
+ answers: [
+ { answerResult: "DW02102", answerText: "Yes", nextQuestionSequence: null },
+ { answerResult: "DW02101", answerText: "No", nextQuestionSequence: null },
+ ],
+ },
+ ];
+}
+
function setupMocks({
questionDataProp = [
{
@@ -351,6 +644,7 @@ function setupMocks({
const mountOptions = getMountOptions({});
mountOptions.propsData = {
questionData: questionDataProp,
+ index: 0,
};
mountOptions["attachTo"] = document.body;
diff --git a/src/digital-components/question-chain/question-chain.vue b/src/digital-components/question-chain/question-chain.vue
index b1960eb51..467e9619e 100644
--- a/src/digital-components/question-chain/question-chain.vue
+++ b/src/digital-components/question-chain/question-chain.vue
@@ -2,9 +2,9 @@
import buttonQuestion from "@/digital-components/button-question/button-question";
import { useValidateForm } from "vee-validate";
+import {
+ isTerminalQuestionChainAnswer,
+ mapQuestionAnswersForChain,
+ normalizeAnswerSelectedValue,
+} from "@/helpers/question-chain-helper";
export default {
name: "questionChain",
@@ -34,54 +39,86 @@ export default {
modelValue: Object,
index: Number,
answerKey: String,
+ hasSavedAnswer: Boolean,
+ hasDownstreamSavedAnswers: Boolean,
},
async created() {
// do a test validation check upon create to prevent out of sync / incorrect valid states
await useValidateForm(); // NOTE: needs to have async/await here; tested and won't work without it
this.questionData.map((q, i) => {
+ const answers = mapQuestionAnswersForChain(q);
const question = {
questionText: q.questionText,
questionSequence: q.questionSequence,
- answers: q.answers.map((a) => {
- return {
- buttonLabel: a.answerText,
- // Name will either be nextQuestionSequence or answerResult
- // Name will be used by list-button as the input value.
- // It must be a single string or number, so concatenating together a string with
- // 4 pieces of data separated by pipe characters:
- // question number|type of answer|answer value|answer text
- value: a.nextQuestionSequence
- ? q.questionSequence +
- "|nextQuestion|" +
- a.nextQuestionSequence +
- "|" +
- a.answerText
- : q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText,
- nextQuestionSequence: a.nextQuestionSequence,
- answerResult: a.answerResult,
- problemQuestionId: a.problemQuestionId,
- questionSequence: q.questionSequence,
- questionType: a.nextQuestionSequence ? "nextQuestion" : "answer",
- };
- }),
- answerSelected: q.answerSelected || "",
+ answers,
+ answerSelected: normalizeAnswerSelectedValue(q.answerSelected, answers),
};
if (!q.suppressThisQuestion) {
this.questions.push(question);
}
});
- if (!this.modelValue?.length > 0 && this.questions.length > 0) {
- // set this.currentQuestionNum to first valid question
- this.currentQuestionNum = this.questions[0].questionSequence;
- // scroll the next question into view
- this.$nextTick(() => {
- document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
- });
+ if (this.questions.length > 0) {
+ this.initializeCurrentQuestionNum();
+ this.scrollCurrentQuestionIntoView();
}
},
methods: {
+ getQuestionSequence(question) {
+ return Number(question?.questionSequence);
+ },
+ isCurrentQuestion(question) {
+ return this.getQuestionSequence(question) === Number(this.currentQuestionNum);
+ },
+ isQuestionVisible(question) {
+ return !!question.answerSelected || this.isCurrentQuestion(question);
+ },
+ initializeCurrentQuestionNum() {
+ const lastAnsweredQuestion = [...this.questions]
+ .reverse()
+ .find((question) => question.answerSelected);
+
+ if (isTerminalQuestionChainAnswer(lastAnsweredQuestion?.answerSelected)) {
+ this.currentQuestionNum = 0;
+ return;
+ }
+
+ if (lastAnsweredQuestion?.answerSelected?.includes("|nextQuestion|")) {
+ const nextQuestionSequence = Number(
+ lastAnsweredQuestion.answerSelected.split("|")[2]
+ );
+
+ if (
+ this.questions.some(
+ (question) => this.getQuestionSequence(question) === nextQuestionSequence
+ )
+ ) {
+ this.currentQuestionNum = nextQuestionSequence;
+ return;
+ }
+ }
+
+ const firstUnanswered = this.questions.find((question) => !question.answerSelected);
+
+ if (firstUnanswered) {
+ this.currentQuestionNum = this.getQuestionSequence(firstUnanswered);
+ }
+ },
+ scrollCurrentQuestionIntoView() {
+ this.$nextTick(() => {
+ document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
+ });
+ },
+ syncAnswerSelectedToQuestionData(questionSequence, answerSelected) {
+ const sourceQuestion = this.questionData?.find(
+ (q) => this.getQuestionSequence(q) === Number(questionSequence)
+ );
+
+ if (sourceQuestion) {
+ sourceQuestion.answerSelected = answerSelected;
+ }
+ },
getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) {
if (!question?.answers || !selectedAnswer) {
return null;
@@ -100,10 +137,20 @@ export default {
*/
question.answerSelected = returnedAnswer;
- const isQuestionChainComplete = this.getQuestionChainAnswerIfComplete(returnedAnswer);
+ this.syncAnswerSelectedToQuestionData(question.questionSequence, returnedAnswer);
+ const questionChainAnswer = this.getQuestionChainAnswerIfComplete(returnedAnswer);
- if (isQuestionChainComplete) {
- this.$emit("update:modelValue", isQuestionChainComplete);
+ if (!questionChainAnswer) {
+ return;
+ }
+
+ const shouldNotifyParent =
+ !questionChainAnswer.incomplete ||
+ this.hasSavedAnswer ||
+ this.hasDownstreamSavedAnswers;
+
+ if (shouldNotifyParent) {
+ this.$emit("update:modelValue", questionChainAnswer);
}
},
getQuestionChainAnswerIfComplete(returnedAnswer) {
@@ -117,20 +164,24 @@ export default {
// "5|answer|DW02104|Yes"
const returnedAnswerArray = returnedAnswer.split("|");
- const questionNum = parseInt(returnedAnswerArray[0]);
+ const questionNum = Number(returnedAnswerArray[0]);
const questionType = returnedAnswerArray[1];
const questionAnswer = returnedAnswerArray[2];
const answeredQuestions = [];
this.questions.forEach((q) => {
+ const questionSequence = this.getQuestionSequence(q);
+
// find this question and mark it as "answered" by populating answerSelected
- if (q.questionSequence === questionNum) {
+ if (questionSequence === questionNum) {
q.answerSelected = returnedAnswer;
+ this.syncAnswerSelectedToQuestionData(questionSequence, returnedAnswer);
}
// remove all answers AFTER this question...
// (needed in case user is changing previously answered questions)
- if (q.questionSequence > questionNum) {
+ if (questionSequence > questionNum) {
delete q.answerSelected;
+ this.syncAnswerSelectedToQuestionData(questionSequence, "");
}
if (q.answerSelected) {
answeredQuestions.push({
@@ -146,22 +197,21 @@ export default {
}
});
- // return false if there's a nextQuestion... or return an object with final answers (truthy)
+ // return incomplete answer if there's a nextQuestion... or return final answers (truthy)
if (questionType === "nextQuestion") {
// update to next question index
- this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question
- // scroll the next question into view
- this.$nextTick(() => {
- document
- .querySelector(".current-question")
- .scrollIntoView({ behavior: "smooth" });
- });
- return false;
+ this.currentQuestionNum = Number(questionAnswer);
+ this.scrollCurrentQuestionIntoView();
+ return {
+ incomplete: true,
+ answeredQuestions,
+ index: this.index,
+ };
} else {
// reset current question index (removes .current-question class)
this.currentQuestionNum = 0; // reset count
const answeredQuestion = this.questions.find(
- (q) => q.questionSequence === questionNum
+ (q) => this.getQuestionSequence(q) === questionNum
);
// return an object with the part answer, all the answered questions, and the part index
diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js
index 96e5434cd..db34bd262 100644
--- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js
+++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.spec.js
@@ -150,6 +150,43 @@ describe("questionsPageLayout.vue", () => {
wrapper.unmount();
});
+
+ test("Should return false when answerData was cleared even if answerSelected remains", async () => {
+ const { wrapper } = setupMocks({});
+ await wrapper.setProps({ index: 1 });
+
+ const testGlassPiece = {
+ questions: [
+ {
+ questionSequence: 1,
+ questionText: "Is your Grand Cherokee the Laredo model?",
+ answers: [],
+ answerSelected: "1|answer|DD11132|Yes",
+ },
+ ],
+ answerData: {},
+ };
+
+ expect(wrapper.vm.showThisQuestionChain(testGlassPiece, 0)).toBe(false);
+
+ wrapper.unmount();
+ });
+
+ test("Should return false for reset chains ahead of the current index", async () => {
+ const { wrapper } = setupMocks({});
+ await wrapper.setProps({ index: 1 });
+
+ const testGlassPiece = {
+ questions: [
+ { questionSequence: 1, questionText: "Rain sensing wipers?", answers: [] },
+ ],
+ answerData: null,
+ };
+
+ expect(wrapper.vm.showThisQuestionChain(testGlassPiece, 2)).toBe(false);
+
+ wrapper.unmount();
+ });
});
describe("method handleForwardButtonAction...", () => {
diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue
index c9f9989e2..3152f5b14 100644
--- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue
+++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue
@@ -10,7 +10,9 @@
-
+
-
-
+
+
glass.answerData?.answerResult);
+ },
showThisQuestionChain(glass, i) {
- // return false if no questions or if suppressed
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
return false;
}
- return this.index === i || glass.answerData?.answerResult?.length > 0;
+
+ if (this.index === i) {
+ return true;
+ }
+
+ // Only show completed chains that still have a saved answer
+ return !!glass.answerData?.answerResult;
},
handleForwardButtonAction() {
this.$emit("forwardButtonAction");
diff --git a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js
index 3db98487a..29d102d01 100644
--- a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js
+++ b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.spec.js
@@ -10,7 +10,7 @@ jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
jest.mock("@/helpers/save-progress-sms-consent/save-progress-sms-consent-helper", () => ({
...jest.requireActual("@/helpers/save-progress-sms-consent/save-progress-sms-consent-helper"),
- getSaveProgressSmsConsentConfig: jest.fn().mockResolvedValue({
+ getSaveProgressSmsConsentConfig: jest.fn().mockReturnValue({
copy: {
transactional: "Sign me up for updates about my upcoming service.",
marketing: "Sign me up for promotional and product offers.",
@@ -159,13 +159,37 @@ describe("save-progress-popup-question ", () => {
wrapper.vm.selectContactMethod("PhoneAnswer");
wrapper.vm.modal.validate = jest.fn().mockResolvedValue({ valid: true });
const resetSpy = jest.spyOn(wrapper.vm, "resetPhoneSendButtonStyle");
+ const scrollSpy = jest
+ .spyOn(wrapper.vm, "scrollConsentIntoView")
+ .mockResolvedValue(undefined);
await wrapper.vm.validatePhoneAndSave();
expect(wrapper.vm.showConsentErrors).toBe(true);
+ expect(scrollSpy).toHaveBeenCalled();
expect(resetSpy).toHaveBeenCalled();
});
+ test("should scroll consent checkboxes into view", async () => {
+ const { wrapper } = setupMocks({
+ props: {
+ modalWidgetName: "SaveProgressPopupWidget",
+ },
+ });
+ const mockScrollIntoView = jest.fn();
+ const consentEl = document.createElement("fieldset");
+ consentEl.className = "save-progress-popup-sms-consent";
+ consentEl.scrollIntoView = mockScrollIntoView;
+ wrapper.element.appendChild(consentEl);
+
+ await wrapper.vm.scrollConsentIntoView();
+
+ expect(mockScrollIntoView).toHaveBeenCalledWith({
+ behavior: "smooth",
+ block: "nearest",
+ });
+ });
+
test("should reset the send button loader when phone validation fails", async () => {
const { wrapper } = setupMocks({
props: {
diff --git a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue
index a226c0522..fc4e95530 100644
--- a/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue
+++ b/src/fmg-components/save-progress-popup-question/save-progress-popup-question.vue
@@ -43,6 +43,7 @@
isRequired
validationRules="phone-number-required" />
({
+ buttonLabel: answer.answerText,
+ value: buildQuestionChainAnswerValue({
+ questionSequence: question.questionSequence,
+ nextQuestionSequence: answer.nextQuestionSequence,
+ answerResult: answer.answerResult,
+ answerText: answer.answerText,
+ }),
+ nextQuestionSequence: answer.nextQuestionSequence,
+ answerResult: answer.answerResult,
+ problemQuestionId: answer.problemQuestionId,
+ questionSequence: question.questionSequence,
+ questionType: answer.nextQuestionSequence ? "nextQuestion" : "answer",
+ }));
+}
+
+export function isTerminalQuestionChainAnswer(answerSelected) {
+ if (!answerSelected) {
+ return false;
+ }
+
+ const [, questionType, answerResult] = answerSelected.split("|");
+
+ return questionType?.toLowerCase() === "answer" && !!answerResult;
+}
+
+export function normalizeAnswerSelectedValue(answerSelected, answers) {
+ if (!answerSelected) {
+ return "";
+ }
+
+ if (answers.some((answer) => answer.value === answerSelected)) {
+ return answerSelected;
+ }
+
+ const matchedAnswer = answers.find(
+ (answer) => answer.value.toUpperCase() === String(answerSelected).toUpperCase()
+ );
+
+ return matchedAnswer?.value ?? answerSelected;
+}
diff --git a/src/helpers/question-chain-helper.spec.js b/src/helpers/question-chain-helper.spec.js
new file mode 100644
index 000000000..f3a31acc1
--- /dev/null
+++ b/src/helpers/question-chain-helper.spec.js
@@ -0,0 +1,90 @@
+import {
+ buildQuestionChainAnswerValue,
+ isTerminalQuestionChainAnswer,
+ mapQuestionAnswersForChain,
+ normalizeAnswerSelectedValue,
+} from "./question-chain-helper";
+
+describe("question-chain-helper", () => {
+ describe("buildQuestionChainAnswerValue", () => {
+ test("returns nextQuestion format when nextQuestionSequence is set", () => {
+ expect(
+ buildQuestionChainAnswerValue({
+ questionSequence: 1,
+ nextQuestionSequence: 3,
+ answerResult: "ignored",
+ answerText: "Yes",
+ })
+ ).toBe("1|nextQuestion|3|Yes");
+ });
+
+ test("returns answer format for final answers", () => {
+ expect(
+ buildQuestionChainAnswerValue({
+ questionSequence: 1,
+ nextQuestionSequence: null,
+ answerResult: "DW01705",
+ answerText: "Yes",
+ })
+ ).toBe("1|answer|DW01705|Yes");
+ });
+ });
+
+ describe("isTerminalQuestionChainAnswer", () => {
+ test("returns true when answer has a part number and no next question", () => {
+ expect(isTerminalQuestionChainAnswer("1|answer|FW04796|Yes")).toBe(true);
+ expect(isTerminalQuestionChainAnswer("3|answer|FW04793|No")).toBe(true);
+ });
+
+ test("returns false when answer continues to the next question", () => {
+ expect(isTerminalQuestionChainAnswer("1|nextQuestion|2|No")).toBe(false);
+ });
+
+ test("returns false when answer type is answer but part number is missing", () => {
+ expect(isTerminalQuestionChainAnswer("1|answer||No")).toBe(false);
+ });
+ });
+
+ describe("normalizeAnswerSelectedValue", () => {
+ const answers = [{ value: "1|answer|DW01705|Yes" }, { value: "1|answer|DW01591|No" }];
+
+ test("returns empty string when answerSelected is empty", () => {
+ expect(normalizeAnswerSelectedValue("", answers)).toBe("");
+ });
+
+ test("returns exact match when casing already matches", () => {
+ expect(normalizeAnswerSelectedValue("1|answer|DW01705|Yes", answers)).toBe(
+ "1|answer|DW01705|Yes"
+ );
+ });
+
+ test("normalizes uppercase legacy values to button value casing", () => {
+ expect(normalizeAnswerSelectedValue("1|ANSWER|DW01705|YES", answers)).toBe(
+ "1|answer|DW01705|Yes"
+ );
+ });
+ });
+
+ describe("mapQuestionAnswersForChain", () => {
+ test("maps API answers to question-chain button answers", () => {
+ const result = mapQuestionAnswersForChain({
+ questionSequence: 1,
+ answers: [
+ {
+ answerResult: "DW01705",
+ answerText: "Yes",
+ nextQuestionSequence: null,
+ problemQuestionId: 123,
+ },
+ ],
+ });
+
+ expect(result[0]).toMatchObject({
+ buttonLabel: "Yes",
+ value: "1|answer|DW01705|Yes",
+ problemQuestionId: 123,
+ questionType: "answer",
+ });
+ });
+ });
+});
diff --git a/src/helpers/save-progress-popup-contact-helper.js b/src/helpers/save-progress-popup-contact-helper.js
index d19d1611f..0168627fc 100644
--- a/src/helpers/save-progress-popup-contact-helper.js
+++ b/src/helpers/save-progress-popup-contact-helper.js
@@ -1,5 +1,4 @@
import { storeActions } from "@/constants/store-actions";
-import store from "@/store";
export const saveProgressPopupContactMethods = {
PHONE: "PhoneAnswer",
@@ -20,7 +19,7 @@ export function normalizePhoneNumberForStore(phoneNumber) {
export async function saveProgressPopupContactToStore(
dispatchStoreAction,
- { contactMethod, userInput, smsConsent, pageName }
+ { contactMethod, userInput, smsConsent }
) {
if (isPhoneContactMethod(contactMethod)) {
await dispatchStoreAction(
@@ -29,17 +28,9 @@ export async function saveProgressPopupContactToStore(
false
);
await dispatchStoreAction(storeActions.SAVE_IS_SMS_OPT_IN, smsConsent.transactional, false);
-
- const existingPageData = store.getters.pageData(pageName) ?? {};
await dispatchStoreAction(
- storeActions.SAVE_PAGE_DATA,
- {
- page: pageName,
- data: {
- ...existingPageData,
- saveProgressSmsConsent: { ...smsConsent },
- },
- },
+ storeActions.SAVE_IS_SMS_MARKETING_OPT_IN,
+ smsConsent.marketing,
false
);
diff --git a/src/helpers/save-progress-popup-contact-helper.spec.js b/src/helpers/save-progress-popup-contact-helper.spec.js
index a2a252c01..ee63067c4 100644
--- a/src/helpers/save-progress-popup-contact-helper.spec.js
+++ b/src/helpers/save-progress-popup-contact-helper.spec.js
@@ -1,5 +1,4 @@
import { storeActions } from "@/constants/store-actions";
-import store from "@/store";
import {
isPhoneContactMethod,
normalizePhoneNumberForStore,
@@ -7,16 +6,9 @@ import {
saveProgressPopupContactToStore,
} from "./save-progress-popup-contact-helper";
-jest.mock("@/store", () => ({
- getters: {
- pageData: jest.fn(),
- },
-}));
-
describe("save-progress-popup-contact-helper", () => {
beforeEach(() => {
jest.clearAllMocks();
- store.getters.pageData.mockReturnValue({ servicePackageSelected: "premium" });
});
describe("isPhoneContactMethod", () => {
@@ -41,7 +33,7 @@ describe("save-progress-popup-contact-helper", () => {
});
describe("saveProgressPopupContactToStore", () => {
- it("should save phone number and persist sms consent on phone tab", async () => {
+ it("should save phone number and sms consent on customer when phone tab is selected", async () => {
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
const smsConsent = { transactional: true, marketing: false };
@@ -49,7 +41,6 @@ describe("save-progress-popup-contact-helper", () => {
contactMethod: saveProgressPopupContactMethods.PHONE,
userInput: "555-123-4567",
smsConsent,
- pageName: "quote",
});
expect(dispatchStoreAction).toHaveBeenCalledWith(
@@ -68,14 +59,13 @@ describe("save-progress-popup-contact-helper", () => {
false
);
expect(dispatchStoreAction).toHaveBeenCalledWith(
+ storeActions.SAVE_IS_SMS_MARKETING_OPT_IN,
+ false,
+ false
+ );
+ expect(dispatchStoreAction).not.toHaveBeenCalledWith(
storeActions.SAVE_PAGE_DATA,
- {
- page: "quote",
- data: {
- servicePackageSelected: "premium",
- saveProgressSmsConsent: smsConsent,
- },
- },
+ expect.anything(),
false
);
});
@@ -87,7 +77,6 @@ describe("save-progress-popup-contact-helper", () => {
contactMethod: saveProgressPopupContactMethods.EMAIL,
userInput: "test@example.com",
smsConsent: { transactional: false, marketing: false },
- pageName: "quote",
});
expect(dispatchStoreAction).toHaveBeenCalledWith(
@@ -105,6 +94,11 @@ describe("save-progress-popup-contact-helper", () => {
expect.anything(),
false
);
+ expect(dispatchStoreAction).not.toHaveBeenCalledWith(
+ storeActions.SAVE_IS_SMS_MARKETING_OPT_IN,
+ expect.anything(),
+ false
+ );
expect(dispatchStoreAction).not.toHaveBeenCalledWith(
storeActions.SAVE_PAGE_DATA,
expect.anything(),
diff --git a/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.js b/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.js
index 2269c86ff..988973471 100644
--- a/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.js
+++ b/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.js
@@ -1,3 +1,5 @@
+import store from "@/store";
+
const SAVE_PROGRESS_SMS_CONSENT_FALLBACK_COPY = {
transactional: "Sign me up for updates about my upcoming service.",
marketing: "Sign me up for promotional and product offers.",
@@ -18,10 +20,12 @@ export function hasSaveProgressSmsConsentSelection(consent, showConsentManagemen
return Boolean(consent?.transactional || (showConsentManagement && consent?.marketing));
}
-export async function getSaveProgressSmsConsentConfig() {
- // Replace with Consent Management API lookup (CASH-1911).
+export function getSaveProgressSmsConsentConfig() {
return {
copy: getSaveProgressSmsConsentFallbackCopy(),
- value: defaultSaveProgressSmsConsent(),
+ value: {
+ transactional: Boolean(store.getters.order.customer.isSmsOptIn),
+ marketing: Boolean(store.getters.order.customer.isSmsMarketingOptIn),
+ },
};
}
diff --git a/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.spec.js b/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.spec.js
index bcc9ae617..34dc68420 100644
--- a/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.spec.js
+++ b/src/helpers/save-progress-sms-consent/save-progress-sms-consent-helper.spec.js
@@ -1,3 +1,5 @@
+import { storeActions } from "@/constants/store-actions";
+import store from "@/store";
import {
defaultSaveProgressSmsConsent,
getSaveProgressSmsConsentFallbackCopy,
@@ -5,12 +7,38 @@ import {
hasSaveProgressSmsConsentSelection,
} from "./save-progress-sms-consent-helper";
+jest.mock("@/store", () => ({
+ getters: {
+ order: {
+ customer: {
+ isSmsOptIn: null,
+ isSmsMarketingOptIn: null,
+ },
+ },
+ },
+}));
+
describe("save-progress-sms-consent-helper", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ store.getters.order.customer.isSmsOptIn = null;
+ store.getters.order.customer.isSmsMarketingOptIn = null;
+ });
+
describe("getSaveProgressSmsConsentConfig", () => {
- it("should return fallback copy and default consent values", async () => {
- const config = await getSaveProgressSmsConsentConfig();
+ it("should return fallback copy and consent values from the store", () => {
+ store.getters.order.customer.isSmsOptIn = true;
+ store.getters.order.customer.isSmsMarketingOptIn = true;
+
+ const config = getSaveProgressSmsConsentConfig();
expect(config.copy).toEqual(getSaveProgressSmsConsentFallbackCopy());
+ expect(config.value).toEqual({ transactional: true, marketing: true });
+ });
+
+ it("should return defaults when no consent is stored", () => {
+ const config = getSaveProgressSmsConsentConfig();
+
expect(config.value).toEqual(defaultSaveProgressSmsConsent());
});
});
diff --git a/src/layouts/capability-questions/capability-questions.spec.js b/src/layouts/capability-questions/capability-questions.spec.js
index fb4515f50..271a2e34c 100644
--- a/src/layouts/capability-questions/capability-questions.spec.js
+++ b/src/layouts/capability-questions/capability-questions.spec.js
@@ -213,7 +213,7 @@ describe("capabilityQuestions.vue", () => {
});
describe("forwardButtonAction", () => {
- test("Should clear out answerData", () => {
+ test("Should clear out answerData", async () => {
// Arrange
const { wrapper } = setupMocks({});
@@ -245,7 +245,7 @@ describe("capabilityQuestions.vue", () => {
});
// Act
- wrapper.vm.forwardButtonAction();
+ await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
diff --git a/src/layouts/capability-questions/capability-questions.vue b/src/layouts/capability-questions/capability-questions.vue
index c4f5df92c..6b9f5aaa1 100644
--- a/src/layouts/capability-questions/capability-questions.vue
+++ b/src/layouts/capability-questions/capability-questions.vue
@@ -180,11 +180,6 @@ export default {
};
});
- // clear out answerData for future page loads; must occur prior to store save
- this.questionsData.forEach((glass) => {
- glass.answerData = {};
- });
-
// save to vuex store as order.damage.capabilityQuestionAnswers (array)
// used in GET_PART_FROM_CAPABILITY_QUESTION_ANSWER call following this one
await this.dispatchStoreAction(
@@ -211,6 +206,8 @@ export default {
}
this.navigateForward(partsOrQuestions);
+
+ this.clearQuestionsAnswerData();
},
},
components: {
diff --git a/src/layouts/molding-questions/molding-questions.spec.js b/src/layouts/molding-questions/molding-questions.spec.js
index 8daa6e780..bb668b62c 100644
--- a/src/layouts/molding-questions/molding-questions.spec.js
+++ b/src/layouts/molding-questions/molding-questions.spec.js
@@ -217,7 +217,7 @@ describe("moldingQuestions.vue", () => {
});
describe("forwardButtonAction", () => {
- test("Should clear out answerData", () => {
+ test("Should clear out answerData", async () => {
// Arrange
const { wrapper } = setupMocks({});
@@ -240,7 +240,7 @@ describe("moldingQuestions.vue", () => {
});
// Act
- wrapper.vm.forwardButtonAction();
+ await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
diff --git a/src/layouts/molding-questions/molding-questions.vue b/src/layouts/molding-questions/molding-questions.vue
index a7f4900de..1e1cf1cb4 100644
--- a/src/layouts/molding-questions/molding-questions.vue
+++ b/src/layouts/molding-questions/molding-questions.vue
@@ -181,11 +181,6 @@ export default {
};
});
- // clear out answerData for future page loads; must occur prior to store save
- this.questionsData.forEach((glass) => {
- glass.answerData = {};
- });
-
// save to vuex store as order.damage.moldingQuestionArrays (array)
await this.dispatchStoreAction(
this.storeActions.SAVE_MOLDING_QUESTION_ANSWERS,
@@ -209,6 +204,8 @@ export default {
}
this.navigateForward(partsOrQuestions);
+
+ this.clearQuestionsAnswerData();
},
},
components: {
diff --git a/src/layouts/part-questions/part-questions.spec.js b/src/layouts/part-questions/part-questions.spec.js
index 4e7cb53dd..527f5dd74 100644
--- a/src/layouts/part-questions/part-questions.spec.js
+++ b/src/layouts/part-questions/part-questions.spec.js
@@ -258,9 +258,7 @@ describe("partQuestions.vue...", () => {
});
// Act
- wrapper.vm.forwardButtonAction();
-
- await nextTick();
+ await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.questionsData[0].answerData).toEqual({});
diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue
index 1c342543f..f6c3c7667 100644
--- a/src/layouts/part-questions/part-questions.vue
+++ b/src/layouts/part-questions/part-questions.vue
@@ -182,13 +182,6 @@ export default {
};
});
- // clear out answerData for future page loads; must occur prior to store save
- this.questionsData.forEach((glass) => {
- if (glass.answerData) {
- glass.answerData = {};
- }
- });
-
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// save to vuex store as order.damage.partQuestionAnswers (array)
@@ -209,6 +202,8 @@ export default {
const glassPartsForStore = partsLookup.data.glassPieceParts;
this.navigateForward(glassPartsForStore);
+
+ this.clearQuestionsAnswerData();
},
},
components: {
diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue
index fc3ad668d..213d21cb1 100644
--- a/src/layouts/payment-adyen/payment-adyen.vue
+++ b/src/layouts/payment-adyen/payment-adyen.vue
@@ -24,7 +24,7 @@
+ inputId="phoneExtension"
+ maxLength="5"
+ validationRules="phone-extension-format" />
@@ -321,6 +323,7 @@ defineRule("policy-number-format", (value) => {
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
+defineRule("phone-extension-format", regex(/^\d{1,5}$/, errorMessages.PHONE_EXTENSION_FORMAT));
defineRule("date-of-loss-required", required(errorMessages.DATE_OF_LOSS_REQUIRED));
defineRule("damage-types-required", required(errorMessages.DAMAGE_TYPES_REQUIRED));
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
diff --git a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue
index 98c6844b8..764d5c9c0 100644
--- a/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue
+++ b/src/layouts/quote/afterpay-modal-banner/afterpay-modal-banner.vue
@@ -1,5 +1,9 @@
-
-
- {{ modalSubHeaderText }}
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -197,119 +136,10 @@ export default {
}
@include media-breakpoint-up(md) {
padding-left: 1rem;
- max-width: 36rem;
}
- }
-}
-.afterpay-modal {
- &.modal.modal-component {
- :deep(.modal-dialog) {
- margin: 0 1rem;
- top: 0;
-
- @include media-breakpoint-up(md) {
- width: 100%;
- max-width: 840px;
- top: 3.4rem;
- right: 0;
- left: 0;
- transform: none;
- margin: 0 auto;
- padding: 0 1rem;
- }
- .modal-content {
- border-radius: 0.5rem;
- margin: 1.5rem 0;
-
- .modal-header.mt-6 {
- background-color: $blue-100;
- margin-top: 0;
- padding: 1.5rem 2rem 1.5rem 1.5rem;
- font-family: UrbanistSemibold;
-
- .modal-title.justify-content-center {
- // OVERRIDE
- justify-content: flex-start;
- }
- .btn-close {
- top: 1.25rem;
- right: 1.5rem;
- }
- }
- .afterpay-section {
- display: block;
- border: 1px solid $gray-200;
- border-radius: 0.25rem;
- padding: 1rem;
-
- .section-header {
- display: flex;
- align-items: center;
- margin-bottom: 1rem;
-
- img {
- width: 40px;
- height: 40px;
- }
- h3 {
- font-family: UrbanistBold;
- font-size: 1rem;
- margin: 0 0 0 0.5rem;
- color: $black;
- }
- }
- p {
- font-size: 0.75rem;
- }
- strong {
- font-family: UrbanistSemibold;
- font-size: 0.875rem;
- }
- }
-
- .modal-body {
- padding: 1.5rem;
-
- .afterpay-sections {
- @include media-breakpoint-up(md) {
- display: flex;
- column-gap: 1rem;
- align-items: stretch;
- }
- .afterpay-section {
- margin-bottom: 1rem;
-
- @include media-breakpoint-up(md) {
- flex: 0 1 33%;
- margin-bottom: 0;
- }
-
- &:last-of-type {
- margin-bottom: 0;
- }
- }
- }
- }
- }
- &.modal-dialog-centered {
- height: auto;
- min-height: auto;
- }
- }
- }
-
- :deep(.modal-footer) {
- position: relative;
- background: $gray-100;
-
- .modal-disclaimer {
- font-style: italic;
- font-size: 0.75rem;
- a {
- padding: 0;
- line-height: 1.625;
- }
+ img {
+ transform: translateY(-0.1rem);
}
}
}
diff --git a/src/layouts/quote/promo-banner/promo-banner.vue b/src/layouts/quote/promo-banner/promo-banner.vue
new file mode 100644
index 000000000..f26a42593
--- /dev/null
+++ b/src/layouts/quote/promo-banner/promo-banner.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue
index 50512bcbf..c21b9164c 100644
--- a/src/layouts/quote/quote.vue
+++ b/src/layouts/quote/quote.vue
@@ -5,6 +5,14 @@
+
@@ -47,8 +55,7 @@
+ cmsWidgetName="AfterpayBannerWidget" />
+ class="quote-disclaimer text-left text-md-center" />
+
@@ -134,6 +146,7 @@ import textBlock from "@/digital-components/text-block/text-block";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner";
+import promoBanner from "@/layouts/quote/promo-banner/promo-banner";
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
@@ -684,6 +697,12 @@ export default {
showAfterpayBanner() {
return !this.isRecalibrationOnOrder || !this.shouldHideRecalibration;
},
+ showPromoBanner() {
+ return experimentMixin.methods.hasSettingEqualTo(
+ experimentSettings.SHOW_PROMO_BANNER,
+ "true"
+ );
+ },
isRecalPriceRemove() {
return (
experimentMixin.methods
@@ -1041,6 +1060,7 @@ export default {
contentGroupModal,
loadingModal,
afterpayModalBanner,
+ promoBanner,
promoModalQuestion,
recalDisclaimer,
saveProgressModalQuestion,
@@ -1166,4 +1186,7 @@ export default {
:deep(.promo-modal-question a) {
@include responsive-font-size-md(0.875rem, 1rem);
}
+.quote-disclaimer:last-child {
+ margin-bottom: 1.5rem;
+}
diff --git a/src/layouts/scheduling/scheduling.spec.js b/src/layouts/scheduling/scheduling.spec.js
index d64ffa779..c76a0985f 100644
--- a/src/layouts/scheduling/scheduling.spec.js
+++ b/src/layouts/scheduling/scheduling.spec.js
@@ -15,6 +15,9 @@ jest.mock("@/store", () => ({
lineItems: { glassParts: [] },
policy: { isItac: false, isNoComp: false },
},
+ applicationUser: {
+ experiments: [],
+ },
},
}));
diff --git a/src/layouts/scheduling/scheduling.vue b/src/layouts/scheduling/scheduling.vue
index 3ace9edf7..2b2a8e803 100644
--- a/src/layouts/scheduling/scheduling.vue
+++ b/src/layouts/scheduling/scheduling.vue
@@ -48,6 +48,10 @@
@address-clicked="onInshopAddressClicked(provider)" />
+
diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js
new file mode 100644
index 000000000..83582df3a
--- /dev/null
+++ b/src/layouts/scheduling/waitlist-question/waitlist-question.spec.js
@@ -0,0 +1,185 @@
+import { mount } from "@vue/test-utils";
+import waitlistQuestion from "./waitlist-question";
+import store from "@/store";
+import { experimentSettings } from "@/constants/experiments";
+
+jest.mock("@/store", () => ({
+ getters: {
+ applicationUser: {
+ experiments: [],
+ },
+ },
+}));
+
+const MOCK_CMS_CONTENT = {
+ WaitListLabelWidget: { Text: "Want to be notified sooner?" },
+ WaitListQuestionWidget: { QuestionText: "Add me to the waitlist" },
+};
+
+function dateStringOffsetFromToday(offsetDays) {
+ const d = new Date();
+ d.setDate(d.getDate() + offsetDays);
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(
+ d.getDate()
+ ).padStart(2, "0")}`;
+}
+
+function enableWaitlistExperiment(thresholdDays = 0) {
+ store.getters.applicationUser.experiments = [
+ {
+ isActive: true,
+ settings: {
+ [experimentSettings.DISPLAY_WAITLIST]: "true",
+ [experimentSettings.WAITLIST_THRESHOLD_DAYS]: String(thresholdDays),
+ },
+ },
+ ];
+}
+
+function mountComponent(props = {}) {
+ const cmsMixin = {
+ methods: {
+ getCmsContent: jest.fn((widgetName, fieldName) => {
+ return MOCK_CMS_CONTENT[widgetName]?.[fieldName] ?? "";
+ }),
+ },
+ };
+
+ return mount(waitlistQuestion, {
+ props: {
+ modelValue: false,
+ availableDates: [dateStringOffsetFromToday(10)],
+ ...props,
+ },
+ global: {
+ mixins: [cmsMixin],
+ },
+ });
+}
+
+describe("waitlist-question.vue", () => {
+ beforeEach(() => {
+ enableWaitlistExperiment();
+ });
+
+ afterEach(() => {
+ store.getters.applicationUser.experiments = [];
+ });
+
+ it("renders the label and checkbox CMS content", () => {
+ const wrapper = mountComponent();
+
+ expect(wrapper.text()).toContain("Want to be notified sooner?");
+ expect(wrapper.text()).toContain("Add me to the waitlist");
+ });
+
+ it("reflects the modelValue prop on the checkbox", () => {
+ const wrapper = mountComponent({ modelValue: true });
+
+ expect(wrapper.find("input[type='checkbox']").element.checked).toBe(true);
+ });
+
+ it("emits update:modelValue with true when the checkbox is checked", async () => {
+ const wrapper = mountComponent({ modelValue: false });
+
+ const input = wrapper.find("input[type='checkbox']");
+ await input.setValue(true);
+
+ expect(wrapper.emitted("update:modelValue")).toEqual([[true]]);
+ });
+
+ it("emits update:modelValue with false when the checkbox is unchecked", async () => {
+ const wrapper = mountComponent({ modelValue: true });
+
+ const input = wrapper.find("input[type='checkbox']");
+ await input.setValue(false);
+
+ expect(wrapper.emitted("update:modelValue")).toEqual([[false]]);
+ });
+
+ it("renders nothing when shouldDisplay is false", () => {
+ store.getters.applicationUser.experiments = [];
+
+ const wrapper = mountComponent();
+
+ expect(wrapper.find("input[type='checkbox']").exists()).toBe(false);
+ });
+
+ describe("clicking the container", () => {
+ it("toggles localValue to true when clicking outside the checkbox", async () => {
+ const wrapper = mountComponent({ modelValue: false });
+
+ await wrapper.find(".waitlist-label").trigger("click");
+
+ expect(wrapper.emitted("update:modelValue")).toEqual([[true]]);
+ });
+
+ it("toggles localValue to false when clicking outside the checkbox", async () => {
+ const wrapper = mountComponent({ modelValue: true });
+
+ await wrapper.find(".waitlist-question").trigger("click");
+
+ expect(wrapper.emitted("update:modelValue")).toEqual([[false]]);
+ });
+
+ it("does not toggle when the click target is the checkbox input itself", () => {
+ // jsdom doesn't reliably run a checkbox's native activation behavior
+ // (toggling + firing "change") for script-dispatched clicks, so this
+ // calls the handler directly with the real input element as the
+ // event target to verify the guard is skipped in that case.
+ const wrapper = mountComponent({ modelValue: false });
+ const inputElement = wrapper.find("input[type='checkbox']").element;
+
+ wrapper.vm.handleContainerClick({ target: inputElement });
+
+ expect(wrapper.emitted("update:modelValue")).toBeUndefined();
+ });
+
+ it("does not toggle when the click lands inside the checkbox wrapper but not on the input", async () => {
+ const wrapper = mountComponent({ modelValue: false });
+
+ await wrapper.find(".ui-checkbox").trigger("click");
+
+ expect(wrapper.emitted("update:modelValue")).toBeUndefined();
+ });
+ });
+
+ describe("shouldDisplay", () => {
+ it("is false when the DISPLAY_WAITLIST experiment is off", () => {
+ store.getters.applicationUser.experiments = [
+ {
+ isActive: true,
+ settings: { [experimentSettings.WAITLIST_THRESHOLD_DAYS]: "3" },
+ },
+ ];
+
+ const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(10)] });
+
+ expect(wrapper.vm.shouldDisplay).toBe(false);
+ });
+
+ it("is false when the earliest available date is within the threshold", () => {
+ enableWaitlistExperiment(3);
+
+ const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(2)] });
+
+ expect(wrapper.vm.shouldDisplay).toBe(false);
+ });
+
+ it("is true when the experiment is on and the earliest available date exceeds the threshold", () => {
+ enableWaitlistExperiment(3);
+
+ const wrapper = mountComponent({ availableDates: [dateStringOffsetFromToday(10)] });
+
+ expect(wrapper.vm.shouldDisplay).toBe(true);
+ });
+
+ it("is false when there are no available dates", () => {
+ enableWaitlistExperiment(0);
+
+ const wrapper = mountComponent({ availableDates: [] });
+
+ expect(wrapper.vm.shouldDisplay).toBe(false);
+ });
+ });
+});
diff --git a/src/layouts/scheduling/waitlist-question/waitlist-question.vue b/src/layouts/scheduling/waitlist-question/waitlist-question.vue
new file mode 100644
index 000000000..c6a81c8b0
--- /dev/null
+++ b/src/layouts/scheduling/waitlist-question/waitlist-question.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js
index 39b1108f2..439becc60 100644
--- a/src/layouts/vin-lookup/vin-lookup.spec.js
+++ b/src/layouts/vin-lookup/vin-lookup.spec.js
@@ -168,6 +168,23 @@ describe("vin-lookup.vue", () => {
expect(wrapper.vm.navigateForward).not.toHaveBeenCalled();
});
+ it("Should keep the continue button loader spinning for vinRequired vehicles when VIN lookup fails", async () => {
+ // Arrange
+ store.getters.vehicle.vinRequired = true;
+ const { wrapper } = setupMocks({});
+ mockOutPromises({ vehicleLookupFailed: true });
+ wrapper.vm.continueWithFailedVin = jest.fn().mockResolvedValue();
+ wrapper.setData({ vin: "1HGCM82633A123456", vinPopulatedOnPageLoad: false });
+
+ // Act
+ await wrapper.vm.forwardButtonAction();
+
+ // Assert
+ expect(wrapper.vm.continueWithFailedVin).toHaveBeenCalled();
+ expect(wrapper.vm.$refs.navbar.removeLoader).not.toHaveBeenCalled();
+ store.getters.vehicle.vinRequired = false;
+ });
+
describe("navigateForward", () => {
test("carId is different from returned vehicle and selected glass isn't available => continue with different glass", async () => {
// Arrange
@@ -394,11 +411,14 @@ function setupMocks({ customMountOptions }) {
return { wrapper };
}
-function mockOutPromises({ carId, isZipValid = true, isZipServiceable = true }) {
+function mockOutPromises({
+ carId,
+ isZipValid = true,
+ isZipServiceable = true,
+ vehicleLookupFailed = false,
+}) {
const apiResponses = {
- vehicleLookupResponse: {
- carId: carId,
- },
+ vehicleLookupResponse: vehicleLookupFailed ? undefined : { carId: carId },
zipCodeData: {
isValid: isZipValid,
isServiceable: isZipServiceable,
diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue
index 35e2a626e..a8a700de0 100644
--- a/src/layouts/vin-lookup/vin-lookup.vue
+++ b/src/layouts/vin-lookup/vin-lookup.vue
@@ -295,7 +295,14 @@ export default {
const isVinLookupRequired = this.$store.getters.vehicle.vinRequired;
if (isVinLookupRequired) {
this.requiredVinNotFound = true;
- this.continueWithFailedVin(this.vin, resultMap.zipCodeData);
+
+ // Check if Service Zip entered is serviceable, if not display an alert
+ if (!resultMap.zipCodeData.isServiceable) {
+ this.displayNonServiceableZipAlert = true;
+ }
+
+ await this.continueWithFailedVin(this.vin, resultMap.zipCodeData);
+ return;
} else {
this.displayVinNotFoundAlert = true;
}
@@ -521,7 +528,7 @@ export default {
},
false
);
- this.navigateForwardWithSingleCarMatch();
+ await this.navigateForwardWithSingleCarMatch();
},
getRequiredVinNotFound() {
// Prevents success alert from showing
diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js
index 1636f0c47..265d54fa2 100644
--- a/src/mixins/vehicle-questions-mixin.js
+++ b/src/mixins/vehicle-questions-mixin.js
@@ -7,9 +7,17 @@ import baseMixin from "@/mixins/base-mixin.js";
import store from "@/store";
import { getIsWindshieldOnly } from "@/helpers/damage-helper";
import { partTypeStrings } from "@/constants/part-type-strings";
+import { buildQuestionChainAnswerValue } from "@/helpers/question-chain-helper";
export default {
methods: {
+ clearQuestionsAnswerData() {
+ this.questionsData?.forEach((glass) => {
+ if (glass.answerData) {
+ glass.answerData = {};
+ }
+ });
+ },
hasPartQuestions(partsOrQuestions) {
return partsOrQuestions?.some((pq) => pq.partQuestions?.length > 0);
},
@@ -114,11 +122,12 @@ export default {
);
});
// set the answerString to use for answerSelected
- if (chosenAns.nextQuestionSequence) {
- answerString = `${answeredQuestion.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`;
- } else {
- answerString = `${answeredQuestion.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`;
- }
+ answerString = buildQuestionChainAnswerValue({
+ questionSequence: answeredQuestion.questionNum,
+ nextQuestionSequence: chosenAns.nextQuestionSequence,
+ answerResult: chosenAns.answerResult,
+ answerText: chosenAns.answerText,
+ });
// mark this question as answered (question-chain will read this)
glass.questions[answeredQuestion.questionNum - 1].answerSelected =
@@ -177,6 +186,8 @@ export default {
*/
const self = vm ?? this;
+ const isIncomplete = !!answer.incomplete;
+ const hadSavedAnswer = !!self.questionsData[answer.index]?.answerData?.answerResult;
// clear out any preloaded answers
self.selectedAnswers = {};
@@ -193,7 +204,6 @@ export default {
*/
const answeredQuestionText = answeredQuestion.questionText.toUpperCase();
- const answeredQuestionAnswer = answeredQuestion.selectedAnswer.toUpperCase();
const answeredQuestionAnswerText =
answeredQuestion.selectedAnswerText.toUpperCase();
const answeredQuestionNum = answeredQuestion.questionNum;
@@ -208,8 +218,7 @@ export default {
if (answeredQuestionIndex === 0) question.answerSelected = null;
if (answeredQuestionNum - 1 === questionIndex) {
- // on the right question
- question.answerSelected = answeredQuestionAnswer;
+ question.answerSelected = answeredQuestion.selectedAnswer;
}
});
}
@@ -346,6 +355,27 @@ export default {
});
});
+ if (isIncomplete) {
+ // User changed an earlier answer; downstream questions were cleared in the chain
+ self.questionsData[answer.index].answerData = null;
+ self.currentGlassIndex = answer.index;
+
+ if (hadSavedAnswer) {
+ self.questionsData[answer.index].key =
+ (self.questionsData[answer.index].key ??
+ self.questionsData[answer.index].answerKey ??
+ answer.index) + Date.now().toString();
+ }
+
+ return;
+ }
+
+ // Force re-render so the updated selection displays when changing a prior answer
+ self.questionsData[answer.index].key =
+ (self.questionsData[answer.index].key ??
+ self.questionsData[answer.index].answerKey ??
+ answer.index) + Date.now().toString();
+
// set final answer data for the current answered glass part
self.questionsData[answer.index].answerData = {
answerResult: answer.answerResult,
diff --git a/src/mixins/vehicle-questions-mixin.spec.js b/src/mixins/vehicle-questions-mixin.spec.js
index 0fd027258..a557adefa 100644
--- a/src/mixins/vehicle-questions-mixin.spec.js
+++ b/src/mixins/vehicle-questions-mixin.spec.js
@@ -499,6 +499,88 @@ describe("vehicle-questions-mixin", () => {
// Assert
expect(wrapper.vm.selectedAnswers).toMatchObject({});
});
+
+ test("should clear answerData and keep currentGlassIndex when answer is incomplete", () => {
+ const answer = {
+ incomplete: true,
+ answeredQuestions: [
+ {
+ questionText: "Is this the Overland edition?",
+ selectedAnswer: "1|nextQuestion|5|No",
+ selectedAnswerText: "No",
+ questionNum: 1,
+ },
+ ],
+ index: 0,
+ };
+ const { wrapper } = setupMocks({});
+
+ wrapper.vm.currentGlassIndex = 1;
+ wrapper.vm.questionsData = [
+ {
+ glassLocation: "Windshield",
+ glassName: "Single",
+ questions: [
+ {
+ questionSequence: 1,
+ questionText: "Is this the Overland edition?",
+ answers: [],
+ },
+ ],
+ answerData: { answerResult: "OLD-PART" },
+ answerKey: "Windshield-Single",
+ },
+ {
+ glassLocation: "Driver",
+ glassName: "Front",
+ questions: [],
+ answerData: { answerResult: "OTHER-PART" },
+ },
+ ];
+
+ wrapper.vm.handleCompletedQuestionChainAnswers(answer, "", wrapper.vm);
+
+ expect(wrapper.vm.questionsData[0].answerData).toBeNull();
+ expect(wrapper.vm.questionsData[1].answerData).toBeNull();
+ expect(wrapper.vm.currentGlassIndex).toBe(0);
+ });
+
+ test("should not bump key when incomplete answer had no saved answer", () => {
+ const answer = {
+ incomplete: true,
+ answeredQuestions: [
+ {
+ questionText: "Is this the Overland edition?",
+ selectedAnswer: "1|nextQuestion|2|Yes",
+ selectedAnswerText: "Yes",
+ questionNum: 1,
+ },
+ ],
+ index: 0,
+ };
+ const { wrapper } = setupMocks({});
+
+ wrapper.vm.questionsData = [
+ {
+ glassLocation: "Windshield",
+ glassName: "Single",
+ questions: [
+ {
+ questionSequence: 1,
+ questionText: "Is this the Overland edition?",
+ answers: [],
+ },
+ ],
+ answerData: null,
+ answerKey: "Windshield-Single",
+ key: "stable-key",
+ },
+ ];
+
+ wrapper.vm.handleCompletedQuestionChainAnswers(answer, "", wrapper.vm);
+
+ expect(wrapper.vm.questionsData[0].key).toBe("stable-key");
+ });
});
describe("questions in glass parts that are after the answered glass", () => {
diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js
index 78cb134cf..a14a956ba 100644
--- a/src/mixins/vin-pages-mixin.js
+++ b/src/mixins/vin-pages-mixin.js
@@ -6,6 +6,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
import { experimentSettings } from "@/constants/experiments";
import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js";
import { bailoutCodes } from "@/constants/bailout-codes";
+import experimentMixin from "@/mixins/experiment-mixin.js";
export default {
computed: {
@@ -37,7 +38,7 @@ export default {
pageNameToLog: pageName,
});
- if (result.PartNotFound) {
+ if (result.PartNotFound || this.bailoutVinRequiredVehicles()) {
return bailoutMixin.methods.navigateToBailoutPage(
this,
bailoutCodes.PART_NOT_FOUND
@@ -75,5 +76,15 @@ export default {
const phoneRegex = /^\(?(\d{3})\)?[-. ]?(\d{3})[-. ]?(\d{4})$/;
return phoneRegex.test(input);
},
+ bailoutVinRequiredVehicles() {
+ var isVinRequiredVehicle = store.getters.order.vehicle?.vinRequired;
+ if (!isVinRequiredVehicle) {
+ return false;
+ }
+ return experimentMixin.methods.hasSettingEqualTo(
+ experimentSettings.BAILOUT_VIN_REQUIRED_VEHICLES,
+ "true"
+ );
+ },
},
};
diff --git a/src/store/index.js b/src/store/index.js
index 8bbe61132..d9ca5c6fe 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -113,6 +113,7 @@ const getDefaultState = () => {
phoneNumber: null,
phoneExtension: null,
isSmsOptIn: null,
+ isSmsMarketingOptIn: null,
waitListRequested: null,
address: {
streetAddress: null,
@@ -499,6 +500,9 @@ export const mutations = {
updateCustomerIsSmsOptin(state, isSmsOptIn) {
state.order.customer.isSmsOptIn = isSmsOptIn;
},
+ updateCustomerIsSmsMarketingOptin(state, isSmsMarketingOptIn) {
+ state.order.customer.isSmsMarketingOptIn = isSmsMarketingOptIn;
+ },
updateCustomerWaitListRequested(state, waitListRequested) {
state.order.customer.waitListRequested = waitListRequested;
},
@@ -575,6 +579,9 @@ export const mutations = {
state.order.customer.emailAddress = customerDetails.emailAddress;
state.order.customer.phoneNumber = customerDetails.phoneNumber;
state.order.customer.isSmsOptIn = customerDetails.isSmsOptIn;
+ if (customerDetails.isSmsMarketingOptIn !== undefined) {
+ state.order.customer.isSmsMarketingOptIn = customerDetails.isSmsMarketingOptIn;
+ }
}
},
updateInsuranceDetails(state, insuranceDetails) {
@@ -972,6 +979,8 @@ export const mutations = {
state.order.customer.phoneNumber = sessionInformation.order.customer.homePhone;
state.order.customer.phoneExtension = sessionInformation.order.customer.phoneExt;
state.order.customer.isSmsOptIn = sessionInformation.order.customer.isSmsOptIn;
+ state.order.customer.isSmsMarketingOptIn =
+ sessionInformation.order.customer.isSmsMarketingOptIn;
if (sessionInformation.order.customer.address) {
state.order.customer.address = Object.assign(state.order.customer.address, {
@@ -2830,6 +2839,7 @@ export const actions = {
firstName: order.customer.firstName,
lastName: order.customer.lastName,
isSmsOptIn: order.customer.isSmsOptIn,
+ isSmsMarketingOptIn: order.customer.isSmsMarketingOptIn,
phoneNumber: order.customer.phoneNumber,
phoneExt: order.customer.phoneExtension,
address: {
@@ -3882,6 +3892,9 @@ export const actions = {
saveIsSmsOptIn(context, isSmsOptIn) {
context.commit(storeMutations.UPDATE_CUSTOMER_IS_SMS_OPT_IN, isSmsOptIn);
},
+ saveIsSmsMarketingOptIn(context, isSmsMarketingOptIn) {
+ context.commit(storeMutations.UPDATE_CUSTOMER_IS_SMS_MARKETING_OPT_IN, isSmsMarketingOptIn);
+ },
saveWaitListRequested(context, waitListRequested) {
context.commit(storeMutations.UPDATE_CUSTOMER_WAITLIST_REQUESTED, waitListRequested);
diff --git a/src/store/store.spec.js b/src/store/store.spec.js
index 1d199a5d5..74cda6c9f 100644
--- a/src/store/store.spec.js
+++ b/src/store/store.spec.js
@@ -354,6 +354,7 @@ describe("Mutations", () => {
emailAddress: "foo@bar.com",
phoneNumber: "555-555-5555",
isSmsOptIn: true,
+ isSmsMarketingOptIn: false,
};
// Act
@@ -365,6 +366,24 @@ describe("Mutations", () => {
expect(state.order.customer.emailAddress).toEqual("foo@bar.com");
expect(state.order.customer.phoneNumber).toEqual("555-555-5555");
expect(state.order.customer.isSmsOptIn).toEqual(true);
+ expect(state.order.customer.isSmsMarketingOptIn).toEqual(false);
+ });
+
+ it("updateCustomerDetails, should preserve marketing sms consent when not provided", () => {
+ const storeState = state;
+ storeState.order.customer.isSmsOptIn = true;
+ storeState.order.customer.isSmsMarketingOptIn = true;
+
+ mutations.updateCustomerDetails(storeState, {
+ firstName: "foo",
+ lastName: "bar",
+ emailAddress: "foo@bar.com",
+ phoneNumber: "555-555-5555",
+ isSmsOptIn: false,
+ });
+
+ expect(state.order.customer.isSmsOptIn).toEqual(false);
+ expect(state.order.customer.isSmsMarketingOptIn).toEqual(true);
});
it("incrementSubmittedStateRevision, should increment submittedStateRevision", () => {