Merge branch 'release/2025.11.20' into rlsmerge/2025.11.20-to-develop

This commit is contained in:
CarlNation 2025-11-14 09:24:04 -05:00
commit 3f27c2205f
19 changed files with 671 additions and 324 deletions

View file

@ -114,7 +114,7 @@ body {
debug: true, debug: true,
}, },
{ {
name: 'Test', name: 'SysTest',
hostName: 'www-test2.safelite.com', hostName: 'www-test2.safelite.com',
apiHostname: 'digitalapi.test.safelite.io', apiHostname: 'digitalapi.test.safelite.io',
debug: true, debug: true,
@ -142,83 +142,178 @@ body {
return match; return match;
} }
function getExperimentSettingsFromVuex(vuexData) {
const experiments = vuexData?.applicationUser?.experiments;
if(experiments) {
return experiments
.filter((e) => !!e.isActive)
.map((e) => e.settings)
.reduce((prev, next) => Object.assign(prev, next), {}) ?? {};
}
return {};
}
function extractDataFromVuex(existingVuexData) {
let bailoutInfo = [];
let headerInfo = [];
try {
// =============== Collect diagnostic data:
bailoutInfo = [];
// App & Site Name
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
// Not included (yet) from heritage:
// - Site ID
// - Code
// - Module
// - Page
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
// - URL
// - Server
const sessionId = getCookieValueByName('sid');
bailoutInfo.push({ name: 'Session Id', value: sessionId });
const userAgent = window.navigator.userAgent;
bailoutInfo.push({ name: 'User Agent', value: userAgent });
// - IP
// - Session Log Sequence Number
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
// - Referral Insured Zip
// - Referral Insured Service Zip
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
const vehicle = existingVuexData.order?.vehicle;
const vehicleString = vehicle?.year
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
: null;
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
const environmentData = getCurrentEnvironmentData();
const userIdCookieName = `FunnelUserId-${environmentData?.name}`;
bailoutInfo.push({ name: 'User Id', value: getCookieValueByName(userIdCookieName) });
// - Parent Account Name
// - Client GUID
// =============== Collect header data:
headerInfo = [];
headerInfo.push({ name: 'X-Experiment-Data', value: JSON.stringify(getExperimentSettingsFromVuex(existingVuexData)) });
headerInfo.push({ name: 'X-Application-Name', value: 'FixMyGlass' });
const sessionKeyCookieName = `FunnelSessionKey-${environmentData?.name}`;
headerInfo.push({ name: 'X-Session-Sequence-Number', value: getCookieValueByName(sessionKeyCookieName) });
headerInfo.push({ name: 'X-Referral-Sequence-Number', value: existingVuexData?.order?.referralSequenceNumber });
headerInfo.push({ name: 'X-Enterprise-Order-Number', value: existingVuexData?.order?.eon });
headerInfo.push({ name: 'log-enabled', value: existingVuexData?.applicationUser?.loggingOption ?? false });
} finally {
return {
headerInfo: headerInfo,
bailoutInfo: bailoutInfo
};
}
}
function generateRequestHeader(headerInfo) {
let headers = {};
headerInfo.forEach(
keyPair => {
headers[keyPair.name] = keyPair.value;
}
);
headers['X-Transaction-Id'] = crypto.randomUUID();
headers['Content-Type'] = 'application/json';
return headers;
}
function getFieldWithName(info, name) {
const match = info?.find(
(row) => row.name === name
);
return match?.value;
}
function clearApplicationData() {
window.localStorage.removeItem('vuex');
const environmentData = getCurrentEnvironmentData();
const cookieNames = [
`FunnelUserId-${environmentData?.name}`,
`FunnelSessionKey-${environmentData?.name}`,
`FunnelSessionInfo-${environmentData?.name}`,
`sid`,
`dxdev`,
];
cookieNames.forEach(
(cookieName) => {
let cookieToAdd = `${cookieName}=undefined; path=/; domain=${environmentData.hostName}; max-age=0`;
document.cookie = cookieToAdd;
}
)
}
window.onload = async () => { window.onload = async () => {
// =============== Check for session info: // =============== Check for session info:
const existingVuexDataJSON = window.localStorage.getItem('vuex'); const existingVuexDataJSON = window.localStorage.getItem('vuex');
const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null; const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null;
const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo'); const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo');
const existingBailoutInfo = existingBailoutInfoJSON ? JSON.parse(existingBailoutInfoJSON) : null; const existingBailoutInfo = existingBailoutInfoJSON ? JSON.parse(existingBailoutInfoJSON) : null;
const existingHeaderInfoJSON = window.sessionStorage.getItem('headerInfo');
const existingHeaderInfo = existingHeaderInfoJSON ? JSON.parse(existingHeaderInfoJSON) : null;
console.log(`================ VUEX DATA`); console.log(`================ VUEX DATA`);
console.log(existingVuexData); console.log(existingVuexData);
console.log(`================ PRIOR BAILOUT DATA`); console.log(`================ PRIOR BAILOUT DATA`);
console.log(existingBailoutInfo); console.log(existingBailoutInfo);
console.log(`================ PRIOR HEADER DATA`);
console.log(existingHeaderInfo);
let bailoutInfo = null; let bailoutInfo = null;
let headerInfo = null;
if(existingVuexData) { if(existingVuexData) {
try { const results = extractDataFromVuex(existingVuexData);
// =============== Collect diagnostic data: bailoutInfo = results.bailoutInfo;
bailoutInfo = []; headerInfo = results.headerInfo;
// App & Site Name clearApplicationData();
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
// Not included (yet) from heritage: if(bailoutInfo) {
// - Site ID window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
// - Code
// - Module
// - Page
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
// - URL
// - Server
const sessionId = getCookieValueByName('sid');
bailoutInfo.push({ name: 'Session Id', value: sessionId });
const userAgent = window.navigator.userAgent;
bailoutInfo.push({ name: 'User Agent', value: userAgent });
// - IP
// - Session Log Sequence Number
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
// - Referral Insured Zip
// - Referral Insured Service Zip
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
const vehicle = existingVuexData.order?.vehicle;
const vehicleString = vehicle?.year
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
: null;
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
// - Parent Account Name
// - Client GUID
} finally {
// If any information gathered, write to session storage.
if(bailoutInfo) {
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
}
// Then *always* clear vuex data.
window.localStorage.removeItem('vuex');
} }
} else if(existingBailoutInfo) {
// Proceed with prior data. if(headerInfo) {
bailoutInfo = existingBailoutInfo; window.sessionStorage.setItem('headerInfo', JSON.stringify(headerInfo));
}
} else {
bailoutInfo = existingBailoutInfo ?? [];
headerInfo = existingHeaderInfo ?? [];
} }
const environmentInfo = getCurrentEnvironmentData(); const environmentInfo = getCurrentEnvironmentData();
@ -275,11 +370,11 @@ body {
); );
const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`; const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`;
const headers = generateRequestHeader(headerInfo);
const request = new Request(endpointUrl, { const request = new Request(endpointUrl, {
method: 'POST', method: 'POST',
headers: { headers: headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
entry: entryString, entry: entryString,
}), }),
@ -292,6 +387,38 @@ body {
console.error(`=== ERROR SENDING LOGGING`); console.error(`=== ERROR SENDING LOGGING`);
console.error(e); console.error(e);
} }
try {
const endpointUrl = `https://${apiHostname}/analytics/api/v1/analytics/log-page-view`;
const headers = generateRequestHeader(headerInfo);
const payload = {
AppName: "FixMyGlass",
action: "",
applicationName: "FixMyGlassNextGen",
event: "ENTRY",
experimentsForUser: [],
pageName: "static-error",
parentAccountNumber: getFieldWithName(bailoutInfo, 'Parent Account Number') ?? 0,
referralSequenceNumber: getFieldWithName(bailoutInfo, 'Referral Seq Number'),
sessionId: getFieldWithName(bailoutInfo, 'Session Id'),
sessionKey: getFieldWithName(headerInfo, 'X-Session-Sequence-Number'),
shouldUseSessionId: false,
userId: getFieldWithName(bailoutInfo, 'User Id'),
};
const request = new Request(endpointUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
});
const response = await fetch(request);
} catch(e) {
console.error(`=== ERROR SENDING PAGEVIEW`);
console.error(e);
}
} }
}; };
</script> </script>

