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 = {
CONCEPT_FUNNEL: "ConceptFunnel",
RECAL_PRICE_REMOVAL: "NextGen_RecalPriceRemoval",
};
const experimentSettings = {
@ -11,6 +12,7 @@ const experimentSettings = {
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional",
SERVICE_PACKAGE_DISCOUNT: "OfferServicePackageDiscount",
RECAL_PRICE_REMOVE: "RecalPriceRemove",
};
const experimentTriggers = {

View file

@ -1,10 +1,9 @@
<template>
<div class="cart">
<div class="px-0">
<!-- set class to 'expaned' on line 7 so cart is open on page load. This may be temporary -->
<div
class="row vin-toggle flex align-items-center pt-4"
:class="[isExpanded ? '' : 'expanded']"
:class="[isExpanded ? 'expanded' : '']"
@click="toggleIsExpanded()">
<a
aria-label="expand cart"
@ -188,10 +187,11 @@ export default {
insuranceDeductible: Number,
insuranceCompanyName: String,
showInsuranceCoverageAs: String,
shouldHideRecalibration: Boolean,
},
data() {
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", {
style: "currency",
currency: "USD",
@ -303,12 +303,11 @@ export default {
},
lineItemsWithoutRecal() {
if (!this.lineItems || this.lineItems.length < 1) return;
if (!this.lineItems.supportingItems) return this.lineItems;
const lineItemsCopy = deepClone(this.lineItems);
lineItemsCopy.supportingItems = baseMixin.methods.filterOutRecalibration(
lineItemsCopy.supportingItems
);
lineItemsCopy.supportingItems = lineItemsCopy.supportingItems ? baseMixin.methods.filterOutRecalibration(
lineItemsCopy?.supportingItems
) : [];
return lineItemsCopy;
},
showCoverageAsPending() {
@ -439,14 +438,13 @@ export default {
packagePrice() {
let packagePrice = 0;
if (!this.isInsurance) {
// CASH ONLY
if (!this.isInsurance && this.shouldHideRecalibration) { // Update packagePrice if Cash only && is part of RemoveRecal experiment
packagePrice = baseMixin.methods.getTierOnePackagePrice(
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(
baseMixin.methods.filterOutFees(this.availableLineItems)
);
@ -1001,22 +999,22 @@ export default {
},
subTotal() {
if (!this.lineItems || this.lineItems.length < 1) return;
return this.isInsurance
? baseMixin.methods.getSubTotal(this.lineItems)
: baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal);
return this.isInsurance || !this.shouldHideRecalibration
? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
: baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculated without recal
},
salesTax() {
if (!this.lineItems || this.lineItems.length < 1) return;
return this.isInsurance
? baseMixin.methods.getSalesTax(this.lineItems)
: baseMixin.methods.getSalesTax(this.lineItemsWithoutRecal);
return this.isInsurance || !this.shouldHideRecalibration
? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
: baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculated without recal
},
amountDue() {
if (!this.lineItems || this.lineItems.length < 1) return;
if (this.showAsPaid) return 0;
return this.isInsurance
? baseMixin.methods.getAmountDue(this.lineItems)
: baseMixin.methods.getAmountDue(this.lineItemsWithoutRecal);
return this.isInsurance || !this.shouldHideRecalibration
? baseMixin.methods.getSubTotal(this.lineItems) // calculate with recal (if on order)
: baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); // calculated without recal
},
amountPaid() {
if (!this.showAsPaid) {

View file

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

View file

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

View file

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

View file

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

View file

@ -42,15 +42,17 @@
</div>
<div class="row justify-content-center">
<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
v-if="!isInsuranceSelected && !isRecalibrationOnOrder"
v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget"
:lineItems="lineItems" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
<recalDisclaimer
class="mb-0"
cmsWidgetName="RecalDisclaimerWidget"
v-if="isRecalibrationOnOrder"
v-if="showRecalDisclaimer"
v-on="{ textLinkClicked: openModalAction }"
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 afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-modal-banner";
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
import experimentMixin from "@/mixins/experiment-mixin.js";
import vehicleQuestionsMixin from "../../mixins/vehicle-questions-mixin";
@ -107,6 +111,8 @@ import { storeActions } from "@/constants/store-actions";
import { deepClone } from "@/helpers/object-helper";
import store from "@/store";
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 { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate";
@ -120,7 +126,6 @@ import {
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question";
import { experimentSettings } from "@/constants/experiments";
import { partTypeStrings } from "@/constants/part-type-strings";
import { containsLineItemWithPartType } from "@/helpers/service-package-helper";
import { nextTick } from "vue";
@ -133,6 +138,9 @@ export default {
async beforeRouteEnter(to, from, next) {
// Call APIs
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(
storeActions.GET_WIPERS,
{
@ -200,6 +208,33 @@ export default {
...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
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
@ -324,6 +359,18 @@ export default {
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: {
openModalAction(modalName) {

View file

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