DigitalConsumer.FixMyGlass/src/layouts/quote/quote.vue
CarlNation c41fe3fcfc CSR-2237
CSR-2237 found another cart price issue.  Salestax was showing $13 on an order that was pending coverage.  Changed salestax to return 0 for not verified insurance
2024-10-01 08:21:47 -04:00

671 lines
29 KiB
Vue

<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
<cashOrInsuranceQuestion
ref="cashOrInsurance"
cmsWidgetName="CashOrInsuranceQuestionWidget"
v-model="isInsuranceSelected"
groupName="CashOrInsuranceQuestion" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-12 col-xl-8">
<servicePackageQuestion
ref="servicePackage"
cashCmsWidgetName="CashServicePackageQuestionWidget"
insuranceCmsWidgetName="InsuranceServicePackageQuestionWidget"
groupName="ServicePackageQuestion"
:availableLineItems="availableLineItems"
:isInsuranceSelected="isInsuranceSelected"
:isRecalibrationOnOrder="isRecalibrationOnOrder"
:shouldHideRecalibration="shouldHideRecalibration"
@vapsItemsSelected="vapsItemsSelectedAction"
@servicePackageDiscountSelected="servicePackageDiscountSelectedAction"
:servicePackage="servicePackage"
:activePromos="lineItems.promos"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
isRequired />
</div>
</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="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="showRecalDisclaimer"
v-on="{ textLinkClicked: openModalAction }"
alertClass="alert-info" />
<promoModalQuestion
class="small mt-6 d-md-flex justify-content-md-center"
v-model="lineItems"
:addableVaps="addableVaps"
pageName="quote"
:taxPromos="false"
:useDefaultCashParentAccount="true"
modalWidgetName="PromoModalWidget" />
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="caption"
class="mt-6" />
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" isRecal />
<navbar
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import cashOrInsuranceQuestion from "./cash-or-insurance-question/cash-or-insurance-question";
import servicePackageQuestion from "./service-package-question/service-package-question";
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 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";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import { deepClone } from "@/helpers/object-helper";
import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { experimentSettings } from "@/constants/experiments";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate";
import { applicationConfig } from "@/constants/application-config";
import { payWithInsuranceStates } from "@/constants/insurance";
import {
revalidatePromosAndValidateQueryStringPromo,
buildToastMessagesFromRevalidateOrValidatePromoResponse,
createPromoSuccessAlert,
} from "@/helpers/promotions-helper";
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question";
import { partTypeStrings } from "@/constants/part-type-strings";
import {
containsLineItemWithPartType,
findLineItemsWithPartType,
} from "@/helpers/service-package-helper";
import { nextTick } from "vue";
import { packageNames } from "@/constants/package-names";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { containsRecalParts } from "@/helpers/recal-helper";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
const INSURANCE_TAB_TO_DISPLAY_THRESHOLD_DEFAULT = 300;
export default {
name: "quote",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
{
serviceZipCode: store.getters.order.serviceLocation.zipCode,
carId: store.getters.vehicle.carId,
},
"quote"
);
const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_DEFENSE,
null,
"quote"
);
const supportingItemsPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS,
null,
"quote"
);
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "wipers",
promise: wipersPromise,
},
{
resultKey: "rainDefense",
promise: rainDefensePromise,
},
{
resultKey: "supportingItems",
promise: supportingItemsPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.vaps = lineItems.vaps ?? [];
const nullSafeGlassParts = lineItems.glassParts ?? [];
const servicePackageDiscountSettingValue = experimentMixin.methods.getSettingValue(
experimentSettings.SERVICE_PACKAGE_DISCOUNT
);
const isServicePackageDiscount = servicePackageDiscountSettingValue === "True";
//Service package cash discount api call when experiment is active
var servicePackageDiscountPart = [];
if (isServicePackageDiscount) {
const servicePackageDiscountPartResponse =
await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SERVICE_PACKAGE_DISCOUNT_PART,
null,
"quote"
);
if (servicePackageDiscountPartResponse.data) {
servicePackageDiscountPart.push(servicePackageDiscountPartResponse.data);
}
}
const availableLineItems = [
resultMap.rainDefense,
...resultMap.supportingItems,
...resultMap.wipers,
...nullSafeGlassParts,
...servicePackageDiscountPart,
];
// When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards
var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber;
var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber;
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER,
false
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_BILL_TO_ACCOUNT_NUMBER,
applicationConfig.CASH_DEFAULT_BILL_TO_ACCOUNT_NUMBER,
false
);
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: availableLineItems,
},
"quote",
false
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS,
resultMap.supportingItems,
false
);
lineItems.supportingItems = resultMap.supportingItems;
const addableVaps = [...resultMap.wipers, resultMap.rainDefense];
// Promo logic
// Populate the previous state of promos for toast message usage in "next()"
const oldActivePromos = store.getters.lineItems.promos?.slice(0);
const oldInactivePromos = store.getters.order.payment.inactivePromos?.slice(0);
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
const { validatePromoResponse, revalidatePromoResponse } =
await revalidatePromosAndValidateQueryStringPromo(
promoCodeFromQueryString,
pricingResults,
"quote",
true
);
// Sync local promos with any new promos added by validate/revalidate
lineItems.promos = store.getters.lineItems.promos ?? [];
// End of promo logic
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.addableVaps = addableVaps;
vm.lineItems = lineItems;
vm.availableLineItems = pricingResults;
vm.holdParentAccountNumber = holdParentAccountNumber;
vm.holdBillToAccountNumber = holdBillToAccountNumber;
if (revalidatePromoResponse) {
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
revalidatePromoResponse,
oldActivePromos,
oldInactivePromos
);
revalidateAlerts.forEach((alert) => {
vm.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
});
}
if (validatePromoResponse) {
const validateAlerts =
buildToastMessagesFromRevalidateOrValidatePromoResponse(validatePromoResponse);
vm.$refs.funnelHeader.pushGlobalAlert(
validateAlerts[0],
validateAlerts[0].shouldAutoFade
);
}
// This is a safeguard to prevent verified insurance from seeing quote page again.
const payment = store.getters.payment;
if (payment?.isInsurance && payment?.insuranceCoverage?.isVerified) {
navigateToHeritageFunnel({ shouldSaveSession: true, pageNameToLog: "quote" });
}
const getIsInsuranceSelectedValue = (availableLineItems, insuranceThreshold) => {
const serviceLocationState = store.getters.order.serviceLocation.state;
// Override if coming back from QuoteDetails. Remove override after Quote release
const isInsuranceOverrideValue = to.query?.isInsurance;
const defaultIsInsuranceSelectedValue = store.getters.order.payment.isInsurance;
const hideRecalCost =
experimentMixin.methods
.getSettingValue(experimentSettings.RECAL_PRICE_REMOVE)
?.toLowerCase() === "true";
//Default to Insurance Tab if user Service zip is from certain States
if (
serviceLocationState != null &&
payWithInsuranceStates.find((item) => item === serviceLocationState)
)
return true;
else if (isInsuranceOverrideValue != null) {
return isInsuranceOverrideValue == "true";
} else if (defaultIsInsuranceSelectedValue != null) {
return defaultIsInsuranceSelectedValue;
} else {
if (availableLineItems) {
const lineItemsForCalculatingPrice = hideRecalCost
? baseMixin.methods.filterOutFees(
baseMixin.methods.filterOutRecalibration(availableLineItems) // Strip out recal before filtering out fees
)
: baseMixin.methods.filterOutFees(availableLineItems);
return (
baseMixin.methods.getTierOnePackagePrice(lineItemsForCalculatingPrice) >
insuranceThreshold
); // compare base price vs arbitrary threshold (representing insurance price)
} else {
return null;
}
}
};
let thresholdToUse = INSURANCE_TAB_TO_DISPLAY_THRESHOLD_DEFAULT;
let internalThreshold = experimentMixin.methods.getSettingValue(
experimentSettings.INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL
);
if (!store.getters.externalParameterState?.isExternalParameter) {
// there are no external parameters
if (internalThreshold) thresholdToUse = internalThreshold;
vm.isInsuranceSelected = getIsInsuranceSelectedValue(
vm.availableLineItems,
thresholdToUse
);
baseMixin.methods.ResetExternalParamsAndHideModal();
} else {
// there ARE external parameters
if (store.getters.externalParameterQuote.isInsurance == true) {
// did user intentionally select insurance?
vm.isInsuranceSelected = true;
vm.servicePackage = store.getters.externalParameterQuote.servicePackage;
await nextTick();
const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm);
if (isValid) {
vm.forwardButtonAction();
} else {
baseMixin.methods.ResetExternalParamsAndHideModal();
}
} else {
// did user come from external source (LeadGen)?
let externalSource = store.getters.externalParameterSource
? store.getters.externalParameterSource
: null;
// Business logic to determine what "external" source is
if (externalSource?.includes("LeadGen")) {
const externalThreshold = experimentMixin.methods.getSettingValue(
experimentSettings.INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL
);
if (externalThreshold) thresholdToUse = externalThreshold;
} else {
if (internalThreshold) thresholdToUse = internalThreshold;
}
vm.isInsuranceSelected = getIsInsuranceSelectedValue(
vm.availableLineItems,
thresholdToUse
);
baseMixin.methods.ResetExternalParamsAndHideModal();
}
}
});
},
data() {
return {
isInsuranceSelected: null,
availableLineItems: null,
lineItems: [],
addableVaps: [],
servicePackage: null,
holdParentAccountNumber: null,
holdBillToAccountNumber: null,
};
},
computed: {
lineItemsCloneForWatcher() {
return Object.assign({}, this.lineItems);
},
isRecalibrationOnOrder() {
return containsRecalParts(this.lineItems);
},
isServicePackageDiscountOnOrder() {
return containsLineItemWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.availableLineItems
);
},
servicePackageDiscountParts() {
return findLineItemsWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.availableLineItems
);
},
shouldHideRecalibration() {
return (
this.getSettingValue(experimentSettings.RECAL_PRICE_REMOVE)?.toLowerCase() ===
"true" && this.isRecalibrationOnOrder
);
},
showAfterpayBanner() {
return (
!this.isInsuranceSelected &&
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
);
},
showRecalDisclaimer() {
return this.isRecalibrationOnOrder && this.shouldHideRecalibration;
},
},
methods: {
openModalAction(modalName) {
this.$refs[modalName].openModal();
},
arePagePrerequisitesValid() {
const payment = store.getters.order.payment;
const log = getQuerystringParameter(queryStrings.LOG);
if (eval(log)) {
console.log("------------- quote.vue pagePrereqs start -----------------");
console.log(
new Date() +
" store.getters.order.serviceLocation.zipCode: " +
store.getters.order.serviceLocation?.zipCode
);
console.log(
new Date() +
" store.getters.order.serviceLocation.zipCodeCtu: " +
store.getters.order.serviceLocation?.zipCodeCtu
);
console.log(
new Date() +
" store.getters.order.damage.isRepair: " +
store.getters.order.damage?.isRepair
);
console.log(
new Date() +
" store.getters.order.lineItems?.glassParts: " +
JSON.stringify(store.getters.order.lineItems?.glassParts)
);
console.log(
new Date() +
" store.getters.order.payment.insuranceCoverage?.isVerified: " +
payment?.insuranceCoverage?.isVerified
);
console.log("----------------- quote.vue pagePrereqs end -----------------");
}
return (
store.getters.order.serviceLocation.zipCode &&
store.getters.order.serviceLocation.zipCodeCtu &&
(store.getters.order.damage.isRepair ||
(store.getters.order.lineItems?.glassParts != null &&
store.getters.order.lineItems.glassParts.length > 0)) &&
(payment.insuranceCoverage?.isVerified == null ||
payment.insuranceCoverage?.isVerified === false)
);
},
vapsItemsSelectedAction(vapsItemsSelected) {
this.lineItems.vaps = vapsItemsSelected;
},
servicePackageDiscountSelectedAction(supportingItemsSelected) {
this.lineItems.supportingItems = supportingItemsSelected;
},
backButtonAction() {
vehicleQuestionsMixin.methods.navigateBack(this);
},
forwardButtonAction() {
this.dispatchStoreAction(
this.storeActions.SAVE_PAYMENT_TYPE,
this.isInsuranceSelected,
false
);
if (!this.isInsuranceSelected) {
this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER,
false
);
this.dispatchStoreAction(
this.storeActions.SAVE_BILL_TO_ACCOUNT_NUMBER,
applicationConfig.CASH_DEFAULT_BILL_TO_ACCOUNT_NUMBER,
false
);
} else {
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
this.holdParentAccountNumber,
false
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_BILL_TO_ACCOUNT_NUMBER,
this.holdBillToAccountNumber,
false
);
}
if (
this.$store.getters.order.payment.parentAccountNumber !=
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER
) {
this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems);
}
if (this.lineItems.glassParts?.length > 0) {
this.dispatchStoreAction(
this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
this.lineItems.glassParts,
false
);
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
this.lineItems.supportingItems,
false
);
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.lineItems.vaps, false);
this.dispatchStoreAction(
storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS,
{
activePromos: this.lineItems.promos,
},
false
);
const payment = this.$store.getters.payment;
if (payment.isInsurance) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_INSURANCE,
this.$route
);
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_CASH,
this.$route
);
}
},
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
if (!this.isInsuranceSelected && this.lineItems.vaps?.length == 0) {
const tierOnePrice =
this.lineItems.promos?.length > 0
? this.$refs.servicePackage.getDiscountedPackagePriceString(
packageNames.TIER_ONE,
false
)
: this.$refs.servicePackage.getPackagePriceString(
packageNames.TIER_ONE
);
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.CASH_QUOTE_DISPLAYED,
tierOnePrice,
true
);
}
});
this.$nextTick(() => {
const availablePackageNames = this.$refs.servicePackage.servicePackageAnswers;
var eventLabel = "";
availablePackageNames?.forEach((tier) => {
if (tier) {
let lineItemsToPrice = this.availableLineItems;
if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) {
lineItemsToPrice =
baseMixin.methods.filterOutRecalibration(lineItemsToPrice);
}
if (this.isServicePackageDiscountOnOrder) {
lineItemsToPrice = lineItemsToPrice?.filter((item) => {
return (
item.partType != this.servicePackageDiscountParts[0].partType
);
});
}
let price = 0;
if (tier.value == "TierTwo" && this.isServicePackageDiscountOnOrder) {
lineItemsToPrice.push(...this.servicePackageDiscountParts);
}
price += baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(lineItemsToPrice)
);
price += this.$refs.servicePackage.getVapsPrice(tier.value);
const formattedPrice = parseFloat(price).toFixed(2);
eventLabel += tier.value + "_" + formattedPrice;
}
eventLabel += ",";
});
eventLabel = eventLabel.slice(0, -1);
this.pushEventToGA("quote", this.GaActions.SERVICE_PACKAGE_PRICE, eventLabel, true);
});
},
},
mounted() {
this.attachCustomEvents();
},
watch: {
lineItemsCloneForWatcher: {
handler(newValue, oldValue) {
if (
!oldValue ||
oldValue.length == 0 ||
!oldValue.vaps ||
!newValue ||
newValue.length == 0
) {
return;
}
if (oldValue.promos.length < newValue.promos.length) {
const oldPromoCodes = oldValue.promos.map(
(promoObject) => promoObject.promoCode
);
const newlyActivatedPromoCodes = newValue.promos.filter(
(newPromo) => !oldPromoCodes.includes(newPromo.promoCode)
);
const alert = createPromoSuccessAlert(newlyActivatedPromoCodes[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
}
},
deep: true,
},
},
components: {
funnelHeader,
navbar,
vehicleBanner,
funnelSubHeader,
Form,
textBlock,
cashOrInsuranceQuestion,
servicePackageQuestion,
contentGroupModal,
loadingModal,
afterpayModalBanner,
promoModalQuestion,
recalDisclaimer,
},
};
</script>
<style>
.funnel-sub-header {
p {
font-size: 1rem;
}
}
.text-block {
display: block;
}
</style>