Merge branch 'develop' into feature/CSR-2130

This commit is contained in:
CarlNation 2024-07-10 07:19:40 -04:00
commit 8b5f837098
11 changed files with 295 additions and 53 deletions

View file

@ -6,7 +6,7 @@ This public/css folder uses a standalone .scss > .css setup.
From a Terminal window, run the Sass Watch command from the public folder only: From a Terminal window, run the Sass Watch command from the public folder only:
```bash ```bash
sass --no-source-map --watch scss:css sass --watch scss:css
``` ```
This will compile and output .css files (into the public/css folder) that can be consumed by the Payment page. This will compile and output .css files (into the public/css folder) that can be consumed by the Payment page.

View file

@ -24,6 +24,7 @@ const errorMessages = {
VIN_FORMAT: VIN_FORMAT:
"Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q", "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
OPTION_REQUIRED: "Please select an option", OPTION_REQUIRED: "Please select an option",
RECAL_ACK_REQUIRED: "Please acknowledge recalibration alert",
VEHICLE_REQUIRED: "Please select a vehicle", VEHICLE_REQUIRED: "Please select a vehicle",
MOBILE_LOCATION_REQUIRED: "Please enter your service address", MOBILE_LOCATION_REQUIRED: "Please enter your service address",
DATE_REQUIRED: "Please select a date", DATE_REQUIRED: "Please select a date",

View file

@ -213,7 +213,7 @@ export default {
classes = "ui-checkbox d-flex"; classes = "ui-checkbox d-flex";
break; break;
case "servicePackageRadio": case "servicePackageRadio":
classes = "package-main"; classes = "package-main d-md-flex justify-content-center";
break; break;
} }
return classes; return classes;
@ -226,7 +226,7 @@ export default {
if (this.buttonTypeString == "radio") { if (this.buttonTypeString == "radio") {
classes += " radio-button-container"; classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") { } else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper"; classes = "package-wrapper col-md-4";
} }
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) { if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {

View file

@ -29,11 +29,25 @@ describe("checkbox-question.vue", () => {
expect(input.attributes().name).toEqual("Checkbox"); expect(input.attributes().name).toEqual("Checkbox");
}); });
it("Should return checkbox id", async () => { it("Should return default id when no custom id is specified", async () => {
// Act
const wrapper = shallowMount(checkboxQuestion, {
propsData: {},
mixins: [mockMixin],
});
// Assert
const input = wrapper.find("input");
// Expect
expect(input.attributes().id).not.toBeFalsy();
});
it("Should return custom id when specified", async () => {
// Act // Act
const wrapper = shallowMount(checkboxQuestion, { const wrapper = shallowMount(checkboxQuestion, {
propsData: { propsData: {
buttonID: "Checkbox ID", customInputId: "Checkbox ID",
}, },
mixins: [mockMixin], mixins: [mockMixin],
}); });

View file

@ -10,32 +10,67 @@
type="checkbox" type="checkbox"
aria-checked="false" aria-checked="false"
:name="checkboxName" :name="checkboxName"
:id="buttonID" :validationRules="validationRules"
:id="inputId"
:tabindex="tabIndex" :tabindex="tabIndex"
:aria-required="isRequired" /> :aria-required="isRequired" />
<p v-html="checkboxLabelCopy" class="m-0"></p> <textBlock
class="m-0"
:customText="checkboxLabelCopy"
marginTopSizeOverride="0"
typeStyle="small"
@text-link-clicked="bubbleTextBlockClick" />
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span> <span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
</label> </label>
</div> </div>
</template> </template>
<script> <script>
import { useField, validate } from "vee-validate";
import { v4 as uuidv4 } from "uuid";
import textBlock from "@/digital-components/text-block/text-block";
export default { export default {
name: "checkboxQuestion", name: "checkboxQuestion",
computed: { computed: {
checkboxLabelCopy() { checkboxLabelCopy() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText"); return this.getCmsContent(this.cmsWidgetName, "QuestionText");
}, },
value: { },
get: function () { setup(props) {
return this.modelValue; const uuid = uuidv4();
}, const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
set: function (newValue) {
this.$emit("update:modelValue", newValue); const fieldOptions = {
}, initialValue: props.modelValue,
}, };
const {
value,
errorMessage,
handleBlur,
handleChange,
meta,
validate,
errors,
resetField,
} = useField(inputId, props.validationRules, fieldOptions);
return {
value,
errorMessage,
handleBlur,
handleChange,
validate,
meta,
errors,
resetField,
inputId,
};
}, },
props: { props: {
customInputId: String,
validationRules: String,
cmsWidgetName: String, cmsWidgetName: String,
checkboxName: String, checkboxName: String,
buttonID: String, buttonID: String,
@ -48,6 +83,24 @@ export default {
default: false, default: false,
}, },
}, },
methods: {
bubbleTextBlockClick(event) {
this.$emit("textLinkClicked", event);
},
},
watch: {
modelValue(newValue, oldValue) {
if (this.value !== newValue) {
this.value = newValue;
}
},
value(newValue, oldValue) {
this.$emit("update:modelValue", newValue);
},
},
components: {
textBlock,
},
}; };
</script> </script>
@ -56,6 +109,7 @@ export default {
.form-check-input { .form-check-input {
border: 1px solid $gray-500; border: 1px solid $gray-500;
border-radius: 2px; border-radius: 2px;
min-width: 1rem; // Prevent squish
&:checked { &:checked {
background-size: 125%; background-size: 125%;
border: 1px solid $blue; border: 1px solid $blue;

View file

@ -1,9 +1,10 @@
<template> <template>
<div class="cart"> <div class="cart">
<div class="px-0"> <div class="px-0">
<!-- set class to 'expaned' on line 7 so cart is open on page load. This may be temporary -->
<div <div
class="row vin-toggle flex align-items-center pt-4" class="row vin-toggle flex align-items-center pt-4"
:class="[isExpanded ? 'expanded' : '']" :class="[isExpanded ? '' : 'expanded']"
@click="toggleIsExpanded()"> @click="toggleIsExpanded()">
<a <a
aria-label="expand cart" aria-label="expand cart"
@ -666,13 +667,27 @@ export default {
recycleFeeCartItemName() { recycleFeeCartItemName() {
return this.getCmsContent("RecycleFeeTextWidget", "Text"); return this.getCmsContent("RecycleFeeTextWidget", "Text");
}, },
requiresRecycleFeeCartItem() {
return !this.isRepair;
},
recycleFeeCartItem() { recycleFeeCartItem() {
let cartItem = null; let cartItem = null;
const recycleFeeLineItem = this.supportingItems.find( let recycleFeeLineItem = this.supportingItems.find(
(supportingItem) => supportingItem.partType == partTypeStrings.REPLACE_FEE (supportingItem) => supportingItem.partType == partTypeStrings.REPLACE_FEE
); );
if (!recycleFeeLineItem && this.requiresRecycleFeeCartItem) {
recycleFeeLineItem = {
partNumber: partTypeStrings.RECYCLE_FEE,
partType: partTypeStrings.REPLACE_FEE,
laborAmount: 0,
kitPrice: 0,
sellingPrice: 0,
salesTax: 0,
};
}
if (recycleFeeLineItem) { if (recycleFeeLineItem) {
cartItem = { cartItem = {
name: this.recycleFeeCartItemName, name: this.recycleFeeCartItemName,

View file

@ -47,10 +47,29 @@ describe("payment-method.vue", () => {
function setupMocks() { function setupMocks() {
store.getters = { store.getters = {
damage: {}, damage: {},
lineItems: [], lineItems: {
glassParts: [],
supportingItems: [],
vaps: [],
promos: [],
},
order: { order: {
payment: { payment: {
isPia: false, isPia: false,
insuranceCoverage: {
isVerified: true,
},
},
lineItems: {
glassParts: [],
supportingItems: [],
vaps: [],
promos: [],
},
policy: {
currentDeductible: 123,
isNoComp: false,
isItac: false,
}, },
}, },
policy: { policy: {

View file

@ -1,7 +1,7 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal notFullScreen ref="loadingModal" /> <loadingModal notFullScreen ref="loadingModal" />
<div class="container-fluid"> <div class="container-fluid payment-method">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
@ -22,6 +22,7 @@
<hr class="my-0" /> <hr class="my-0" />
<cart <cart
ref="cart"
:damage="damageInfo" :damage="damageInfo"
:availableVaps="availableVaps" :availableVaps="availableVaps"
:allowItemRemoval="true" :allowItemRemoval="true"
@ -45,13 +46,31 @@
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
</div> </div>
<div v-if="hasRecal && !isInsurance" class="questions-about-service my-5">
<textBlock
cmsWidgetName="QuestionsAboutYourServiceWidget"
justifyText="left"
class="service-questions" />
<textBlock cmsWidgetName="CallOrTextWidget" justifyText="left" />
<hr class="my-5" />
<div class="d-flex flex-row checkbox-group">
<checkboxQuestion
cmsWidgetName="RecalConfirmWidget"
class="mb-5"
isRequired="true"
v-model="isRecalAckOptIn"
validationRules="recal-ack-required"
@text-link-clicked="openModal('RecalModal')" />
</div>
</div>
<paymentMethodQuestion <paymentMethodQuestion
v-if="!isPiaDisabled" v-if="isPiaEnabled && totalAmountDue > 0"
v-model="paymentMethodInternalModel" v-model="paymentMethodInternalModel"
validationRules="option-required" /> validationRules="payment-method-required" />
<alert <alert
v-if="!isPiaDisabled" v-if="!isPiaEnabled && totalAmountDue > 0"
:isDismissible="false" :isDismissible="false"
alertClass="alert-info" alertClass="alert-info"
cmsWidgetName="NoPiaDisclaimerWidget" cmsWidgetName="NoPiaDisclaimerWidget"
@ -65,6 +84,11 @@
buttonSize buttonSize
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
<contentGroupModal
ref="RecalModal"
cmsWidgetName="RecalModal"
class="recal-modal" />
</div> </div>
</div> </div>
</div> </div>
@ -82,13 +106,20 @@ import cart from "@/fmg-components/cart/cart";
import reviewDropdown from "@/layouts/payment-method/review-dropdown/review-dropdown"; import reviewDropdown from "@/layouts/payment-method/review-dropdown/review-dropdown";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import textBlock from "@/digital-components/text-block/text-block";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
// Supporting Items // Supporting Items
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import {
fetchCmsContentForPage,
doesCopyContainRouterLink,
getRouterLinkRouteFromCopy,
} from "@/helpers/cms-content-helper";
import { paymentMethods } from "@/constants/payment-method-constants"; import { paymentMethods } from "@/constants/payment-method-constants";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { import {
@ -115,10 +146,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
import { mapTaxedLineItemsToStoreFormat } from "../../store"; import { mapTaxedLineItemsToStoreFormat } from "../../store";
import { coverageStatus } from "@/constants/insurance"; import { coverageStatus } from "@/constants/insurance";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
export default { export default {
name: "paymentMethod", name: "paymentMethod",
props: {
recyclingModalCmsWidgetName: String,
},
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -309,9 +344,15 @@ export default {
paymentMethodInternalModel: this.getPaymentMethodFromStore(), paymentMethodInternalModel: this.getPaymentMethodFromStore(),
inactivePromos: this.getInactivePromosFromStore(), inactivePromos: this.getInactivePromosFromStore(),
isAppleDevice: false, isAppleDevice: false,
isRecalAckOptIn: false,
}; };
}, },
methods: { methods: {
doesCopyContainRouterLink,
getRouterLinkRouteFromCopy,
navigateWithScenario(scenarioName) {
this.$router.navigateWithoutSaving(scenarioName, this.$route);
},
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
// Service Location // Service Location
const serviceLocation = store.getters.order.serviceLocation; const serviceLocation = store.getters.order.serviceLocation;
@ -451,14 +492,12 @@ export default {
this.paymentMethod, this.paymentMethod,
false false
); );
// save lineitems as they now have salestax added // save lineitems as they now have salestax added
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING, storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
this.lineItems.glassParts, this.lineItems.glassParts,
false false
); );
await this.dispatchStoreAction( await this.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING, storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
this.lineItems.supportingItems, this.lineItems.supportingItems,
@ -473,7 +512,6 @@ export default {
}, },
false false
); );
if (this.paymentMethod == paymentMethods.LATER) { if (this.paymentMethod == paymentMethods.LATER) {
// this creates the final work order // this creates the final work order
await submitWorkOrder({ pageNameToLog: "payment-method", submitAfterSave: true }); await submitWorkOrder({ pageNameToLog: "payment-method", submitAfterSave: true });
@ -521,8 +559,20 @@ export default {
hasSubmittedOrder() { hasSubmittedOrder() {
return this.$store.getters.hasSubmittedOrder; return this.$store.getters.hasSubmittedOrder;
}, },
openModal(modalName) {
this.$refs[modalName].openModal();
},
}, },
computed: { computed: {
hasRecal() {
const recalLineItem = this.$store.getters.order.lineItems.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.RECALIBRATION
);
if (recalLineItem) {
return true;
}
return false;
},
damageInfo() { damageInfo() {
return this.$store.getters.damage; return this.$store.getters.damage;
}, },
@ -584,21 +634,21 @@ export default {
return null; return null;
} }
}, },
isPiaDisabled() { totalAmountDue() {
return baseMixin.methods.getAmountDue(this.lineItems);
},
isPiaEnabled() {
const piaExperience = this.getSettingValue(experimentSettings.PIA_EXPERIENCE); const piaExperience = this.getSettingValue(experimentSettings.PIA_EXPERIENCE);
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE); const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
var isEnabled = false;
if (this.isInsurance) { if (this.isInsurance) {
isEnabled = piaInsurance === "true" && this.currentDeductible > 0; return piaInsurance === "true" && this.currentDeductible > 0;
} else { } else {
isEnabled = piaExperience === "PIA Optional" || piaExperience === "PIA Required"; return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
} }
return !isEnabled;
}, },
paymentMethod() { paymentMethod() {
if (this.isPiaDisabled) { if (!this.isPiaEnabled) {
return paymentMethods.LATER; return paymentMethods.LATER;
} }
@ -665,11 +715,27 @@ export default {
alert, alert,
paymentMethodQuestion, paymentMethodQuestion,
reviewDropdown, reviewDropdown,
textBlock,
checkboxQuestion,
contentGroupModal,
}, },
}; };
</script> </script>
<style></style>
<style lang="scss" scoped> <style lang="scss" scoped>
.checkbox-group {
align-items: start;
> * {
margin-top: 0;
}
}
.questions-about-service {
.service-questions {
font-weight: $font-weight-bold;
}
}
.cart { .cart {
margin-bottom: 0; margin-bottom: 0;
} }
@ -677,4 +743,22 @@ export default {
color: $gray-550; color: $gray-550;
margin: 0; margin: 0;
} }
.payment-method {
.recal-modal {
:deep(.modal-body) {
display: flex;
flex-direction: column;
h5 {
text-align: center;
order: 1;
}
p {
order: 3;
}
img {
order: 2;
}
}
}
}
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" /> <loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles"> <div class="container-fluid page-container-grouped-styles quote">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
@ -20,7 +20,10 @@
cmsWidgetName="CashOrInsuranceQuestionWidget" cmsWidgetName="CashOrInsuranceQuestionWidget"
v-model="isInsuranceSelected" v-model="isInsuranceSelected"
groupName="CashOrInsuranceQuestion" /> groupName="CashOrInsuranceQuestion" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<servicePackageQuestion <servicePackageQuestion
ref="servicePackage" ref="servicePackage"
cashCmsWidgetName="CashServicePackageQuestionWidget" cashCmsWidgetName="CashServicePackageQuestionWidget"
@ -34,7 +37,10 @@
v-on="{ 'buttonEvent.openModal': openModalAction }" v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required" validationRules="option-required"
isRequired /> isRequired />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<afterpayModalBanner <afterpayModalBanner
v-if="!isInsuranceSelected && !isRecalibrationOnOrder" v-if="!isInsuranceSelected && !isRecalibrationOnOrder"
cmsWidgetName="AfterpayModalWidget" cmsWidgetName="AfterpayModalWidget"
@ -43,7 +49,6 @@
:lineItems="lineItems" /> :lineItems="lineItems" />
<recalDisclaimer <recalDisclaimer
class="my-5"
cmsWidgetName="RecalDisclaimerWidget" cmsWidgetName="RecalDisclaimerWidget"
v-if="isRecalibrationOnOrder" v-if="isRecalibrationOnOrder"
v-on="{ textLinkClicked: openModalAction }" v-on="{ textLinkClicked: openModalAction }"

View file

@ -12,7 +12,9 @@
]" ]"
for="testradio"> for="testradio">
<div class="package-specs"> <div class="package-specs">
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row"> <div
v-if="this.additionalButtonData.servicePackageDiscount"
class="row justify-content-between">
<div class="col md-6"> <div class="col md-6">
<p class="m-0"> <p class="m-0">
<span v-html="this.buttonLabel"></span> <span v-html="this.buttonLabel"></span>
@ -136,24 +138,20 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.discount-text {
line-height: 36px;
display: flex;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
background-color: #469d2a;
align-items: center;
justify-content: center;
padding: 5px;
width: 100%;
color: white;
}
.package-main { .package-main {
.package-wrapper { .package-wrapper {
margin: 0.5rem 0; margin: 1rem 0;
@include media-breakpoint-up(md) {
margin: 0.5rem 0;
}
position: relative;
@include media-breakpoint-up(md) {
margin: 1rem 0.5rem;
}
label { label {
display: block; display: block;
height: 100%;
} }
input[type="radio"] { input[type="radio"] {
@ -174,13 +172,21 @@ export default {
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
min-height: 60px; min-height: 60px;
@include media-breakpoint-up(md) {
max-height: 500px;
height: 100%;
}
&.has-subheader { &.has-subheader {
min-height: 80px; min-height: 80px;
} }
&.has-text { &.has-text {
border-radius: 0 0 0.5rem 0.5rem; border-radius: 0 0 0.5rem 0.5rem;
border: 1px solid #469d2a !important; border: 1px solid #469d2a;
@include media-breakpoint-up(md) {
border-radius: 0.5rem;
padding-top: 3.75rem;
}
} }
&:before { &:before {
@ -265,6 +271,9 @@ export default {
width: 100%; width: 100%;
max-height: 0; max-height: 0;
transition: all 0.5s ease; transition: all 0.5s ease;
@include media-breakpoint-up(md) {
max-height: 500px;
}
p { p {
font-weight: 500; font-weight: 500;
@ -337,6 +346,34 @@ export default {
.hide-when-closed { .hide-when-closed {
display: none; display: none;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
}
}
}
.discount-text {
line-height: 36px;
display: flex;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
background-color: #469d2a;
align-items: center;
justify-content: center;
padding: 5px;
width: 100%;
color: white;
height: 2.875rem;
@include media-breakpoint-up(md) {
position: absolute;
z-index: 1;
~ label {
.package-label {
&:after {
top: 68px;
}
}
}
} }
} }
} }

View file

@ -37,6 +37,19 @@ body {
max-width: 100%; max-width: 100%;
} }
} }
// Quote page desktop
&.quote {
.col-md-6,
.col-lg-4 {
max-width: 1200px;
}
.col-md-6.col-lg-4 {
@include media-breakpoint-up(lg) {
width: 100%;
max-width: 910px;
}
}
}
//END set max-width on columns //END set max-width on columns
} }
.pointer { .pointer {