Merge branch 'develop' into feature/CSR-2091

This commit is contained in:
CarlNation 2024-06-18 06:59:29 -04:00
commit d85e375e1c
21 changed files with 590 additions and 59 deletions

View file

@ -21,7 +21,7 @@ body span {
line-height: 24px;
}
body a {
color: #1574A1;
color: #0070D1;
text-decoration: none;
}
body .form-group {

View file

@ -24,7 +24,7 @@ body {
}
a {
color: #1574A1;
color: #0070D1;
text-decoration: none;
}

View file

@ -1,6 +1,3 @@
<svg version="1.1" id="SafeliteLogo" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 451.2 73.6" style="enable-background:new 0 0 451.2 73.6" xml:space="preserve">
<style>
.st0{fill:#db0020}
</style>
<path class="st0" d="M439.8 67.9c0-3.1 2.6-5.7 5.7-5.7 3.2 0 5.7 2.6 5.7 5.7 0 3.2-2.5 5.7-5.7 5.7-3.1 0-5.7-2.5-5.7-5.7m10.5 0c0-2.6-2.1-4.7-4.7-4.7s-4.7 2.1-4.7 4.7 2.1 4.7 4.7 4.7c2.5 0 4.7-2.1 4.7-4.7m-6 .4v2.9h-1v-6.7h2.2c1.2 0 2.5.3 2.5 1.9 0 .8-.5 1.4-1.4 1.6.9.2 1 .6 1.1 1.3.1.6.2 1.3.4 1.9h-1.3c-.1-.4-.2-.8-.2-1.2-.1-.6-.1-1.1-.4-1.4-.3-.3-.6-.2-1-.3h-.9zm1.1-1.1c1 0 1.2-.4 1.2-1 0-.5-.2-.8-1.1-.8h-1.3v1.7h1.2zM47.2 22.8c-.8 0-1.4-.6-1.4-1.4v-1.5c0-4.1-2.6-7.5-8.1-7.5-6.1 0-8.8 4.3-8.8 7.4 0 13.7 46.4 7 46.4 32.3 0 14.7-13.6 21.3-38.6 21.3C13.1 73.4 0 68.2 0 53.9v-1c0-.8.6-1.4 1.4-1.4H26c.8 0 1.4.6 1.4 1.4v1.5c0 5.9 3.8 8 9.3 8 5.8 0 9.2-4.2 9.2-8 0-13.7-44.5-6.9-44.5-31.4C1.4 9 13.1 1.4 37.1 1.4c23.8 0 34.5 6 35.2 19.8 0 .8-.6 1.5-1.4 1.5l-23.7.1zM153.6 15.7c0-9.8 6-13.4 23.8-13.4 3.3 0 7.3.1 11 .4.7.1 1.3.7 1.3 1.4v6c0 .8-.7 1.5-1.6 1.4-.8-.1-1.6-.1-2.7-.1-3.1 0-6.1 1-6.1 3.4v6.9c0 .8.6 1.4 1.4 1.4h7.6c.8 0 1.4.6 1.4 1.4v8.1c0 .8-.6 1.4-1.4 1.4h-7.6c-.8 0-1.4.6-1.4 1.4v35.2c0 .8-.6 1.4-1.4 1.4h-22.8c-.8 0-1.4-.6-1.4-1.4l-.1-54.9zM218 50.6c-.8 0-1.4.6-1.4 1.4v6.3c0 5.3 3.4 6.6 7 6.6 4.3 0 6.9-2.7 7.2-8.5 0-.7.7-1.3 1.4-1.3h20.2c.9 0 1.5.7 1.4 1.6-1 10.9-9.9 16.9-30.2 16.9-24.8 0-32.8-7.1-32.8-25.8 0-18.8 9.7-25.9 32.8-25.9 20.5 0 31 5.8 31 23.6v3.6c0 .8-.6 1.4-1.4 1.4l-35.2.1zm12.6-12.4c0-5.2-1.7-7.5-7.1-7.5-5.3 0-7 2.4-7 7.5v2.1c0 .8.6 1.4 1.4 1.4h11.3c.8 0 1.4-.6 1.4-1.4v-2.1zM265.2 0H288c.8 0 1.4.6 1.4 1.4v69.5c0 .8-.6 1.4-1.4 1.4h-22.8c-.8 0-1.4-.6-1.4-1.4V1.4c-.1-.8.6-1.4 1.4-1.4zM302 23.2h22.8c.8 0 1.4.6 1.4 1.4v46.3c0 .8-.6 1.4-1.4 1.4H302c-.8 0-1.4-.6-1.4-1.4V24.6c-.1-.8.6-1.4 1.4-1.4zM302 0h22.8c.8 0 1.4.6 1.4 1.4v12.1c0 .8-.6 1.4-1.4 1.4L302 15c-.8 0-1.4-.6-1.4-1.4V1.5c-.1-.9.6-1.5 1.4-1.5zM403.2 50.6c-.8 0-1.4.6-1.4 1.4v6.3c0 5.3 3.4 6.6 7 6.6 4.2 0 6.9-2.7 7.2-8.5 0-.7.7-1.3 1.4-1.3h20.2c.9 0 1.5.7 1.4 1.6-1 10.9-9.9 16.9-30.1 16.9-24.8 0-32.8-7.1-32.8-25.8 0-18.8 9.7-25.9 32.8-25.9 20.5 0 31 5.8 31 23.6v3.6c0 .8-.6 1.4-1.4 1.4l-35.3.1zm12.7-12.4c0-5.2-1.7-7.5-7.1-7.5-5.3 0-7 2.4-7 7.5v2.1c0 .8.6 1.4 1.4 1.4h11.3c.8 0 1.4-.6 1.4-1.4v-2.1zM144.4 37.7C144.3 25 131.8 22 113.9 22c-11.2 0-28.2 2.3-29.5 14.9-.1.8.6 1.6 1.4 1.6h19.4c.7 0 1.3-.6 1.4-1.3.5-4.3 2.8-6.5 7.2-6.5s6.5 1.5 6.5 4.5v4.4c0 .7-.5 1.3-1.2 1.4-1.2.2-2.9.4-6.3.9l-10.1 1.5c-16.4 2.4-21.4 6.7-21.4 15.8 0 8.4 6 14.4 21.4 14.4 7.3 0 13.9-2.2 17-6.2.2-.2.6-.2.6.1.3 1 .9 2.9 1.2 3.9.2.6.7.9 1.3.9h20c.8 0 1.4-.6 1.4-1.4l.2-33.2zm-32 26.3c-3.9 0-6.3-3.4-6.3-6.7 0-3.3 1.5-5.3 5.3-6.3 2.4-.7 5.5-1.1 7.4-1.4.9-.1 1.6.5 1.6 1.4v4.9c0 5.3-2.2 8.1-8 8.1zM364.2 34.3h7.3c.8 0 1.4-.6 1.4-1.4v-8.1c0-.8-.6-1.4-1.4-1.4h-7.3c-.8 0-1.4-.6-1.4-1.4l-.1-20.4v-.4c0-.6-.5-1.1-1.1-1.1-.2 0-.3 0-.4.1h-.1l-22.6 13.4c-1 .6-1.4.7-1.4 1.4v45.4c0 10.9 6.1 12.5 21.8 12.5 3.9 0 8.3-.2 12.7-.4.7-.1 1.3-.7 1.3-1.4v-6.8c0-.8-.6-1.4-1.4-1.4h-2.6c-4.1 0-6.1-1-6.1-3.8V35.8c0-.9.6-1.5 1.4-1.5z"/>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 439.8 73.6" viewBox="0 0 439.8 73.6">
<path d="M47.2 22.8c-.8 0-1.4-.6-1.4-1.4v-1.5c0-4.1-2.6-7.5-8.1-7.5-6.1 0-8.8 4.3-8.8 7.4 0 13.7 46.4 7 46.4 32.3 0 14.7-13.6 21.3-38.6 21.3C13.1 73.4 0 68.2 0 53.9v-1c0-.8.6-1.4 1.4-1.4H26c.8 0 1.4.6 1.4 1.4v1.5c0 5.9 3.8 8 9.3 8 5.8 0 9.2-4.2 9.2-8 0-13.7-44.5-6.9-44.5-31.4C1.4 9 13.1 1.4 37.1 1.4c23.8 0 34.5 6 35.2 19.8 0 .8-.6 1.5-1.4 1.5l-23.7.1zm106.4-7.1c0-9.8 6-13.4 23.8-13.4 3.3 0 7.3.1 11 .4.7.1 1.3.7 1.3 1.4v6c0 .8-.7 1.5-1.6 1.4-.8-.1-1.6-.1-2.7-.1-3.1 0-6.1 1-6.1 3.4v6.9c0 .8.6 1.4 1.4 1.4h7.6c.8 0 1.4.6 1.4 1.4v8.1c0 .8-.6 1.4-1.4 1.4h-7.6c-.8 0-1.4.6-1.4 1.4v35.2c0 .8-.6 1.4-1.4 1.4h-22.8c-.8 0-1.4-.6-1.4-1.4l-.1-54.9zM218 50.6c-.8 0-1.4.6-1.4 1.4v6.3c0 5.3 3.4 6.6 7 6.6 4.3 0 6.9-2.7 7.2-8.5 0-.7.7-1.3 1.4-1.3h20.2c.9 0 1.5.7 1.4 1.6-1 10.9-9.9 16.9-30.2 16.9-24.8 0-32.8-7.1-32.8-25.8 0-18.8 9.7-25.9 32.8-25.9 20.5 0 31 5.8 31 23.6v3.6c0 .8-.6 1.4-1.4 1.4l-35.2.1zm12.6-12.4c0-5.2-1.7-7.5-7.1-7.5-5.3 0-7 2.4-7 7.5v2.1c0 .8.6 1.4 1.4 1.4h11.3c.8 0 1.4-.6 1.4-1.4v-2.1zM265.2 0H288c.8 0 1.4.6 1.4 1.4v69.5c0 .8-.6 1.4-1.4 1.4h-22.8c-.8 0-1.4-.6-1.4-1.4V1.4c-.1-.8.6-1.4 1.4-1.4zM302 23.2h22.8c.8 0 1.4.6 1.4 1.4v46.3c0 .8-.6 1.4-1.4 1.4H302c-.8 0-1.4-.6-1.4-1.4V24.6c-.1-.8.6-1.4 1.4-1.4zM302 0h22.8c.8 0 1.4.6 1.4 1.4v12.1c0 .8-.6 1.4-1.4 1.4L302 15c-.8 0-1.4-.6-1.4-1.4V1.5c-.1-.9.6-1.5 1.4-1.5zm101.2 50.6c-.8 0-1.4.6-1.4 1.4v6.3c0 5.3 3.4 6.6 7 6.6 4.2 0 6.9-2.7 7.2-8.5 0-.7.7-1.3 1.4-1.3h20.2c.9 0 1.5.7 1.4 1.6-1 10.9-9.9 16.9-30.1 16.9-24.8 0-32.8-7.1-32.8-25.8 0-18.8 9.7-25.9 32.8-25.9 20.5 0 31 5.8 31 23.6v3.6c0 .8-.6 1.4-1.4 1.4l-35.3.1zm12.7-12.4c0-5.2-1.7-7.5-7.1-7.5-5.3 0-7 2.4-7 7.5v2.1c0 .8.6 1.4 1.4 1.4h11.3c.8 0 1.4-.6 1.4-1.4v-2.1zm-271.5-.5C144.3 25 131.8 22 113.9 22c-11.2 0-28.2 2.3-29.5 14.9-.1.8.6 1.6 1.4 1.6h19.4c.7 0 1.3-.6 1.4-1.3.5-4.3 2.8-6.5 7.2-6.5s6.5 1.5 6.5 4.5v4.4c0 .7-.5 1.3-1.2 1.4-1.2.2-2.9.4-6.3.9l-10.1 1.5c-16.4 2.4-21.4 6.7-21.4 15.8 0 8.4 6 14.4 21.4 14.4 7.3 0 13.9-2.2 17-6.2.2-.2.6-.2.6.1.3 1 .9 2.9 1.2 3.9.2.6.7.9 1.3.9h20c.8 0 1.4-.6 1.4-1.4l.2-33.2zm-32 26.3c-3.9 0-6.3-3.4-6.3-6.7 0-3.3 1.5-5.3 5.3-6.3 2.4-.7 5.5-1.1 7.4-1.4.9-.1 1.6.5 1.6 1.4v4.9c0 5.3-2.2 8.1-8 8.1zm251.8-29.7h7.3c.8 0 1.4-.6 1.4-1.4v-8.1c0-.8-.6-1.4-1.4-1.4h-7.3c-.8 0-1.4-.6-1.4-1.4l-.1-20.4v-.4c0-.6-.5-1.1-1.1-1.1-.2 0-.3 0-.4.1h-.1l-22.6 13.4c-1 .6-1.4.7-1.4 1.4v45.4c0 10.9 6.1 12.5 21.8 12.5 3.9 0 8.3-.2 12.7-.4.7-.1 1.3-.7 1.3-1.4v-6.8c0-.8-.6-1.4-1.4-1.4h-2.6c-4.1 0-6.1-1-6.1-3.8V35.8c0-.9.6-1.5 1.4-1.5z" style="fill:#dc1f26"/>
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

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

View file

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

View file

@ -72,6 +72,10 @@ const endpoints = {
url: "/parts/api/v1/parts/mobile-fee",
method: "GET",
},
GetServicePackageDiscountPart: {
url: "/parts/api/v1/parts/service-package-discount",
method: "POST",
},
GetServiceabilityDetails: {
url: "/location/api/v1/location/serviceability-details",
method: "GET",

View file

@ -9,6 +9,7 @@ const experimentSettings = {
PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional",
SERVICE_PACKAGE_DISCOUNT: "OfferServicePackageDiscount",
};
const experimentTriggers = {

View file

@ -8,6 +8,7 @@ const partTypeStrings = {
MOBILE_FEE: "MOBILE FEE",
REPAIR_FEE: "REPAIR FEE",
EARLY_BIRD: "EARLY BIRD",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
};
export { partTypeStrings };

View file

@ -29,6 +29,7 @@ const storeActions = {
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",

View file

@ -306,6 +306,49 @@ describe("cart.vue", () => {
expect(found).toBe(true);
});
// Service package discount Fee Cart Item
test("if there is service package discount fee on the order, a service package discount cart item should be added to the cart", () => {
// Arrange
const lineItems = {
glassParts: [],
supportingItems: [
{
description: null,
id: "bc00294e-6baa-403e-866e-52c267187a15",
kitPrice: 0,
laborAmount: 0,
partNumber: "DISC CASHSAVE70",
partType: "SERVICE PACKAGE DISCOUNT",
salesTax: null,
sellingPrice: -70,
},
],
vaps: [],
promos: [],
};
const availableVaps = [];
// Act
const { wrapper } = setupMocks({
props: {
modelValue: lineItems,
availableVaps: availableVaps,
},
});
// Assert
expect(wrapper.vm.servicePackageDiscountCartItem).not.toBeNull();
expect(wrapper.vm.servicePackageDiscountCartItem.subTotal).toEqual(-70);
expect(wrapper.vm.servicePackageDiscountCartItem.salesTax).toEqual(0);
const found =
wrapper.vm.cartItems.findIndex(
(cartItem) => cartItem == wrapper.vm.servicePackageDiscountCartItem
) >= 0;
expect(found).toBe(true);
});
// Other Supporting Items Cart Item
test("if there are other supporting items on the order, an other supporting items cart item should be added to the cart but should not be displayed", () => {
// Arrange

View file

@ -225,6 +225,13 @@ export default {
return subTotal;
},
getServicePackageDiscount() {
const servicePackageDiscountLineItems = this.servicePackageDiscountCartItem;
let subTotal = 0;
subTotal += servicePackageDiscountLineItems?.subTotal ?? 0;
return subTotal;
},
getVapsCartItemsForSelectedPackage(packageName) {
const packageContentTypes = getPackageContents(
this.glassToReplace,
@ -265,7 +272,7 @@ export default {
getLineItemAmount(amount, useVerifyingText, category) {
if (useVerifyingText) return this.verifyingCoverageText;
return category == "promos"
return category == "promos" || category == "servicePackageDiscount"
? "(" + this.currencyFormatter.format(amount * -1) + ")"
: this.currencyFormatter.format(amount);
},
@ -372,6 +379,9 @@ export default {
if (this.mobileFeeCartItem) {
cartItems.push(this.mobileFeeCartItem);
}
if (this.servicePackageDiscountCartItem) {
cartItems.push(this.servicePackageDiscountCartItem);
}
if (this.premiumAppointmentDiscountCartItem) {
cartItems.push(this.premiumAppointmentDiscountCartItem);
@ -422,6 +432,9 @@ export default {
}
packagePrice += this.getVapsPrice(this.packageLevel);
if (this.servicePackageDiscountCartItem) {
packagePrice -= this.getServicePackageDiscount();
}
return packagePrice;
},
@ -769,13 +782,55 @@ export default {
return cartItem;
},
servicePackageDiscountCartItemName() {
return this.getCmsContent("ServicePackageDiscountTextWidget", "Text");
},
servicePackageDiscountCartItem() {
let cartItem = null;
const servicePackageDiscountLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT
);
if (servicePackageDiscountLineItem) {
cartItem = {
name:
this.servicePackageDiscountCartItemName +
Math.abs(
baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem)
),
category: cartItemCategories.SERVICE_PACKAGE_DISCOUNT,
cartItemType: cartItemTypes.SERVICE_PACKAGE_DISCOUNT,
isDisplayed: true,
isRemovable: false,
subTotal: 0,
salesTax: 0,
lineItems: [],
isCoveredByInsurance: false,
};
servicePackageDiscountLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(servicePackageDiscountLineItem);
cartItem.subTotal +=
(servicePackageDiscountLineItem.kitPrice ?? 0) +
(servicePackageDiscountLineItem.laborAmount ?? 0) +
(servicePackageDiscountLineItem.sellingPrice ?? 0);
cartItem.salesTax += servicePackageDiscountLineItem.salesTax ?? 0;
}
return cartItem;
},
otherSupportingItemsCartItem() {
let cartItem = null;
const otherSupportingItems = this.supportingItems.filter((item) => {
return item.partType != partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
});
// Don't include fees they are part of the package total
let otherSupportingItemsLineItems = baseMixin.methods.filterOutFees(
this.supportingItems
);
let otherSupportingItemsLineItems =
baseMixin.methods.filterOutFees(otherSupportingItems);
if (otherSupportingItemsLineItems.length > 0) {
cartItem = {

View file

@ -421,7 +421,7 @@ export default {
}
.add-to-calendar-text {
margin: 0 0 0 7px;
color: #1574a1;
color: #0070d1;
vertical-align: middle;
font-weight: 500;
font-family: "Roboto";

View file

@ -6,6 +6,8 @@ import quote from "@/layouts/quote/quote.vue";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
import { nextTick } from "vue";
import { experimentSettings } from "@/constants/experiments";
import baseMixin from "@/mixins/base-mixin.js";
jest.mock("@/store", () => ({
commit: jest.fn(),
@ -60,9 +62,14 @@ const mockMixin = {
filterOutFees: jest.fn().mockImplementation(() => {
return null;
}),
getSettingValue: jest.fn((settingName) => {
if (settingName === experimentSettings.SERVICE_PACKAGE_DISCOUNT) {
return true;
}
return false;
}),
},
};
let mockTierOnePrice = 501;
const mockPriceOrderStoreAction = storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
@ -234,8 +241,12 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
//mock this to avoid needing to populate this.$route in an unrelated test
wrapper.vm.getDefaultIsInsuranceSelectedValue = jest.fn();
@ -254,6 +265,47 @@ describe("quote.vue", () => {
// This should have its own test
//expect(vm.isInsuranceSelected !== null).toBe(true);
});
test("Returns true if service package discount setting is true", async () => {
//Arrange
store.getters = {
lineItems: {
glassParts: ["item", "item2"],
},
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
},
},
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "false" } };
const isServicePackageDiscount = mockMixin.methods.getSettingValue(
experimentSettings.SERVICE_PACKAGE_DISCOUNT
);
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(isServicePackageDiscount).toBe(true);
});
test("should default to insurance if query param 'isInsurance' is true", async () => {
//Arrange
store.getters = {
@ -274,6 +326,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "true" } };
@ -309,6 +364,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "false" } };
@ -345,6 +403,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection
@ -381,6 +442,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection
@ -418,6 +482,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
mockTierOnePrice = 200;
const { wrapper } = setupMocks({});
@ -457,6 +524,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
mockTierOnePrice = 505;
const { wrapper } = setupMocks({});
@ -495,6 +565,9 @@ describe("quote.vue", () => {
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: null };
@ -533,6 +606,35 @@ describe("quote.vue", () => {
expect(wrapper.vm.lineItems.promos !== null).toBe(true);
});
test("On forward button action save supporting lineItems", async () => {
//Arrange
store.getters.payment = {
insuranceCoverage: {},
isInsurance: false,
};
store.getters.order = {
lineItems: [],
payment: {
parentAccountNumber: 167132,
},
};
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithSaving: jest.fn(),
},
route: { quote },
},
});
wrapper.vm.forwardButtonAction();
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith(
"saveSupportingItems",
wrapper.vm.lineItems.supportingItems,
false
);
});
});
function setupMocks({ customMountOptions }) {

View file

@ -29,6 +29,7 @@
:availableLineItems="availableLineItems"
:isInsuranceSelected="isInsuranceSelected"
@vapsItemsSelected="vapsItemsSelectedAction"
@servicePackageDiscountSelected="servicePackageDiscountSelectedAction"
:activePromos="lineItems.promos"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
@ -86,6 +87,7 @@ 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 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";
@ -105,7 +107,7 @@ 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";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
export default {
@ -155,11 +157,29 @@ export default {
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,
];
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
@ -278,6 +298,9 @@ export default {
vapsItemsSelectedAction(vapsItemsSelected) {
this.lineItems.vaps = vapsItemsSelected;
},
servicePackageDiscountSelectedAction(supportingItemsSelected) {
this.lineItems.supportingItems = supportingItemsSelected;
},
backButtonAction() {
vehicleQuestionsMixin.methods.navigateBack(this);
},
@ -315,7 +338,11 @@ export default {
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,

View file

@ -6,6 +6,12 @@ import {
mockProcessedCmsContent,
inputQuestionWidgetAnswers,
} from "./service-package-question-test-helper";
import {
containsLineItemWithPartType,
findLineItemsWithPartType,
} from "@/helpers/service-package-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings";
import { nextTick } from "vue";
jest.mock("@/store", () => ({
@ -45,7 +51,46 @@ describe("service-package-question.vue", () => {
partType: "RAIN DEFENSE",
price: 35.5,
},
{
partNumber: "DISC CASHSAVE70",
description: null,
partType: "SERVICE PACKAGE DISCOUNT",
price: -70,
},
],
lineItems: {
supportingItems: [
{
description: null,
id: "b0c68683-da85-46ba-bc86-2642bd9d5fdb",
kitPrice: 0,
laborAmount: 0,
partNumber: "RECYCLE FEE",
partType: "REPLACE FEE",
salesTax: null,
sellingPrice: 0,
},
{
description: null,
id: "5f205a07-3c08-4244-9935-76d63a7bcd19",
kitPrice: 0,
laborAmount: 395,
partNumber: "RECAL STATIC",
partType: "RECALIBRATION",
salesTax: null,
sellingPrice: 0,
},
{
description: null,
kitPrice: 0,
laborAmount: 0,
partNumber: "DISC CASHSAVE70",
partType: "SERVICE PACKAGE DISCOUNT",
salesTax: null,
sellingPrice: -70,
},
],
},
activePromos: [],
};
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
@ -68,6 +113,44 @@ describe("service-package-question.vue", () => {
},
]);
});
it("should return price when there is discount", () => {
const wrapper = setupMocks({});
const partType = partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
wrapper.vm.isServicePackageDiscountOnOrder = containsLineItemWithPartType(
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);
});
it("isServicePackageDiscountOnOrder returns true if it contains service package discount lineItems", () => {
// Arrange
const wrapper = setupMocks({});
// Assert
expect(wrapper.vm.isServicePackageDiscountOnOrder).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", () => {
// Arrange
mockProps.isInsuranceSelected = true;

View file

@ -69,12 +69,34 @@ export default {
selectedPackageName(newValue) {
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
const servicePackageDiscountPartInSelectedPackage =
this.getServicePackageDiscountPartForSelectedPackage(newValue);
let supportingItems = this.supportingLineItems;
if (servicePackageDiscountPartInSelectedPackage?.length > 0) {
supportingItems?.push(servicePackageDiscountPartInSelectedPackage[0]);
} else {
if (
supportingItems?.length > 0 &&
containsLineItemWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
supportingItems
)
) {
supportingItems = supportingItems.filter(
(item) => item.partType !== partTypeStrings.SERVICE_PACKAGE_DISCOUNT
);
}
}
this.$emit("servicePackageDiscountSelected", supportingItems);
},
},
computed: {
nullSafeAvailableLineItems() {
return this.availableLineItems ?? [];
},
supportingLineItems() {
return this.$store.getters.lineItems?.supportingItems;
},
servicePackageAnswers() {
const cmsWidgetName = this.isInsuranceSelected
? this.insuranceCmsWidgetName
@ -99,10 +121,17 @@ export default {
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName),
buttonAuxillaryCopy: this.getDiscountedPackagePriceString(answer.Name),
//The package supports service-package-discount if it has the text value in CMS
buttonAuxillaryCopy: this.getDiscountedPackagePriceString(
answer.Name,
this.isServicePackageDiscountOnOrder && answer.Text != ""
),
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
additionalButtonData: {
strikeThroughPrice: this.getPackagePriceString(answer.Name),
servicePackageDiscount:
this.isServicePackageDiscountOnOrder && answer.Text != "",
Text: answer.Text + this.getServicePackageDiscountPrice(),
},
}));
return modifiedAnswers;
@ -113,6 +142,12 @@ export default {
this.nullSafeAvailableLineItems
);
},
isServicePackageDiscountOnOrder() {
return containsLineItemWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.nullSafeAvailableLineItems
);
},
frontWipersApplicableForTierTwo() {
return shouldFrontWipersBeAvailable(
this.glassToReplace,
@ -159,6 +194,12 @@ export default {
isRepair() {
return this.$store.getters.order.damage.isRepair;
},
servicePackageDiscountParts() {
return findLineItemsWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.nullSafeAvailableLineItems
);
},
},
methods: {
processIfStatements,
@ -178,21 +219,35 @@ export default {
},
getPackagePriceString(packageName) {
const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, { discountedPrice: false })
this.getPackagePrice(packageName, {
discountedPrice: false,
servicePackageDiscount: false,
})
).toFixed(2);
return "$" + formattedPriceFloat;
},
getDiscountedPackagePriceString(packageName) {
getDiscountedPackagePriceString(packageName, servicePackageDiscount) {
const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, { discountedPrice: true })
this.getPackagePrice(packageName, { discountedPrice: true, servicePackageDiscount })
).toFixed(2);
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
},
getPackagePrice(packageName, { discountedPrice = false }) {
const lineItemsToPrice = [...this.nullSafeAvailableLineItems];
getPackagePrice(packageName, { discountedPrice = false, servicePackageDiscount = false }) {
var lineItemsToPrice = [...this.nullSafeAvailableLineItems];
//remove service package discount part
if (this.isServicePackageDiscountOnOrder) {
lineItemsToPrice = this.nullSafeAvailableLineItems.filter((item) => {
return item.partType != this.servicePackageDiscountParts[0].partType;
});
}
if (discountedPrice) {
lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos));
}
if (servicePackageDiscount) {
lineItemsToPrice.push(...this.servicePackageDiscountParts);
}
let priceFloat = this.isInsuranceSelected
? 0
: baseMixin.methods.getTierOnePackagePrice(
@ -203,9 +258,17 @@ export default {
return priceFloat;
},
getServicePackageDiscountPrice() {
if (this.isServicePackageDiscountOnOrder) {
let price = 0;
price += baseMixin.methods.getTotalLineItemPrice(
this.servicePackageDiscountParts[0]
);
return Math.abs(price);
}
},
getVapsPrice(packageName, applyPromoDiscounts = false) {
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
let price = 0;
vapsItems.forEach((item) => {
@ -261,6 +324,14 @@ export default {
return vapsLineItemsForSelectedPackage;
},
getServicePackageDiscountPartForSelectedPackage(packageName) {
const selectedPackage = this.servicePackageAnswers.filter(
(item) => item.value === packageName
);
if (selectedPackage?.[0]?.additionalButtonData?.servicePackageDiscount) {
return this.servicePackageDiscountParts;
} else return [];
},
combineLineItemsWithoutDuplicates(lineItemsOne, lineItemsTwo) {
const combinedLineItemArray = [...lineItemsTwo];
lineItemsOne.forEach((lineItemOne) => {

View file

@ -18,21 +18,6 @@ describe("service-package-radio.vue", () => {
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]));
});
it("Should include buttonLabelAuxillaryCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelAuxillaryCopy"]));
});
it("Should include buttonLabelSubCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
@ -123,7 +108,11 @@ const mockProps = {
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>",
buttonFooterCopy: "buttonFooterCopy test copy",
buttonAuxillaryCopy: "buttonAuxillaryCopy test copy",
additionalButtonData: { strikeThroughPrice: "strikeThroughPrice test copy" },
additionalButtonData: {
strikeThroughPrice: "strikeThroughPrice test copy",
servicePackageDiscount: true,
Text: "Special save",
},
};
function setupMocks({ mountOptionsMockData = {} }) {

View file

@ -1,24 +1,51 @@
<template>
<div
:class="[this.additionalButtonData.Text ? 'discount-text' : '']"
v-if="this.additionalButtonData.servicePackageDiscount"
v-html="this.additionalButtonData.Text"></div>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div
class="package-label"
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
this.additionalButtonData.servicePackageDiscount ? 'has-text' : '',
]"
for="testradio">
<div class="package-specs">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info">
<span
class="strikethrough-price"
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<span class="col md-6 service-package-discount">
<div
class="strikethrough-discount-price"
v-if="shouldDisplayStrikeThroughPrice"
v-html="this.additionalButtonData.strikeThroughPrice"></span>
<span v-html="this.buttonAuxillaryCopy"></span>*
v-html="this.additionalButtonData.strikeThroughPrice"></div>
<div class="discount-price" v-html="this.buttonAuxillaryCopy"></div>
</span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<div v-else>
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info">
<span
class="strikethrough-price"
v-if="shouldDisplayStrikeThroughPrice"
v-html="this.additionalButtonData.strikeThroughPrice"></span>
<span v-html="this.buttonAuxillaryCopy"></span>*
</span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<div class="hide-when-closed">
<!-- Parse the body text containing <ul> with logic -->
<ul>
@ -109,6 +136,18 @@ export default {
</script>
<style lang="scss">
.discount-text {
line-height: 36px;
display: flex;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
background-color: #469d2a;
align-items: center;
justify-content: center;
padding: 5px;
width: 100%;
color: white;
}
.package-main {
.package-wrapper {
margin: 0.5rem 0;
@ -139,6 +178,10 @@ export default {
&.has-subheader {
min-height: 80px;
}
&.has-text {
border-radius: 0 0 0.5rem 0.5rem;
border: 1px solid #469d2a !important;
}
&:before {
content: "";
@ -246,6 +289,28 @@ export default {
font-size: 0.75rem;
}
}
span {
&.service-package-discount {
color: #469d2a;
text-align: end;
font-weight: 500;
font-size: 13px;
div.strikethrough-discount-price {
text-decoration: line-through;
margin-right: 0;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: $gray-550;
padding-right: 2px;
}
div.discount-price {
color: #469d2a;
font-weight: 600;
font-size: 26px;
}
}
}
ul {
margin: 1rem 0 0 -15px;
padding: 0;

View file

@ -158,12 +158,17 @@ export default {
includeTax
);
}
const order = store.getters.hasSubmittedOrder
? store.getters.submittedOrder
: store.getters.order;
if (
store.getters.payment.insuranceCoverage.isVerified &&
!store.getters.policy.isNoComp &&
!store.getters.policy.isItac
order.payment.insuranceCoverage.isVerified &&
!order.policy.isNoComp &&
!order.policy.isItac
) {
amountDue += store.getters.policy.currentDeductible;
amountDue += order.policy.currentDeductible;
}
return ((amountDue * 100) / 100).toFixed(2);

View file

@ -175,9 +175,66 @@ describe("baseMixin.js", () => {
promos: [],
};
const payment = { insuranceCoverage: { isVerified: true } };
const policy = { isNoComp: false, isItac: false, currentDeductible: 250 };
store.getters = {
payment: { insuranceCoverage: { isVerified: true } },
policy: { isNoComp: false, isItac: false, currentDeductible: 250 },
payment: payment,
policy: policy,
order: {
payment: payment,
policy: policy,
},
hasSubmittedOrder: false,
};
var amtDue = mixIn.methods.getAmountDue(lineItems);
expect(amtDue).toEqual("303.00");
});
test("getAmountDue should function with a submitted order", () => {
const mixIn = getMixInInstance({});
const lineItems = {
glassParts: [],
supportingItems: [],
vaps: [
{
cartItemType: "FRONT WIPERS",
description: 'WIPER BLADE STANDARD 18"',
kitPrice: 0,
laborAmount: 0,
partNumber: "WB18",
partType: "FRONT WIPER",
salesTax: 2.0,
sellingPrice: 30.0,
},
{
cartItemType: "FRONT WIPERS",
description: 'WIPER BLADE STANDARD 26"',
kitPrice: 0,
laborAmount: 0,
partNumber: "WB26",
partType: "FRONT WIPER",
salesTax: 1.0,
sellingPrice: 20.0,
},
],
promos: [],
};
const payment = { insuranceCoverage: { isVerified: true } };
const policy = { isNoComp: false, isItac: false, currentDeductible: 250 };
store.getters = {
payment: {},
policy: {},
order: {},
submittedOrder: {
payment: payment,
policy: policy,
},
hasSubmittedOrder: true,
};
var amtDue = mixIn.methods.getAmountDue(lineItems);
@ -241,9 +298,17 @@ describe("baseMixin.js", () => {
promos: [],
};
const payment = { isInsurance: null, insuranceCoverage: { isVerified: null } };
const policy = { isNoComp: false, isItac: false, currentDeductible: 0 };
store.getters = {
payment: { isInsurance: null, insuranceCoverage: { isVerified: null } },
policy: { isNoComp: false, isItac: false, currentDeductible: 0 },
payment: payment,
policy: policy,
order: {
payment: payment,
policy: policy,
},
hasSubmittedOrder: false,
};
var amtDue = mixIn.methods.getAmountDue(lineItems);

View file

@ -1422,6 +1422,26 @@ export const actions = {
pageNameToLog: pageNameToLog,
});
},
getServicePackageDiscountPart(context, { pageNameToLog }) {
const damage = context.getters.damage;
const damageType = damage.isRepair ? "Repair" : "Replace";
const glassPieces = convertGlassPieceNamingForApi(damage.glassToReplace);
return globalMethods
.callHttpClient({
method: endpoints.GetServicePackageDiscountPart.method,
endpoint: endpoints.GetServicePackageDiscountPart.url,
payload: {
glassPieces: glassPieces,
damageType: damageType,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
})
.then((response) => {
return response;
});
},
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
const lineItemsWithOnlyPartNumbers = context.getters.order.lineItems.supportingItems?.map(