Merge branch 'develop' into feature/CSR-1384-modals-for-add-vaps

This commit is contained in:
Adam Caouette 2023-10-11 14:04:54 -04:00
commit d6653354e7
12 changed files with 583 additions and 18 deletions

View file

@ -6,6 +6,8 @@ const experimentSettings = {
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
};
const experimentTriggers = {

View file

@ -0,0 +1,13 @@
export const paymentMethods = {
NONE: null,
LATER: "Later",
CREDIT_CARD: "CreditCard",
PAYPAL: "Paypal",
AFTERPAY: "Afterpay",
};
export const paymentTimes = {
NONE: null,
ADVANCE: "Advance",
LATER: "Later",
};

View file

@ -7,7 +7,7 @@
@click="toggleClass()">
<div class="col d-flex justify-content-between">
<span class="label">Amount Due</span
><span class="label amount-due">${{ amountDue }}</span>
><span class="label amount-due">{{ amountDue }}</span>
</div>
</div>
<div class="price-table">
@ -110,7 +110,7 @@ export default {
return price;
},
getAmountDue() {
return baseMixin.methods.getAmountDue(this.storeLineItems);
return baseMixin.methods.getDisplayAmountDue(this.storeLineItems);
},
removeItem(item) {
this.$emit("cart-remove", item);

View file

@ -0,0 +1,104 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import paymentMethodListButton from "@/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button";
const testConstants = {
images: {
A: "imageA",
B: "imageB",
NONE: null,
},
names: {
A: "nameA",
B: "nameB",
},
content: {
withInline: "Button text with {custom:inlineImage} inline.",
noInline: "Button text with no inline",
},
};
let cmsContent;
describe("Payment Method Question", () => {
beforeEach(() => {
cmsContent = {};
});
describe("Side image", () => {
it("Renders side image if image is present and not inline", () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
const showSideImage = wrapper.vm.shouldDisplaySideImage;
expect(showSideImage).toBe(true);
});
it("Does not render side image if no image is present", () => {
// Arrange
let props = generateDefaultProps();
props.buttonImage = testConstants.images.NONE;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
const showSideImage = wrapper.vm.shouldDisplaySideImage;
expect(showSideImage).toBe(false);
});
it("Does not render side image if inline", () => {
// Arrange
let props = generateDefaultProps();
props.buttonLabel = testConstants.content.withInline;
props.altText = testConstants.content.withInline;
const { wrapper } = setupMocks({
propsData: props,
});
// Act
const showSideImage = wrapper.vm.shouldDisplaySideImage;
expect(showSideImage).toBe(false);
});
});
});
function generateDefaultProps() {
return {
buttonLabel: testConstants.noInline,
altText: testConstants.noInline,
groupName: "payment-method",
value: testConstants.names.A,
buttonImage: testConstants.images.A,
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(paymentMethodListButton, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,123 @@
<template>
<baseInputButton
v-bind="$props"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
v-model="selectedValue">
<div class="button-content list-button-content d-flex flex-row py-3 px-4">
<img
v-if="shouldDisplaySideImage"
:id="buttonImageId"
class="ms-auto order-3"
:src="buttonImage"
:alt="altText" />
<div class="order-2">
<p class="m-0">
<span v-for="token in tokens" :key="token">
<span v-if="isInlineImageToken(token)">
<img
v-if="hasImage"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}
</span>
</span>
</p>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
function isInlineImageToken(token) {
return token.includes(INLINE_IMAGE_TOKEN);
}
function getInlineAltText(token) {
let innerTokens = token.split(",");
return innerTokens[1] ?? "";
}
export default {
name: "payment-method-list-button",
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
},
methods: {
isInlineImageToken,
getInlineAltText,
},
computed: {
tokens() {
return splitCopyOnCMSPlaceHolder(this.buttonLabel ?? "");
},
hasImage() {
return !!this.buttonImage;
},
shouldDisplaySideImage() {
return this.hasImage && this.tokens.every((token) => !isInlineImageToken(token));
},
},
};
</script>
<style lang="scss" scoped>
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
color: $gray-600;
}
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
}
</style>

View file