View file

@ -3,7 +3,7 @@ const cartItemCategories = {
GLASS_PARTS: "glassParts", GLASS_PARTS: "glassParts",
SUPPORTING_ITEMS: "supportingItems", SUPPORTING_ITEMS: "supportingItems",
PROMOS: "promos", PROMOS: "promos",
SERVICE_PACKAGE_DISCOUNT: "servicePackageDiscount", QUOTE_PAGE_DISCOUNT: "quotePageDiscount",
}; };
export { cartItemCategories }; export { cartItemCategories };

View file

@ -10,7 +10,7 @@ const cartItemTypes = {
PROMOS: "PROMOS", PROMOS: "PROMOS",
EARLY_BIRD: "EARLY BIRD", EARLY_BIRD: "EARLY BIRD",
SUPPLIES_REPAIR: "SUPPLIES-REPAIR", SUPPLIES_REPAIR: "SUPPLIES-REPAIR",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION", DONATION: "DONATION",
}; };

View file

@ -10,6 +10,7 @@ const partTypeStrings = {
REPAIR_FEE: "REPAIR FEE", REPAIR_FEE: "REPAIR FEE",
EARLY_BIRD: "EARLY BIRD", EARLY_BIRD: "EARLY BIRD",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION", DONATION: "DONATION",
}; };

View file

@ -0,0 +1,80 @@
import { packageNames } from "./package-names";
import { partTypeStrings } from "./part-type-strings";
export const quotePageDiscountTable = [
// Repair
{
name: "Repair Glass Only",
experimentCode: "Show_GlassOnly_Repair_Discount",
partNumber: "GL RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Repair Standard",
experimentCode: "Show_Standard_Repair_Discount",
partNumber: "STD RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Repair Premium",
experimentCode: "Show_Premium_Repair_Discount",
partNumber: "PRM RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - No recal
{
name: "Replace Glass Only",
experimentCode: "Show_GlassOnly_Replace_Discount",
partNumber: "GL RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Replace Standard",
experimentCode: "Show_Standard_Replace_Discount",
partNumber: "STD RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Replace Premium",
experimentCode: "Show_Premium_Replace_Discount",
partNumber: "PRM RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - with recal
{
name: "Recal Glass Only",
experimentCode: "Show_GlassOnly_Recal_Discount",
partNumber: "GL RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Recal Standard",
experimentCode: "Show_Standard_Recal_Discount",
partNumber: "STD RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Recal Premium",
experimentCode: "Show_Premium_Recal_Discount",
partNumber: "PRM RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
];

View file

@ -33,6 +33,7 @@
<div class="modal-footer"> <div class="modal-footer">
<slot name="modal-footer-slot"></slot> <slot name="modal-footer-slot"></slot>
<modalButtonMain <modalButtonMain
v-if="!isFooterButtonSuppressed"
:isPrimary="isFooterButtonPrimary" :isPrimary="isFooterButtonPrimary"
class="w-100 modal-footer-button" class="w-100 modal-footer-button"
:id="modalId + '-modalbtn'" :id="modalId + '-modalbtn'"
@ -67,6 +68,7 @@ export default {
staticBackdrop: Boolean, staticBackdrop: Boolean,
footerButtonDisabled: Boolean, footerButtonDisabled: Boolean,
isFooterButtonPrimary: Boolean, isFooterButtonPrimary: Boolean,
isFooterButtonSuppressed: Boolean,
onModalOpenedCallback: { onModalOpenedCallback: {
type: Function, type: Function,
}, },

View file

@ -145,9 +145,7 @@ export default {
return "$" + decimalPrice.toFixed(2); return "$" + decimalPrice.toFixed(2);
}, },
hasPackageDiscount() { hasPackageDiscount() {
return ( return this.additionalButtonData.hasDiscount && this.additionalButtonData.Text;
this.additionalButtonData.servicePackageDiscount && this.additionalButtonData.Text
);
}, },
}, },
}; };

View file

@ -4,11 +4,11 @@
class="package-label pricing-by-day-pkg-lbl" class="package-label pricing-by-day-pkg-lbl"
:class="[ :class="[
this.buttonLabelSubCopy ? 'has-subheader' : '', this.buttonLabelSubCopy ? 'has-subheader' : '',
!this.additionalButtonData.servicePackageDiscount ? 'adjust-top' : '', !this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
]" ]"
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.hasDiscount" class="row">
<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>
@ -84,10 +84,7 @@
</div> </div>
<div <div
v-if=" v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
class="special-save-box"> class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span> <span v-html="this.additionalButtonData.Text"></span>
</div> </div>

