Merge pull request #1970 from Safelite/feature/CSR-2179-ajc

Feature/CSR-2179-ajc
This commit is contained in:
AdamCaouetteSafelite 2024-09-12 09:41:19 -04:00 committed by GitHub
commit b42553aec0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 129 additions and 39 deletions

View file

@ -1,5 +1,6 @@
const experimentUniverses = { const experimentUniverses = {
CONCEPT_FUNNEL: "ConceptFunnel", CONCEPT_FUNNEL: "ConceptFunnel",
RECAL_PRICE_REMOVAL: "NextGen_RecalPriceRemoval",
}; };
const experimentSettings = { const experimentSettings = {
@ -11,6 +12,7 @@ const experimentSettings = {
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA", SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional", IS_EMAIL_OPTIONAL: "isEmailOptional",
SERVICE_PACKAGE_DISCOUNT: "OfferServicePackageDiscount", SERVICE_PACKAGE_DISCOUNT: "OfferServicePackageDiscount",
RECAL_PRICE_REMOVE: "RecalPriceRemove",
}; };
const experimentTriggers = { const experimentTriggers = {

View file

@ -1,10 +1,9 @@
<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"
@ -188,10 +187,11 @@ export default {
insuranceDeductible: Number, insuranceDeductible: Number,
insuranceCompanyName: String, insuranceCompanyName: String,
showInsuranceCoverageAs: String, showInsuranceCoverageAs: String,
shouldHideRecalibration: Boolean,
}, },
data() { data() {
return { return {
isExpanded: false, isExpanded: true, // set default to true so cart is open on page load. This may be temporary.
currencyFormatter: new Intl.NumberFormat("en-US", { currencyFormatter: new Intl.NumberFormat("en-US", {
style: "currency", style: "currency",
currency: "USD", currency: "USD",
@ -303,12 +303,11 @@ export default {
}, },
lineItemsWithoutRecal() { lineItemsWithoutRecal() {
if (!this.lineItems || this.lineItems.length < 1) return; if (!this.lineItems || this.lineItems.length < 1) return;
if (!this.lineItems.supportingItems) return this.lineItems;
const lineItemsCopy = deepClone(this.lineItems); const lineItemsCopy = deepClone(this.lineItems);
lineItemsCopy.supportingItems = baseMixin.methods.filterOutRecalibration( lineItemsCopy.supportingItems = lineItemsCopy.supportingItems ? baseMixin.methods.filterOutRecalibration(
lineItemsCopy.supportingItems lineItemsCopy?.supportingItems
); ) : [];
return lineItemsCopy; return lineItemsCopy;
}, },
showCoverageAsPending() { showCoverageAsPending() {
@ -439,14 +438,13 @@ export default {
packagePrice() { packagePrice() {
let packagePrice = 0; let packagePrice = 0;
if (!this.isInsurance) { if (!this.isInsurance && this.shouldHideRecalibration) { // Update packagePrice if Cash only && is part of RemoveRecal experiment
// CASH ONLY
packagePrice = baseMixin.methods.getTierOnePackagePrice( packagePrice = baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees( baseMixin.methods.filterOutFees(
baseMixin.methods.filterOutRecalibration(this.availableLineItems) baseMixin.methods.filterOutRecalibration(this.availableLineItems) // Strip out recal before filtering out fees
) )
); );
} else if (!this.showCoverageAsVerified && !this.showCoverageAsPending) { } else if (!this.showCoverageAsVerified && !this.showCoverageAsPending) { // Update packagePrice if is verified insurance OR if Cash && not part of RemoveRecal experiment
packagePrice = baseMixin.methods.getTierOnePackagePrice( packagePrice = baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(this.availableLineItems) baseMixin.methods.filterOutFees(this.availableLineItems)
); );
@ -1001,22 +999,22 @@ export default {
}, },
subTotal() { subTotal() {
if (!this.lineItems || this.lineItems.length < 1) return; if (!this.lineItems || this.lineItems.length < 1) return;
return this.isInsurance return this.isInsurance || !this.shouldHideRecalibration
? baseMixin.methods.getSubTotal(this.lineItems) ? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
: baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); : baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculated without recal
}, },
salesTax() { salesTax() {
if (!this.lineItems || this.lineItems.length < 1) return; if (!this.lineItems || this.lineItems.length < 1) return;
return this.isInsurance return this.isInsurance || !this.shouldHideRecalibration
? baseMixin.methods.getSalesTax(this.lineItems) ? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
: baseMixin.methods.getSalesTax(this.lineItemsWithoutRecal); : baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculated without recal
}, },
amountDue() { amountDue() {
if (!this.lineItems || this.lineItems.length < 1) return; if (!this.lineItems || this.lineItems.length < 1) return;
if (this.showAsPaid) return 0; if (this.showAsPaid) return 0;
return this.isInsurance return this.isInsurance || !this.shouldHideRecalibration
? baseMixin.methods.getAmountDue(this.lineItems) ? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
: baseMixin.methods.getAmountDue(this.lineItemsWithoutRecal); : baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculated without recal
}, },
amountPaid() { amountPaid() {
if (!this.showAsPaid) { if (!this.showAsPaid) {

View file

@ -565,6 +565,7 @@ function setupMocks({ customMountOptions }) {
getCmsContent: jest.fn().mockImplementation(() => { getCmsContent: jest.fn().mockImplementation(() => {
return wordingText; return wordingText;
}), }),
getSettingValue: jest.fn(),
}, },
}, },
]; ];

View file

@ -62,7 +62,8 @@
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
:insuranceDeductible="currentDeductible" :insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName" :insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs" /> :showInsuranceCoverageAs="showInsuranceCoverageAs"
:shouldHideRecalibration="shouldHideRecalibration" />
<hr class="mb-5" /> <hr class="mb-5" />
@ -105,11 +106,13 @@ import { Form } from "vee-validate";
import { get12HourTimeFormat, get12HourTimeMobileFormat } from "@/helpers/date-helper"; import { get12HourTimeFormat, get12HourTimeMobileFormat } from "@/helpers/date-helper";
import { coverageStatus } from "@/constants/insurance"; import { coverageStatus } from "@/constants/insurance";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper"; import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
import { experimentSettings } from "@/constants/experiments";
import experimentMixin from "@/mixins/experiment-mixin.js";
export default { export default {
name: "confirmation", name: "confirmation",
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_ORDER); await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_ORDER); // This nulls the Store order
// Call APIs // Call APIs
const submittedOrder = baseMixin.methods.getSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder();
@ -166,6 +169,11 @@ export default {
}; };
}, },
computed: { computed: {
shouldHideRecalibration() {
return this.getSettingValue(
experimentSettings.RECAL_PRICE_REMOVE
);
},
ShowCart() { ShowCart() {
if (this.isPia && this.submittedOrder?.settledTenderAmount == 0) { if (this.isPia && this.submittedOrder?.settledTenderAmount == 0) {
// settleTenderAmount always shows 0 via localhost or dev. // settleTenderAmount always shows 0 via localhost or dev.

View file

@ -33,7 +33,8 @@
:isInsurance="isInsurance" :isInsurance="isInsurance"
:insuranceDeductible="currentDeductible" :insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName" :insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs" /> :showInsuranceCoverageAs="showInsuranceCoverageAs"
:shouldHideRecalibration="shouldHideRecalibration" />
<hr class="my-5" /> <hr class="my-5" />
@ -47,14 +48,14 @@
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
</div> </div>
<div v-if="hasRecal && !isInsurance" class="questions-about-service my-5"> <div v-if="!isInsurance" class="questions-about-service my-5">
<textBlock <textBlock
cmsWidgetName="QuestionsAboutYourServiceWidget" cmsWidgetName="QuestionsAboutYourServiceWidget"
justifyText="left" justifyText="left"
class="service-questions" /> class="service-questions" />
<textBlock cmsWidgetName="CallOrTextWidget" justifyText="left" /> <textBlock cmsWidgetName="CallOrTextWidget" justifyText="left" />
<hr class="my-5" /> <hr class="my-5" />
<div class="d-flex flex-row checkbox-group"> <div class="d-flex flex-row checkbox-group" v-if="isRecalibrationOnOrder && shouldHideRecalibration">
<checkboxQuestion <checkboxQuestion
cmsWidgetName="RecalConfirmWidget" cmsWidgetName="RecalConfirmWidget"
class="mb-5" class="mb-5"
@ -125,6 +126,7 @@ import {
} from "@/helpers/cms-content-helper"; } 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 experimentMixin from "@/mixins/experiment-mixin.js";
import { import {
revalidatePromosAndValidateQueryStringPromo, revalidatePromosAndValidateQueryStringPromo,
buildToastMessagesFromRevalidateOrValidatePromoResponse, buildToastMessagesFromRevalidateOrValidatePromoResponse,
@ -148,6 +150,7 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { partTypeStrings } from "@/constants/part-type-strings"; 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";
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
@ -582,15 +585,16 @@ export default {
}, },
}, },
computed: { computed: {
hasRecal() { isRecalibrationOnOrder() {
const recalLineItem = this.$store.getters.order.lineItems?.supportingItems?.find( return containsLineItemWithPartType(
(lineItem) => lineItem.partType.indexOf(partTypeStrings.RECALIBRATION) !== -1 partTypeStrings.RECALIBRATION,
this.$store.getters.order.lineItems?.supportingItems
);
},
shouldHideRecalibration() {
return this.getSettingValue(
experimentSettings.RECAL_PRICE_REMOVE
); );
if (recalLineItem) {
return true;
}
return false;
}, },
showApplePay() { showApplePay() {
return baseMixin.methods.showApplePay(); return baseMixin.methods.showApplePay();
@ -662,7 +666,7 @@ export default {
return true; return true;
} }
} else { } else {
if (this.isPiaEnabled && this.totalAmountDue > 0 && !this.hasRecal) { if (this.isPiaEnabled && this.totalAmountDue > 0 && !this.isRecalibrationOnOrder) {
return true; return true;
} }
} }
@ -768,7 +772,7 @@ export default {
} }
.questions-about-service { .questions-about-service {
.service-questions { .service-questions {
font-weight: $font-weight-bold; font-weight: 600;
} }
} }
.cart { .cart {

View file

@ -238,6 +238,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -281,6 +284,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -322,6 +328,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -360,6 +369,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -399,6 +411,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -438,6 +453,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -478,6 +496,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -520,6 +541,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -561,6 +585,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
@ -654,6 +681,9 @@ describe("quote.vue", () => {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],
}, },
applicationUser: {
experiments: [],
},
order: { order: {
lineItems: { lineItems: {
glassParts: ["item", "item2"], glassParts: ["item", "item2"],

View file

@ -42,15 +42,17 @@
</div> </div>
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
<afterpayModalBanner <afterpayModalBanner
v-if="!isInsuranceSelected && !isRecalibrationOnOrder" v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget" cmsWidgetName="AfterpayModalWidget"
:lineItems="lineItems" /> :lineItems="lineItems" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
<recalDisclaimer <recalDisclaimer
class="mb-0" class="mb-0"
cmsWidgetName="RecalDisclaimerWidget" cmsWidgetName="RecalDisclaimerWidget"
v-if="isRecalibrationOnOrder" v-if="showRecalDisclaimer"
v-on="{ textLinkClicked: openModalAction }" v-on="{ textLinkClicked: openModalAction }"
alertClass="alert-info" /> alertClass="alert-info" />
@ -99,6 +101,8 @@ import contentGroupModal from "@/fmg-components/content-group-modal/content-grou
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner"; import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner";
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue"; import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js"; import experimentMixin from "@/mixins/experiment-mixin.js";
import vehicleQuestionsMixin from "../../mixins/vehicle-questions-mixin"; import vehicleQuestionsMixin from "../../mixins/vehicle-questions-mixin";
@ -107,6 +111,8 @@ import { storeActions } from "@/constants/store-actions";
import { deepClone } from "@/helpers/object-helper"; import { deepClone } from "@/helpers/object-helper";
import store from "@/store"; import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { experimentUniverses, experimentSettings } from "@/constants/experiments";
import { getSessionKeyValue, getUserIdValue } from "@/helpers/heritage-integration/cookie-helper";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
@ -120,7 +126,6 @@ import {
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getQuerystringParameter } from "@/helpers/querystring-helper";
import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question"; import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question";
import { experimentSettings } from "@/constants/experiments";
import { partTypeStrings } from "@/constants/part-type-strings"; import { partTypeStrings } from "@/constants/part-type-strings";
import { containsLineItemWithPartType } from "@/helpers/service-package-helper"; import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
import { nextTick } from "vue"; import { nextTick } from "vue";
@ -133,6 +138,9 @@ export default {
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);
const experimentForLogging = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.RECAL_PRICE_REMOVAL
);
const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging( const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS, storeActions.GET_WIPERS,
{ {
@ -200,6 +208,33 @@ export default {
...servicePackageDiscountPart, ...servicePackageDiscountPart,
]; ];
// If the recal experiment is found then log the experiment exposure.
const isRecalibrationOnOrder = containsLineItemWithPartType(
partTypeStrings.RECALIBRATION,
availableLineItems
);
const canSafeliteRecalibrate = nullSafeGlassParts.some((glassPart) => {
return glassPart.partType === "WINDSHIELD" && glassPart.canSafeliteRecalibrate;
});
if (
experimentForLogging !== undefined &&
isRecalibrationOnOrder &&
canSafeliteRecalibrate
) {
// Log experiment exposure
baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.LOG_EXPERIMENT_EXPOSURE,
{
userId: getUserIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
experiment: experimentForLogging,
},
"quote",
false
);
}
// When calling pricing from the quote page, always use the cash parent account // When calling pricing from the quote page, always use the cash parent account
baseMixin.methods.dispatchStoreAction( baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PARENT_ACCOUNT_NUMBER, storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
@ -324,6 +359,18 @@ export default {
this?.availableLineItems this?.availableLineItems
); );
}, },
shouldHideRecalibration() {
const recalSettingValue = experimentMixin.methods.getSettingValue(
experimentSettings.RECAL_PRICE_REMOVE
);
return recalSettingValue;
},
showAfterpayBanner() {
return !this.isInsuranceSelected && (!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
},
showRecalDisclaimer() {
return this.isRecalibrationOnOrder && this.shouldHideRecalibration
},
}, },
methods: { methods: {
openModalAction(modalName) { openModalAction(modalName) {

View file

@ -104,7 +104,7 @@ export default {
}, },
filterOutRecalibration(lineItems) { filterOutRecalibration(lineItems) {
const filteredLineItems = lineItems.filter((item) => { const filteredLineItems = lineItems.filter((item) => {
return item.partType != partTypeStrings.RECALIBRATION; return !item.partType.includes(partTypeStrings.RECALIBRATION);
}); });
return filteredLineItems; return filteredLineItems;
}, },