@ -0,0 +1,130 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import paymentMethodQuestion from "@/layouts/payment-method/payment-method-question/payment-method-question";
const testConstants = {
cms: {
question: {
text: "Choose a payment option",
},
answers: {
optionA: {
name: "NameA",
text: "TextA",
subText: "SubTextA",
imageId: "idA",
image: "imageA",
subWidget: "subWidgetA",
},
optionB: {
name: "NameB",
text: "TextB",
subText: "SubTextB",
imageId: "idB",
image: "imageB",
subWidget: "subWidgetB",
},
},
},
};
let cmsContent;
describe("Payment Method Question", () => {
beforeEach(() => {
cmsContent = {
PaymentMethodWidget: {
QuestionText: testConstants.cms.question,
Answers: [
{
Name: testConstants.cms.answers.optionA.name,
Text: testConstants.cms.answers.optionA.text,
SubText: testConstants.cms.answers.optionA.subText,
ImageId: testConstants.cms.answers.optionA.imageId,
AnswerImageUrl: testConstants.cms.answers.optionA.image,
SubWidgetName: testConstants.cms.answers.optionA.subWidget,
},
{
Name: testConstants.cms.answers.optionB.name,
Text: testConstants.cms.answers.optionB.text,
SubText: testConstants.cms.answers.optionB.subText,
ImageId: testConstants.cms.answers.optionB.imageId,
AnswerImageUrl: testConstants.cms.answers.optionB.image,
SubWidgetName: testConstants.cms.answers.optionB.subWidget,
},
],
},
};
});
it("Should map cms content correctly", () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
const mappedContent = wrapper.vm.paymentMethodAnswerData;
// Assert
expect(mappedContent).toEqual([
{
buttonLabel: testConstants.cms.answers.optionA.text,
altText: testConstants.cms.answers.optionA.text,
groupName: "payment-method",
value: testConstants.cms.answers.optionA.name,
buttonImage: testConstants.cms.answers.optionA.image,
},
{
buttonLabel: testConstants.cms.answers.optionB.text,
altText: testConstants.cms.answers.optionB.text,
groupName: "payment-method",
value: testConstants.cms.answers.optionB.name,
buttonImage: testConstants.cms.answers.optionB.image,
},
]);
});
it("Handles null cms content gracefully", () => {
// Arrange
cmsContent = {};
const props = generateDefaultProps();
const { wrapper } = setupMocks({
propsData: props,
});
// Act
const mappedContent = wrapper.vm.paymentMethodAnswerData;
// Assert
expect(mappedContent).toEqual([]);
});
});
function generateDefaultProps() {
return {
modelValue: "modelValue",
};
}
function setupMocks(customMountOptions) {
const mountOptions = getMountOptions(customMountOptions);
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(paymentMethodQuestion, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}

View file

@ -0,0 +1,80 @@
<template>
<div class="payment-method-question">
<buttonQuestion
groupName="payment-method"
buttonTypeString="payment-method-list-button"
:buttonTypeObject="paymentMethodListButton"
isWide
:questionText="paymentMethodQuestionText"
:answers="paymentMethodAnswerData"
isRequired
v-model="selectedMethod"
textPosition="text-start" />
</div>
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import paymentMethodListButton from "./payment-method-list-button/payment-method-list-button";
import { paymentMethods, paymentTimes } from "@/constants/payment-method-constants";
export default {
name: "payment-method-question",
data() {
return {
paymentMethodListButton: paymentMethodListButton,
};
},
props: {
modelValue: String,
},
methods: {
getAnswersNullSafe(widgetName) {
const rawData = this.getCmsContent(widgetName, "Answers");
if (!rawData) {
return [];
} else {
return rawData;
}
},
},
computed: {
selectedMethod: {
get: function () {
return this.modelValue;
},
set: function (selectedMethod) {
this.$emit("update:modelValue", selectedMethod);
},
},
paymentMethodQuestionText() {
return this.getCmsContent("PaymentMethodWidget", "QuestionText");
},
paymentMethodAnswerData() {
const cmsData = this.getAnswersNullSafe("PaymentMethodWidget");
return cmsData.map((answer) => {
return {
buttonLabel: answer.Text,
altText: answer.Text,
groupName: "payment-method",
value: answer.Name,
buttonImage: answer.AnswerImageUrl,
};
});
},
},
components: {
buttonQuestion,
},
};
</script>
<style lang="scss">
.payment-method-question {
.question-text span {
text-align: left;
}
}
</style>

View file

@ -5,6 +5,7 @@ import customerDetails from "@/layouts/payment-method/payment-method.vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import { experimentSettings } from "@/constants/experiments";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
@ -15,6 +16,8 @@ jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
submitWorkOrder: jest.fn(),
}));
let piaDisabledFlag = false;
describe("payment-method.vue", () => {
describe("navigation", () => {
test("if the back button is clicked, navigate back", async () => {
@ -52,19 +55,37 @@ function setupMocks() {
},
};
const wrapper = shallowMount(
customerDetails,
getMountOptions({
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
store: {
getters: store.getters,
},
})
);
const mountOptions = getMountOptions({
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
store: {
getters: store.getters,
},
});
const mockMixin = {
methods: {
getSettingValue: jest.fn((settingName) => {
if (!piaDisabledFlag) {
if (settingName === experimentSettings.PIA_EXPERIENCE) {
return "PIA Optional";
}
if (settingName === experimentSettings.SUBMIT_ORDER_ENABLE_PIA) {
return "true";
}
}
return "false";
}),
},
};
mountOptions.global.mixins = [mockMixin];
const wrapper = shallowMount(customerDetails, mountOptions);
return { wrapper };
}

View file

@ -78,8 +78,13 @@
</div>
<button @click="openModal('RainDefenseModal')">OPEN MODAL</button>
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
<paymentMethodQuestion
v-if="!isPiaDisabled"
v-model="paymentMethodInternalModel" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction"
@ -95,6 +100,7 @@
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import paymentMethodQuestion from "@/layouts/payment-method/payment-method-question/payment-method-question";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import cart from "@/fmg-components/cart/cart";
import addVapsModalButtons from "@/fmg-components/add-vaps-modal-buttons/add-vaps-modal-buttons";
@ -107,6 +113,9 @@ import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { paymentMethods } from "@/constants/payment-method-constants";
import { experimentSettings } from "@/constants/experiments";
import { Form } from "vee-validate";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
@ -192,6 +201,7 @@ export default {
lineItems: [],
availableLineItems: [],
displayPaypalAlert: this.getPaypalAlert(),
paymentMethodInternalModel: null,
};
},
methods: {
@ -320,6 +330,38 @@ export default {
// console.log("this.$store.getters.damage ", this.$store.getters.damage)
return this.$store.getters.damage;
},
customCtaCopy() {
switch (this.paymentMethod) {
case paymentMethods.CREDIT_CARD:
return "Continue to checkout";
case paymentMethods.PAYPAL:
return "Continue to Paypal";
case paymentMethods.AFTERPAY:
return "Continue to Afterpay";
default:
return null;
}
},
isPiaDisabled() {
const piaExperience =
this.getSettingValue(experimentSettings.PIA_EXPERIENCE) === "PIA Optional";
const enablePIA =
this.getSettingValue(experimentSettings.SUBMIT_ORDER_ENABLE_PIA) === "true";
return !(piaExperience && enablePIA);
},
paymentMethod() {
if (this.isPiaDisabled) {
return paymentMethods.LATER;
}
return this.paymentMethodInternalModel;
},
},
watch: {
customCtaCopy(newValue) {
this.$refs.navbar.updateButtonText(newValue);
},
},
components: {
funnelHeader,
@ -331,6 +373,7 @@ export default {
alert,
addVapsModalButtons,
contentGroupModal,
paymentMethodQuestion,
},
};
</script>

View file

@ -128,9 +128,11 @@ export default {
const itemsClone = { ...lineItems };
delete itemsClone.serverData;
for (var propertyName in itemsClone) {
amountDue += this.getTotalPriceOfAllLineItemsAndChildParts(
itemsClone[propertyName]
);
if (itemsClone[propertyName]) {
amountDue += this.getTotalPriceOfAllLineItemsAndChildParts(
itemsClone[propertyName]
);
}
}
return amountDue;
},

View file

@ -599,7 +599,10 @@ export const getters = {
funnelServiceCity: state.order.serviceLocation.city,
funnelServiceState: state.order.serviceLocation.state,
funnelServiceZipCode: state.order.serviceLocation.zipCode,
funnelServiceZipCodeCtu: state.order.serviceLocation.zipCodeCtu,
funnelProviderNumber: state.order.serviceLocation.provider.providerNumber,
funnelParentAccountNumber: state.order.payment.parentAccountNumber,
funnelReferralType: state.order.payment.isInsurance ? "INSURANCE" : "CASH QUOTE",
funnelIsCoverageVerified: state.order.payment.insuranceCoverage.isVerified,
funnelHasRecalibrationPart: getHasRecalibrationPart(state),
funnelSelectedMultiGlass: state.order.damage.glassToReplace?.length > 1,

View file

@ -2934,6 +2934,9 @@ describe("Getters", () => {
funnelSupportingItems: null,
funnelVaps: null,
funnelGlassToReplace: null,
funnelProviderNumber: "1",
funnelReferralType: "CASH QUOTE",
funnelServiceZipCodeCtu: "11111",
};
//Act
@ -2948,12 +2951,17 @@ describe("Getters", () => {
city: mockStateValues.funnelServiceCity,
state: mockStateValues.funnelServiceState,
zipCode: mockStateValues.funnelServiceZipCode,
zipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
provider: {
providerNumber: mockStateValues.funnelProviderNumber,
},
});
mutations.updateParentAcctNumber(storeState, mockStateValues.funnelParentAccountNumber);
mutations.updateInsuranceVerifiedStatus(
storeState,
mockStateValues.funnelIsCoverageVerified
);
mutations.updateIsInsurance(storeState, false);
mutations.updateGlassParts(storeState, mockStateValues.funnelGlassParts);
mutations.updateSupportingItems(storeState, mockStateValues.funnelSupportingItems);
mutations.updateVaps(storeState, mockStateValues.funnelVaps);
@ -2981,6 +2989,9 @@ describe("Getters", () => {
funnelSelectedBackGlass: false,
funnelSelectedDriverSideGlass: false,
funnelSelectedPassengerSideGlass: false,
funnelProviderNumber: mockStateValues.funnelProviderNumber,
funnelReferralType: mockStateValues.funnelReferralType,
funnelServiceZipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
});
});
@ -3003,6 +3014,9 @@ describe("Getters", () => {
funnelGlassParts: [],
funnelOtherParts: [],
funnelGlassToReplace: [],
funnelProviderNumber: "1",
funnelReferralType: "CASH QUOTE",
funnelServiceZipCodeCtu: "11111",
};
//Act
@ -3017,12 +3031,17 @@ describe("Getters", () => {
city: mockStateValues.funnelServiceCity,
state: mockStateValues.funnelServiceState,
zipCode: mockStateValues.funnelServiceZipCode,
zipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
provider: {
providerNumber: mockStateValues.funnelProviderNumber,
},
});
mutations.updateParentAcctNumber(storeState, mockStateValues.funnelParentAccountNumber);
mutations.updateInsuranceVerifiedStatus(
storeState,
mockStateValues.funnelIsCoverageVerified
);
mutations.updateIsInsurance(storeState, false);
mutations.updateGlassParts(storeState, mockStateValues.funnelGlassParts);
mutations.updateSupportingItems(storeState, mockStateValues.funnelSupportingItems);
mutations.updateVaps(storeState, mockStateValues.funnelVaps);
@ -3050,6 +3069,9 @@ describe("Getters", () => {
funnelSelectedBackGlass: false,
funnelSelectedDriverSideGlass: false,
funnelSelectedPassengerSideGlass: false,
funnelProviderNumber: mockStateValues.funnelProviderNumber,
funnelReferralType: mockStateValues.funnelReferralType,
funnelServiceZipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
});
});
@ -3085,6 +3107,9 @@ describe("Getters", () => {
glassName: "Single",
},
],
funnelProviderNumber: "1",
funnelReferralType: "CASH QUOTE",
funnelServiceZipCodeCtu: "11111",
};
//Act
@ -3099,9 +3124,14 @@ describe("Getters", () => {
city: mockStateValues.serviceCity,
state: mockStateValues.serviceState,
zipCode: mockStateValues.serviceZipCode,
zipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
provider: {
providerNumber: mockStateValues.funnelProviderNumber,
},
});
mutations.updateParentAcctNumber(storeState, mockStateValues.parentAccountNumber);
mutations.updateInsuranceVerifiedStatus(storeState, mockStateValues.isCoverageVerified);
mutations.updateIsInsurance(storeState, false);
mutations.updateGlassParts(storeState, mockStateValues.glassParts);
mutations.updateSupportingItems(storeState, mockStateValues.funnelSupportingItems);
mutations.updateVaps(storeState, mockStateValues.funnelVaps);
@ -3129,6 +3159,9 @@ describe("Getters", () => {
funnelSelectedBackGlass: false,
funnelSelectedDriverSideGlass: false,
funnelSelectedPassengerSideGlass: false,
funnelProviderNumber: mockStateValues.funnelProviderNumber,
funnelReferralType: mockStateValues.funnelReferralType,
funnelServiceZipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
});
});
@ -3190,6 +3223,9 @@ describe("Getters", () => {
glassName: "Quarter",
},
],
funnelProviderNumber: "1",
funnelReferralType: "CASH QUOTE",
funnelServiceZipCodeCtu: "11111",
};
//Act
@ -3204,9 +3240,14 @@ describe("Getters", () => {
city: mockStateValues.serviceCity,
state: mockStateValues.serviceState,
zipCode: mockStateValues.serviceZipCode,
zipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
provider: {
providerNumber: mockStateValues.funnelProviderNumber,
},
});
mutations.updateParentAcctNumber(storeState, mockStateValues.parentAccountNumber);
mutations.updateInsuranceVerifiedStatus(storeState, mockStateValues.isCoverageVerified);
mutations.updateIsInsurance(storeState, false);
mutations.updateGlassParts(storeState, mockStateValues.glassParts);
mutations.updateSupportingItems(storeState, mockStateValues.funnelSupportingItems);
mutations.updateVaps(storeState, mockStateValues.funnelVaps);
@ -3234,6 +3275,9 @@ describe("Getters", () => {
funnelSelectedBackGlass: true,
funnelSelectedDriverSideGlass: true,
funnelSelectedPassengerSideGlass: true,
funnelProviderNumber: mockStateValues.funnelProviderNumber,
funnelReferralType: mockStateValues.funnelReferralType,
funnelServiceZipCodeCtu: mockStateValues.funnelServiceZipCodeCtu,
});
});
});