View file

@ -190,6 +190,7 @@ import { cartItemCategories } from "@/constants/cart-item-categories";
import { cartItemTypes } from "@/constants/cart-item-types"; import { cartItemTypes } from "@/constants/cart-item-types";
import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance"; import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { quotePageDiscountTable } from "@/constants/quote-page-discounts";
export default { export default {
name: "cart", name: "cart",
@ -256,11 +257,11 @@ export default {
return subTotal; return subTotal;
}, },
getServicePackageDiscount() { getQuotePageDiscount() {
const servicePackageDiscountLineItems = this.servicePackageDiscountCartItem; const quotePageDiscountLineItems = this.quotePageDiscountCartItem;
let subTotal = 0; let subTotal = 0;
subTotal += servicePackageDiscountLineItems?.subTotal ?? 0; subTotal += quotePageDiscountLineItems?.subTotal ?? 0;
return subTotal; return subTotal;
}, },
getVapsCartItemsForSelectedPackage(packageName) { getVapsCartItemsForSelectedPackage(packageName) {
@ -303,7 +304,8 @@ export default {
getLineItemAmount(amount, useVerifyingText, category) { getLineItemAmount(amount, useVerifyingText, category) {
if (useVerifyingText) return this.verifyingCoverageText; if (useVerifyingText) return this.verifyingCoverageText;
return category == "promos" || category == "servicePackageDiscount" return category == cartItemCategories.PROMOS ||
category == cartItemCategories.QUOTE_PAGE_DISCOUNT
? "(" + this.currencyFormatter.format(amount * -1) + ")" ? "(" + this.currencyFormatter.format(amount * -1) + ")"
: this.currencyFormatter.format(amount); : this.currencyFormatter.format(amount);
}, },
@ -316,16 +318,21 @@ export default {
if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) { if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) {
this.saveVaps(this.lineItems); this.saveVaps(this.lineItems);
// Check if service package discount should be removed after vaps change // Check if service package discount should be removed after vaps change
if ( if (this.quotePageDiscountCartItem) {
this.servicePackageDiscountCartItem && const existingLineItem = this.quotePageDiscountCartItem.lineItems.at(0);
this.discountPackageNames != this.packageLevel const discountInfo = quotePageDiscountTable.find(
) { (info) => info.partNumber === existingLineItem?.partNumber
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.servicePackageDiscountCartItem.cartItemType
); );
shouldSaveSupportingItems = true; const packageLevelForDiscount = discountInfo?.packageLevel;
if (this.packageLevel !== packageLevelForDiscount) {
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.quotePageDiscountCartItem.cartItemType
);
shouldSaveSupportingItems = true;
}
} }
} }
if (shouldSaveSupportingItems) { if (shouldSaveSupportingItems) {
@ -483,8 +490,8 @@ export default {
cartItems.push(this.mobileFeeCartItem); cartItems.push(this.mobileFeeCartItem);
} }
if (this.servicePackageDiscountCartItem) { if (this.quotePageDiscountCartItem) {
cartItems.push(this.servicePackageDiscountCartItem); cartItems.push(this.quotePageDiscountCartItem);
} }
if (this.premiumAppointmentDiscountCartItem) { if (this.premiumAppointmentDiscountCartItem) {
@ -500,12 +507,6 @@ export default {
return cartItems; return cartItems;
}, },
}, },
discountPackageNames() {
const discountServicePackage = experimentMixin.methods.getSettingValue(
experimentSettings.PROMO_ON_PACKAGE
);
return getDiscountedPackageName(discountServicePackage);
},
servicePackageTitleWidget() { servicePackageTitleWidget() {
const servicePackageNames = this.getCmsContent( const servicePackageNames = this.getCmsContent(
this.servicePackageOptionsCmsName, this.servicePackageOptionsCmsName,
@ -550,8 +551,8 @@ export default {
} }
packagePrice += this.getVapsPrice(this.packageLevel); packagePrice += this.getVapsPrice(this.packageLevel);
if (this.servicePackageDiscountCartItem) { if (this.quotePageDiscountCartItem) {
packagePrice -= this.getServicePackageDiscount(); packagePrice -= this.getQuotePageDiscount();
} }
return packagePrice; return packagePrice;
@ -928,25 +929,25 @@ export default {
} }
return cartItem; return cartItem;
}, },
servicePackageDiscountCartItemName() { quotePageDiscountCartItemName() {
return this.getCmsContent("ServicePackageDiscountTextWidget", "Text"); return this.getCmsContent("ServicePackageDiscountTextWidget", "Text");
}, },
servicePackageDiscountCartItem() { quotePageDiscountCartItem() {
let cartItem = null; let cartItem = null;
const servicePackageDiscountLineItem = this.supportingItems.find( const quotePageDiscountLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT (lineItem) => lineItem.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT
); );
if (servicePackageDiscountLineItem) { if (quotePageDiscountLineItem) {
cartItem = { cartItem = {
name: name:
this.servicePackageDiscountCartItemName + this.quotePageDiscountCartItemName +
Math.abs( Math.abs(
baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem) baseMixin.methods.getTotalLineItemPrice(quotePageDiscountLineItem)
), ),
category: cartItemCategories.SERVICE_PACKAGE_DISCOUNT, category: cartItemCategories.QUOTE_PAGE_DISCOUNT,
cartItemType: cartItemTypes.SERVICE_PACKAGE_DISCOUNT, cartItemType: cartItemTypes.QUOTE_PAGE_DISCOUNT,
isDisplayed: true, isDisplayed: true,
isRemovable: false, isRemovable: false,
subTotal: 0, subTotal: 0,
@ -955,15 +956,15 @@ export default {
isCoveredByInsurance: false, isCoveredByInsurance: false,
}; };
servicePackageDiscountLineItem.cartItemType = cartItem.cartItemType; quotePageDiscountLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(servicePackageDiscountLineItem); cartItem.lineItems.push(quotePageDiscountLineItem);
cartItem.subTotal += cartItem.subTotal +=
(servicePackageDiscountLineItem.kitPrice ?? 0) + (quotePageDiscountLineItem.kitPrice ?? 0) +
(servicePackageDiscountLineItem.laborAmount ?? 0) + (quotePageDiscountLineItem.laborAmount ?? 0) +
(servicePackageDiscountLineItem.sellingPrice ?? 0); (quotePageDiscountLineItem.sellingPrice ?? 0);
cartItem.salesTax += servicePackageDiscountLineItem.salesTax ?? 0; cartItem.salesTax += quotePageDiscountLineItem.salesTax ?? 0;
} }
return cartItem; return cartItem;
@ -972,7 +973,7 @@ export default {
let cartItem = null; let cartItem = null;
const otherSupportingItems = this.supportingItems.filter((item) => { const otherSupportingItems = this.supportingItems.filter((item) => {
return item.partType != partTypeStrings.SERVICE_PACKAGE_DISCOUNT; return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
}); });
// Don't include fees they are part of the package total // Don't include fees they are part of the package total
let otherSupportingItemsLineItems = let otherSupportingItemsLineItems =

View file

@ -1,10 +1,5 @@
<template> <template>
<div id="afterpay-banner" role="alert"> <div class="afterpay-modal-banner" role="alert">
<component
:is="'script'"
src="https://js.squarecdn.com/square-marketplace.js"
async></component>
<div> <div>
<span <span
class="callout" class="callout"
@ -22,22 +17,52 @@
<span v-else v-html="token"></span> <span v-else v-html="token"></span>
<span class="nbsp">&nbsp;</span> <span class="nbsp">&nbsp;</span>
</span> </span>
<a <textLink
id="afterpay-learnmore" linkType="text"
href="#" :text="bannerCta"
data-afterpay-modal="en_US" href="javascript:void(0)"
data-bind="click:afterpayLearnMore"> @click-event="openModal" />
{{ modalCopy }}
</a>
</div> </div>
</div> </div>
<modal
:ref="modalName"
class="afterpay-modal"
:headerText="modalHeaderText"
:isFooterButtonSuppressed="true">
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot>
<span class="afterpay-sections">
<span
v-for="section in afterpaySectionsContent"
:key="section"
class="afterpay-section">
<span class="section-header">
<img v-if="section.AnswerImageUrl" :src="section.AnswerImageUrl" />
<h3 v-if="section.Text" v-html="section.Text"></h3>
</span>
<span v-html="section.content"></span>
</span>
</span>
</template>
<template v-slot:modal-footer-slot>
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
</template>
</modal>
</template> </template>
<script> <script>
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper"; import {
import baseMixin from "@/mixins/base-mixin.js"; doesCopyContainTextLink,
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper"; splitCopyOnCMSPlaceHolder,
import { getArrayOfAllLineItemsAndChildParts } from "@/store"; getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
splitCMSCopyOnBR,
} from "@/helpers/cms-content-helper";
import textLink from "@/ux-components/text-link/text-link";
import modal from "@/digital-components/modal/modal";
const INLINE_IMAGE_TOKEN = "custom:inlineImage"; const INLINE_IMAGE_TOKEN = "custom:inlineImage";
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice"; const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
@ -48,20 +73,16 @@ function isInlineImageToken(token) {
function isAfterpayPriceToken(token) { function isAfterpayPriceToken(token) {
return token.includes(AFTERPAY_PRICE_TOKEN); return token.includes(AFTERPAY_PRICE_TOKEN);
} }
function getInlineAltText(token) { function getInlineAltText(token) {
const innerTokens = token.split(","); const innerTokens = token.split(",");
return innerTokens[1] ?? ""; return innerTokens[1] ?? "";
} }
export default { export default {
name: "afterpay-modal-banner", name: "afterpay-banner",
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
lineItems: Array, modalWidgetName: String,
isInsuranceSelected: Boolean,
afterpayExtendedPayOptionThreshold: Number,
}, },
data() { data() {
return {}; return {};
@ -70,6 +91,17 @@ export default {
isInlineImageToken, isInlineImageToken,
isAfterpayPriceToken, isAfterpayPriceToken,
getInlineAltText, getInlineAltText,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
openModal() {
this.modal?.openModal();
},
closeModal() {
this.modal?.closeModal();
},
}, },
computed: { computed: {
imageUrl() { imageUrl() {
@ -84,39 +116,40 @@ export default {
afterpayCopyTokens() { afterpayCopyTokens() {
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText")); return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
}, },
modalCopy() { bannerCta() {
return this.getCmsContent(this.cmsWidgetName, "FooterText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
getTierOnePackagePrice() { modalHeaderText() {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); return this.getCmsContent(this.modalWidgetName, "HeaderText");
if (this.lineItems.promos) { },
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT"); modalDisclaimerText() {
} return this.getCmsContent(this.modalWidgetName, "BodyText2");
},
let price = baseMixin.methods.getTierOnePackagePrice( modalName() {
baseMixin.methods.filterOutFees(allLineItems) return this.modalWidgetName;
); },
modal() {
if (this.lineItems.promos) { return this.$refs[this.modalName];
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); },
const promos = getPromosThatMatchLineItemsOnOrder( afterpaySectionsContent() {
this.lineItems.promos, const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
allLineItems if (!sections) return [];
); sections.forEach((section) => {
promos.forEach((promo) => { const sectionContent = this.getCmsContent(section.Name, "BodyText");
price += baseMixin.methods.getTotalLineItemPrice(promo); section["content"] = sectionContent;
}); });
} return sections;
return price;
}, },
}, },
components: {}, components: {
textLink,
modal,
},
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
#afterpay-banner { .afterpay-modal-banner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: $blue-150; background-color: $blue-150;
@ -166,12 +199,116 @@ export default {
max-width: 32rem; max-width: 32rem;
} }
} }
& .alert-heading { }
font-size: 0.875rem;
.afterpay-modal {
&.modal.modal-component {
:deep(.modal-dialog) {
margin: 0 1rem;
top: 0;
@include media-breakpoint-up(md) {
width: 100%;
max-width: 840px;
top: 3.4rem;
right: 0;
left: 0;
transform: none;
margin: 0 auto;
padding: 0 1rem;
}
.modal-content {
border-radius: 0.5rem;
margin: 1.5rem 0;
.modal-header.mt-6 {
background-color: $blue-100;
margin-top: 0;
padding: 1.5rem 2rem 1.5rem 1.5rem;
font-family: UrbanistSemibold;
.modal-title.justify-content-center {
// OVERRIDE
justify-content: flex-start;
}
.btn-close {
top: 1.25rem;
right: 1.5rem;
}
}
.afterpay-section {
display: block;
border: 1px solid $gray-200;
border-radius: 0.25rem;
padding: 1rem;
.section-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
img {
width: 40px;
height: 40px;
}
h3 {
font-family: UrbanistSemibold;
font-size: 1rem;
margin: 0 0 0 0.5rem;
}
}
p {
font-size: 0.75rem;
}
strong {
font-family: UrbanistSemibold;
font-size: 0.875rem;
}
}
.modal-body {
padding: 1.5rem;
.afterpay-sections {
@include media-breakpoint-up(md) {
display: flex;
column-gap: 1rem;
align-items: stretch;
}
.afterpay-section {
margin-bottom: 1rem;
@include media-breakpoint-up(md) {
flex: 0 1 33%;
margin-bottom: 0;
}
&:last-of-type {
margin-bottom: 0;
}
}
}
}
}
&.modal-dialog-centered {
height: auto;
min-height: auto;
}
}
}
:deep(.modal-footer) {
position: relative;
background: $gray-100;
.modal-disclaimer {
font-style: italic;
font-size: 0.75rem;
a {
padding: 0;
line-height: 1.625;
}
}
} }
} }
#afterpay-learnmore {
font-weight: 600;
font-size: 0.875rem;
}
</style> </style>

View file

@ -82,7 +82,7 @@ jest.mock("@/mixins/base-mixin", () => ({
filterOutFees(items) { filterOutFees(items) {
return null; return null;
}, },
filterOutServicePackageDiscountPart(items) { filterOutQuotePageDiscountPart(items) {
return null; return null;
}, },
isFormValid(form) { isFormValid(form) {

View file

@ -31,10 +31,11 @@
:isRecalibrationOnOrder="isRecalibrationOnOrder" :isRecalibrationOnOrder="isRecalibrationOnOrder"
:shouldHideRecalibration="shouldHideRecalibration" :shouldHideRecalibration="shouldHideRecalibration"
@vapsItemsSelected="vapsItemsSelectedAction" @vapsItemsSelected="vapsItemsSelectedAction"
@servicePackageDiscountSelected="servicePackageDiscountSelectedAction" @quotePageDiscountSelected="quotePageDiscountSelectedAction"
@currentNumberOfPackages="currentNumberOfPackagesAction" @currentNumberOfPackages="currentNumberOfPackagesAction"
:servicePackage="servicePackage" :servicePackage="servicePackage"
:activePromos="lineItems.promos" :activePromos="lineItems.promos"
:availableQuotePageDiscounts="quoteDiscountPartsInfo"
v-on="{ 'buttonEvent.openModal': openModalAction }" v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required" validationRules="option-required"
isRequired /> isRequired />
@ -45,10 +46,8 @@
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner --> <!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
<afterpayModalBanner <afterpayModalBanner
v-if="showAfterpayBanner" v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget" cmsWidgetName="AfterpayBannerWidget"
:afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold" modalWidgetName="AfterpayModalWidget" />
:isInsuranceSelected="isInsuranceSelected"
:lineItems="lineItems" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. --> <!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
<recalDisclaimer <recalDisclaimer
@ -176,6 +175,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { debugLog } from "@/helpers/debug-log-helper"; import { debugLog } from "@/helpers/debug-log-helper";
import { savePageData } from "@/router/methods/helpers/save-page-data"; import { savePageData } from "@/router/methods/helpers/save-page-data";
import { quotePageDiscountTable } from "../../constants/quote-page-discounts";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -186,6 +186,14 @@ const ACCEPTED_QUOTE_TYPES = {
INSURANCE: "insurance", INSURANCE: "insurance",
}; };
function getAvailableQuotePageDiscounts() {
const activeDiscounts = quotePageDiscountTable?.filter((discountEntry) =>
experimentMixin?.methods?.hasSettingEqualTo(discountEntry?.experimentCode, "true")
);
return activeDiscounts ?? [];
}
export default { export default {
name: "quote", name: "quote",
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -302,29 +310,22 @@ export default {
const lineItems = deepClone(store.getters.order.lineItems); const lineItems = deepClone(store.getters.order.lineItems);
lineItems.vaps = lineItems.vaps ?? []; lineItems.vaps = lineItems.vaps ?? [];
const nullSafeGlassParts = lineItems.glassParts ?? []; const nullSafeGlassParts = lineItems.glassParts ?? [];
const servicePackageDiscountSettingValue = experimentMixin.methods.getSettingValue(
experimentSettings.SERVICE_PACKAGE_DISCOUNT const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts();
);
const isServicePackageDiscount = servicePackageDiscountSettingValue === "True"; const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({
//Service package cash discount api call when experiment is active partNumber: partInfo.partNumber,
var servicePackageDiscountPart = []; partType: partInfo.partType,
if (isServicePackageDiscount) { description: partInfo.description,
const servicePackageDiscountPartResponse = isInsurable: null,
await baseMixin.methods.dispatchStoreActionWithLogging( }));
storeActions.GET_SERVICE_PACKAGE_DISCOUNT_PART,
null,
"quote"
);
if (servicePackageDiscountPartResponse.data) {
servicePackageDiscountPart.push(servicePackageDiscountPartResponse.data);
}
}
const availableLineItems = [ const availableLineItems = [
resultMap.rainRepel, resultMap.rainRepel,
...resultMap.supportingItems, ...resultMap.supportingItems,
...resultMap.wipers, ...resultMap.wipers,
...nullSafeGlassParts, ...nullSafeGlassParts,
...servicePackageDiscountPart, ...quotePageDiscountParts,
]; ];
// 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 // 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
@ -351,7 +352,7 @@ export default {
false false
); );
// This will remove any servicePackageDiscount item from lineItems.supportingItems // This will remove any discount item from lineItems.supportingItems
baseMixin.methods.dispatchStoreAction( baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS, storeActions.SAVE_SUPPORTING_ITEMS,
resultMap.supportingItems, resultMap.supportingItems,
@ -453,10 +454,9 @@ export default {
) )
: baseMixin.methods.filterOutFees(availableLineItems); : baseMixin.methods.filterOutFees(availableLineItems);
const lineItemsNoDiscount = const lineItemsNoDiscount = baseMixin.methods.filterOutQuotePageDiscountPart(
baseMixin.methods.filterOutServicePackageDiscountPart( lineItemsForCalculatingPrice
lineItemsForCalculatingPrice );
);
if (isInsuranceFromQueryString != null) { if (isInsuranceFromQueryString != null) {
if (isInsuranceFromQueryString.toLowerCase() === "true") { if (isInsuranceFromQueryString.toLowerCase() === "true") {
@ -580,17 +580,8 @@ export default {
isRecalibrationOnOrder() { isRecalibrationOnOrder() {
return store.getters.isRecalibrationOnOrder; return store.getters.isRecalibrationOnOrder;
}, },
isServicePackageDiscountOnOrder() { isQuotePageDiscountOnOrder() {
return containsLineItemWithPartType( return this.quotePageDiscountPartsInfo?.length > 0;
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.availableLineItems
);
},
servicePackageDiscountParts() {
return findLineItemsWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.availableLineItems
);
}, },
shouldHideRecalibration() { shouldHideRecalibration() {
return ( return (
@ -619,6 +610,23 @@ export default {
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount)); thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
return thresholdAmount; return thresholdAmount;
}, },
quoteDiscountPartsInfo() {
const defs = getAvailableQuotePageDiscounts();
const withPrices = defs
.map((def) => {
const match = this.availableLineItems?.find?.(
(pricedItem) => pricedItem.partNumber === def.partNumber
);
return {
packageLevel: def.packageLevel,
item: match,
};
})
.filter((result) => result.item);
return withPrices;
},
}, },
methods: { methods: {
getColCount() { getColCount() {
@ -662,7 +670,7 @@ export default {
vapsItemsSelectedAction(vapsItemsSelected) { vapsItemsSelectedAction(vapsItemsSelected) {
this.lineItems.vaps = vapsItemsSelected; this.lineItems.vaps = vapsItemsSelected;
}, },
servicePackageDiscountSelectedAction(supportingItemsSelected) { quotePageDiscountSelectedAction(supportingItemsSelected) {
this.lineItems.supportingItems = supportingItemsSelected; this.lineItems.supportingItems = supportingItemsSelected;
}, },
backButtonAction() { backButtonAction() {
@ -724,10 +732,10 @@ export default {
false false
); );
} }
if (this.isInsuranceSelected && this.isServicePackageDiscountOnOrder) { if (this.isInsuranceSelected && this.isQuotePageDiscountOnOrder) {
let supportingItems = this.lineItems.supportingItems; let supportingItems = this.lineItems.supportingItems;
supportingItems = supportingItems.filter((item) => { supportingItems = supportingItems.filter((item) => {
return item.partType != this.servicePackageDiscountParts[0].partType; return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
}); });
this.dispatchStoreAction( this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS, this.storeActions.SAVE_SUPPORTING_ITEMS,
@ -813,19 +821,23 @@ export default {
lineItemsToPrice = lineItemsToPrice =
baseMixin.methods.filterOutRecalibration(lineItemsToPrice); baseMixin.methods.filterOutRecalibration(lineItemsToPrice);
} }
if (this.isServicePackageDiscountOnOrder) { if (this.isQuotePageDiscountOnOrder) {
lineItemsToPrice = lineItemsToPrice?.filter((item) => { lineItemsToPrice = lineItemsToPrice?.filter((item) => {
return ( return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
item.partType != this.servicePackageDiscountParts[0].partType
);
}); });
} }
let price = 0; let price = 0;
if ( if (
tier.additionalButtonData?.servicePackageDiscount && tier.additionalButtonData?.hasDiscount &&
this.isServicePackageDiscountOnOrder this.isQuotePageDiscountOnOrder
) { ) {
lineItemsToPrice.push(...this.servicePackageDiscountParts); const parts = this.quoteDiscountPartsInfo;
const matchPart = parts.find(
(discountInfo) => discountInfo.packageLevel === tier
);
if (matchPart) {
lineItemsToPrice.push(matchPart);
}
} }
price += baseMixin.methods.getTierOnePackagePrice( price += baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(lineItemsToPrice) baseMixin.methods.filterOutFees(lineItemsToPrice)

View file

@ -14,6 +14,7 @@ import { experimentSettings } from "@/constants/experiments";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings"; import { partTypeStrings } from "@/constants/part-type-strings";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { packageNames } from "../../../constants/package-names";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
commit: jest.fn(), commit: jest.fn(),
@ -73,9 +74,9 @@ describe("service-package-question.vue", () => {
price: 35.5, price: 35.5,
}, },
{ {
partNumber: "DISC CASHSAVE70", partNumber: "PRM RCL CSH PKG",
description: null, description: null,
partType: "SERVICE PACKAGE DISCOUNT", partType: "QUOTE PAGE DISCOUNT",
price: -70, price: -70,
}, },
], ],
@ -105,13 +106,27 @@ describe("service-package-question.vue", () => {
description: null, description: null,
kitPrice: 0, kitPrice: 0,
laborAmount: 0, laborAmount: 0,
partNumber: "DISC CASHSAVE70", partNumber: "PRM RCL CSH PKG",
partType: "SERVICE PACKAGE DISCOUNT", partType: "QUOTE PAGE DISCOUNT",
salesTax: null, salesTax: null,
sellingPrice: -70, sellingPrice: -70,
}, },
], ],
}, },
availableQuotePageDiscounts: [
{
packageLevel: packageNames.TIER_THREE,
item: {
description: null,
kitPrice: 0,
laborAmount: 0,
partNumber: "PRM RCL CSH PKG",
partType: "QUOTE PAGE DISCOUNT",
salesTax: null,
sellingPrice: -70,
},
},
],
activePromos: [], activePromos: [],
}; };
const packageNameKey = "05_01_CSR_Quote_Standard_Repair"; const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
@ -136,41 +151,17 @@ describe("service-package-question.vue", () => {
}); });
it("should return price when there is discount", () => { it("should return price when there is discount", () => {
const wrapper = setupMocks({}); const wrapper = setupMocks({});
const partType = partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
wrapper.vm.isServicePackageDiscountOnOrder = containsLineItemWithPartType( const price = wrapper.vm.getDiscountPrice(packageNames.TIER_THREE);
partType,
mockProps.lineItems.supportingItems
);
const servicePackageDiscountLineItem = findLineItemsWithPartType(
partType,
mockProps.lineItems.supportingItems
);
wrapper.vm.getServicePackageDiscountPrice();
const price = baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem[0]);
expect(Math.abs(price)).toEqual(70); expect(Math.abs(price)).toEqual(70);
}); });
it("isServicePackageDiscountOnOrder returns true if it contains service package discount lineItems", () => { it("isDiscountOnOrder returns true if it contains quote package discount lineItems", () => {
// Arrange // Arrange
const wrapper = setupMocks({}); const wrapper = setupMocks({});
// Assert // Assert
expect(wrapper.vm.isServicePackageDiscountOnOrder).toBe(true); expect(wrapper.vm.isDiscountOnOrder).toBe(true);
});
it("servicePackageDiscountParts should returns service package discount lineItems", () => {
// Arrange
const wrapper = setupMocks({});
// Assert
expect(wrapper.vm.servicePackageDiscountParts).toEqual([
{
partNumber: "DISC CASHSAVE70",
description: null,
partType: "SERVICE PACKAGE DISCOUNT",
price: -70,
},
]);
}); });
it("should have the correct insurance pricing text when insurance is selected", () => { it("should have the correct insurance pricing text when insurance is selected", () => {
// Arrange // Arrange

View file

@ -60,6 +60,7 @@ export default {
servicePackage: null, servicePackage: null,
isRecalibrationOnOrder: Boolean, isRecalibrationOnOrder: Boolean,
shouldHideRecalibration: Boolean, shouldHideRecalibration: Boolean,
availableQuotePageDiscounts: null,
}, },
data() { data() {
return { return {
@ -85,27 +86,18 @@ export default {
selectedPackageName(newValue) { selectedPackageName(newValue) {
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue); const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage); this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
const servicePackageDiscountPartInSelectedPackage = const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue);
this.getServicePackageDiscountPartForSelectedPackage(newValue);
let supportingItems = this.supportingLineItems; let supportingItems = this.supportingLineItems;
if (
containsLineItemWithPartType( supportingItems = supportingItems.filter(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT, (item) => item.partType !== partTypeStrings.QUOTE_PAGE_DISCOUNT
supportingItems );
)
) { if (discountPartInSelectedPackage) {
supportingItems = supportingItems.filter( supportingItems.push(discountPartInSelectedPackage);
(item) => item.partType !== partTypeStrings.SERVICE_PACKAGE_DISCOUNT
);
}
if (
servicePackageDiscountPartInSelectedPackage?.length > 0 &&
supportingItems != null
) {
supportingItems.push(servicePackageDiscountPartInSelectedPackage[0]);
} }
this.$emit("servicePackageDiscountSelected", supportingItems); this.$emit("quotePageDiscountSelected", supportingItems);
}, },
servicePackage(newValue) { servicePackage(newValue) {
if (newValue !== null) { if (newValue !== null) {
@ -118,7 +110,7 @@ export default {
return this.availableLineItems ?? []; return this.availableLineItems ?? [];
}, },
supportingLineItems() { supportingLineItems() {
return this.$store.getters.lineItems?.supportingItems; return this.$store.getters.lineItems?.supportingItems ?? [];
}, },
servicePackageAnswers() { servicePackageAnswers() {
const cmsWidgetName = this.isInsuranceSelected const cmsWidgetName = this.isInsuranceSelected
@ -149,18 +141,15 @@ export default {
? this.getDiscountedPackagePriceString(answer.Name, false) ? this.getDiscountedPackagePriceString(answer.Name, false)
: this.getDiscountedPackagePriceString( : this.getDiscountedPackagePriceString(
answer.Name, answer.Name,
this.isServicePackageDiscountOnOrder && this.hasDiscountForPackage(answer.Name)
this.discountPackageNames == answer.Name
), ),
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName), buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
additionalButtonData: { additionalButtonData: {
strikeThroughPrice: this.getPackagePriceString(answer.Name), strikeThroughPrice: this.getPackagePriceString(answer.Name),
servicePackageDiscount: hasDiscount: this.hasDiscountForPackage(answer.Name),
this.isServicePackageDiscountOnOrder &&
this.discountPackageNames == answer.Name,
Text: this.isInsuranceSelected Text: this.isInsuranceSelected
? false ? false
: "Special: Save $" + this.getServicePackageDiscountPrice(), : "Special: Save $" + this.getDiscountPrice(answer.Name),
isInsuranceSelected: this.isInsuranceSelected, isInsuranceSelected: this.isInsuranceSelected,
}, },
})); }));
@ -168,17 +157,8 @@ export default {
this.$emit("currentNumberOfPackages", modifiedAnswers.length); this.$emit("currentNumberOfPackages", modifiedAnswers.length);
return modifiedAnswers; return modifiedAnswers;
}, },
isServicePackageDiscountOnOrder() { isDiscountOnOrder() {
return containsLineItemWithPartType( return this.availableQuotePageDiscounts?.length > 0;
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.nullSafeAvailableLineItems
);
},
discountPackageNames() {
const discountServicePackage = experimentMixin.methods.getSettingValue(
experimentSettings.PROMO_ON_PACKAGE
);
return getDiscountedPackageName(discountServicePackage);
}, },
frontWipersApplicableForTierTwo() { frontWipersApplicableForTierTwo() {
return shouldFrontWipersBeAvailable( return shouldFrontWipersBeAvailable(
@ -226,12 +206,6 @@ export default {
isRepair() { isRepair() {
return this.$store.getters.order.damage.isRepair; return this.$store.getters.order.damage.isRepair;
}, },
servicePackageDiscountParts() {
return findLineItemsWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.nullSafeAvailableLineItems
);
},
showRecalInServicePackages() { showRecalInServicePackages() {
return this.isRecalibrationOnOrder && !this.shouldHideRecalibration; return this.isRecalibrationOnOrder && !this.shouldHideRecalibration;
}, },
@ -280,16 +254,16 @@ export default {
const formattedPriceFloat = parseFloat( const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, { this.getPackagePrice(packageName, {
discountedPrice: false, discountedPrice: false,
servicePackageDiscount: false, quotePageDiscount: false,
}) })
).toFixed(2); ).toFixed(2);
return "$" + formattedPriceFloat; return "$" + formattedPriceFloat;
}, },
getDiscountedPackagePriceString(packageName, servicePackageDiscount) { getDiscountedPackagePriceString(packageName, quotePageDiscount) {
const formattedPriceFloat = parseFloat( const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, { this.getPackagePrice(packageName, {
discountedPrice: true, discountedPrice: true,
servicePackageDiscount, quotePageDiscount,
}) })
).toFixed(2); ).toFixed(2);
@ -298,7 +272,7 @@ export default {
} }
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat; return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
}, },
getPackagePrice(packageName, { discountedPrice = false, servicePackageDiscount = false }) { getPackagePrice(packageName, { discountedPrice = false, quotePageDiscount = false }) {
let lineItemsToPrice = [...this.nullSafeAvailableLineItems]; let lineItemsToPrice = [...this.nullSafeAvailableLineItems];
if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) { if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) {
@ -306,17 +280,20 @@ export default {
} }
//remove service package discount part //remove service package discount part
if (this.isServicePackageDiscountOnOrder) { if (this.isDiscountOnOrder) {
lineItemsToPrice = lineItemsToPrice.filter((item) => { lineItemsToPrice = lineItemsToPrice.filter((item) => {
return item.partType != this.servicePackageDiscountParts[0].partType; return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
}); });
} }
if (discountedPrice) { if (discountedPrice) {
lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos)); lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos));
} }
if (servicePackageDiscount) { if (quotePageDiscount) {
lineItemsToPrice.push(...this.servicePackageDiscountParts); const part = this.getDiscountPartForSelectedPackage(packageName);
if (part) {
lineItemsToPrice.push(part);
}
} }
let priceFloat = this.isInsuranceSelected let priceFloat = this.isInsuranceSelected
? 0 ? 0
@ -328,14 +305,16 @@ export default {
return priceFloat; return priceFloat;
}, },
getServicePackageDiscountPrice() { getDiscountPrice(packageName) {
if (this.isServicePackageDiscountOnOrder) { const part = this.getDiscountPartForSelectedPackage(packageName);
let price = 0;
price += baseMixin.methods.getTotalLineItemPrice( if (part) {
this.servicePackageDiscountParts[0] const price = baseMixin.methods.getTotalLineItemPrice(part);
);
return Math.abs(price); return Math.abs(price);
} }
return null;
}, },
getVapsPrice(packageName, applyPromoDiscounts = false) { getVapsPrice(packageName, applyPromoDiscounts = false) {
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName); const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
@ -398,13 +377,18 @@ export default {
return vapsLineItemsForSelectedPackage; return vapsLineItemsForSelectedPackage;
}, },
getServicePackageDiscountPartForSelectedPackage(packageName) { getDiscountPartForSelectedPackage(packageName) {
const selectedPackage = this.servicePackageAnswers.filter( if (this.availableQuotePageDiscounts?.length > 0) {
(item) => item.value === packageName const match = this.availableQuotePageDiscounts.find(
); (discountInfo) => discountInfo.packageLevel === packageName
if (selectedPackage?.[0]?.additionalButtonData?.servicePackageDiscount) { );
return this.servicePackageDiscountParts; return match?.item;
} else return []; }
return null;
},
hasDiscountForPackage(packageName) {
return !!this.getDiscountPartForSelectedPackage(packageName);
}, },
combineLineItemsWithoutDuplicates(lineItemsOne, lineItemsTwo) { combineLineItemsWithoutDuplicates(lineItemsOne, lineItemsTwo) {
const combinedLineItemArray = [...lineItemsTwo]; const combinedLineItemArray = [...lineItemsTwo];

View file

@ -5,7 +5,7 @@
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']" :class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
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.hasDiscount" class="row">
<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>
@ -77,10 +77,7 @@
</div> </div>
<div <div
v-if=" v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
class="special-save-box"> class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span> <span v-html="this.additionalButtonData.Text"></span>
</div> </div>

View file

@ -129,9 +129,9 @@ export default {
}); });
return lineItemsArray; return lineItemsArray;
}, },
filterOutServicePackageDiscountPart(lineItems) { filterOutQuotePageDiscountPart(lineItems) {
const filteredLineItems = lineItems?.filter((item) => { const filteredLineItems = lineItems?.filter((item) => {
return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT); return !item?.partType?.includes(partTypeStrings.QUOTE_PAGE_DISCOUNT);
}); });
return filteredLineItems; return filteredLineItems;
}, },

View file

@ -3,15 +3,33 @@ import { queryStrings } from "@/constants/query-strings";
import store from "@/store"; import store from "@/store";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function consumeReferralQuerystrings() { export async function consumeReferralQuerystrings() {
const referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER); var referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT); var parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID); var correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
var referralDate = null;
// If no referral number in querystring, check if heritage funnel updated last.
// This would indicate a customer that went to heritage but did not return through the
// normal route. ie: left the site in search of a discount and returned without querystrings.
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
if (didHeritageFunnelUpdateLast && !referralNumber) {
referralNumber = getFunnelCookie().ReferralNumber;
parentAccount = getFunnelCookie().ReferralParentAccountNumber;
correlationId = getFunnelCookie().ReferralCorrelationId;
referralDate = getFunnelCookie().ReferralDate;
}
if (referralNumber) { if (referralNumber) {
store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount); store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount);
store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId); store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId);
if (referralDate) {
store.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
}
// log(" --update cookie"); // log(" --update cookie");
updateOrCreateFunnelCookie(); updateOrCreateFunnelCookie();
} }

View file

@ -2,11 +2,13 @@ import { queryStrings } from "@/constants/query-strings";
import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info"; import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info";
import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper"; import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper";
import { routeData } from "@/router/constants/routes"; import { routeData } from "@/router/constants/routes";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function handleHeritageReturn(to, from) { export async function handleHeritageReturn(to, from) {
const fromHeritageFlag = to.query[queryStrings.FROM_HERITAGE]; const fromHeritageFlag = to.query[queryStrings.FROM_HERITAGE];
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
if (fromHeritageFlag) { if (fromHeritageFlag || didHeritageFunnelUpdateLast) {
// Get new referral info from querystring in case passed there. // Get new referral info from querystring in case passed there.
await consumeReferralQuerystrings(); await consumeReferralQuerystrings();

View file

@ -123,8 +123,8 @@ $body-color: $gray-600;
//Fonts //Fonts
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif; $font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
$font-family-monospace: $font-family-monospace: UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; monospace;
// stylelint-enable value-keyword-case // stylelint-enable value-keyword-case
$font-family-base: $font-family-sans-serif; $font-family-base: $font-family-sans-serif;
$font-family-code: $font-family-monospace; $font-family-code: $font-family-monospace;