Merge pull request #581 from Safelite/feature/digital/SSR-1133

Feature/digital/ssr 1133
This commit is contained in:
michaela-brydon-safelite 2024-03-29 11:21:23 -04:00 committed by GitHub
commit 855bfaee05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 2116 additions and 185 deletions

View file

@ -0,0 +1,5 @@
const partNumberStrings = Object.freeze({
RECYCLE_FEE: 'RECYCLE FEE'
});
export default partNumberStrings;

View file

@ -4,7 +4,6 @@
RAIN_DEFENSE: 'RAIN DEFENSE',
RECALIBRATION: 'RECALIBRATION',
REPLACE_FEE: 'REPLACE FEE',
RECYCLE_FEE: 'RECYCLE FEE',
MOBILE_FEE: 'MOBILE FEE',
REPAIR_FEE: 'REPAIR FEE'
});

View file

@ -1,4 +1,4 @@
import getPriceOfLineItems from "@/helpers/price-calculator.js";
import getPriceOfLineItems from '@/helpers/price-calculator.js';
describe('getPriceOfLineItems', () => {
test('Returns zero when no line items', () => {

View file

@ -3,8 +3,8 @@ import damageLocationsSelected from '@/constants/damage-locations-selected';
import packageNames from '@/constants/package-names';
export function findLineItemsWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = itemsToSearch?.filter((lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase());
return partTypeMatches;
return itemsToSearch
?.filter((lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase());
}
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
@ -55,7 +55,6 @@ export function shouldFrontWipersBeAvailable(
export function shouldRearWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
targetTier
) {
const rearWiperIsAvailable = containsLineItemWithPartType(
@ -103,11 +102,13 @@ export function shouldRainDefenseBeAvailable(
const rearWipersInTierTwo = shouldRearWipersBeAvailable(
glassToReplace,
availableLineItems,
isRepair,
packageNames.TIER_TWO
);
return isTierThree && !(rearWipersInTierTwo && !frontWipersInTierTwo && frontWipersInTierThree);
return isTierThree
&& !(rearWipersInTierTwo
&& !frontWipersInTierTwo
&& frontWipersInTierThree);
}
export function getPackageContents(glassToReplace, availableLineItems, isRepair, targetTier) {
@ -117,7 +118,7 @@ export function getPackageContents(glassToReplace, availableLineItems, isRepair,
vaps.push(partTypeStrings.FRONT_WIPER);
}
if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, targetTier)) {
vaps.push(partTypeStrings.REAR_WIPER);
}

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,10 @@ exports[`cart-dropdown component initial data rendered as expected 1`] = `
Object {
"baseServiceLineItems": Array [],
"deductible": null,
"glassToReplace": Array [],
"isExpanded": false,
"isRepair": null,
"vapsInOrder": Array [],
"widget": Object {
"amountDue": "AmountDueTextWidget",
"basePrice": "BasePriceWidget",
@ -12,7 +15,9 @@ Object {
"mobileFee": "MobileServiceWidget",
"recycleFee": "RecycleFeeWidget",
"salesTax": "SalesTaxWidget",
"servicePackage": "ServicePackageTitle",
"subtotal": "SubtotalWidget",
"vapsItemDescriptions": "VapsItemDescriptions",
},
}
`;

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@
id="cart-deductible-or-base-price"
class="cart-item cart-gray">
<div
v-if="showDeductibleLineItem"
v-if="showDeductibleCartItem"
id="cart-deductible"
class="d-flex justify-content-between align-items-center">
<span id="deductible-label">{{ deductibleLabel }}</span>
@ -39,35 +39,69 @@
</div>
<!-- Packaged Cart Items -->
<div
id="cart-service-package"
class="color-darker-gray px-5">
<div
id="service-package-name"
class="py-1 d-flex justify-content-between align-items-center">
<textBlock
:cmsWidgetName="servicePackageLabelWidget"
class="force-no-top-margin" />
<span id="service-package-price">{{ formatAmountInDollars(packagePrice) }}</span>
</div>
<div
id="service-items"
class="ps-4">
<div
v-for="(item, i) in servicePackageCartItems"
:key="i"
class="packaged-cart-item">
<div class="py-1 d-flex justify-content-between align-items-center">
<span>{{ item?.name ?? '' }}</span>
<textLink
v-if="!readOnly || !showAsPaid"
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="removeItem">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
</div>
</div>
</div>
</div>
<!-- Nonpackaged Cart Items -->
<!-- This will be a loop when this is implemented -->
<div class="non-packaged-cart-item">
<div
v-if="mobileFeeCartItem"
id="mobile-fee"
class="cart-item"
v-if="mobileFeeCartItem">
class="cart-item">
<div
id="mobile-fee-item"
class="d-flex justify-content-between align-items-center">
<span id="mobile-fee-label">{{ mobileFeeCartItem.name }}</span>
<span id="mobile-fee-label">{{ mobileFeeCartItem?.name ?? '' }}</span>
<span id="mobile-fee-value">{{ getDisplayed(mobileFeeCartItem.subTotal) }}</span>
</div>
</div>
<div
v-if="recycleFeeCartItem"
id="recycle-fee"
class="cart-item cart-gray"
v-if="recycleFeeCartItem">
class="cart-item cart-gray">
<div
id="recycle-fee-item"
class="d-flex justify-content-between align-items-center">
<span id="recycle-fee-label">
<textLink
linkType="text"
:text="recycleFeeCartItem.name"
href="javascript:void(0)"
@clickEvent="openModal(recyclingModalCmsWidgetName)" />
linkType="text"
:text="recycleFeeCartItem?.name ?? ''"
href="javascript:void(0)"
@clickEvent="openModal(recyclingModalCmsWidgetName)" />
</span>
<span id="recycle-fee-value">{{ getDisplayed(recycleFeeCartItem.subTotal) }}</span>
</div>
@ -102,7 +136,7 @@
</div>
<contentGroupModal
ref="RecycleModal"
cmsWidgetName="RecycleModal">
:cmsWidgetName="recyclingModalCmsWidgetName">
<textBlock cmsWidgetName="RecycleTextBlock" />
</contentGroupModal>
</div>
@ -116,36 +150,43 @@ import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
import getPriceOfLineItems from '@/helpers/price-calculator.js';
import { formatAmountInDollars } from '@/helpers/text-helper.js';
import baseMixin from '@/mixins/base-mixin';
import partTypeStrings from '@/constants/part-type-strings';
import { getHighestFullySatisfiedTier, getPackageContents } from '@/helpers/service-package-helper.js';
// Constants
import partTypeStrings from '@/constants/part-type-strings.js';
import partNumberStrings from '@/constants/part-number-strings.js';
import widgetFields from '@/constants/cms-widget-fields.js';
const VERIFYING_COVERAGE = 'Verifying coverage';
export default {
name: 'cart-dropdown',
components: { contentGroupModal,
components: {
contentGroupModal,
textBlock,
textLink },
props: {
recyclingModalCmsWidgetName: String,
showAsPaid: Boolean
textLink
},
setup() {
const mainStore = useMainStore();
return { mainStore };
props: {
showAsPaid: Boolean,
readOnly: Boolean,
recyclingModalCmsWidgetName: String,
availableVaps: Array
},
data() {
const { currentDeductible } = useMainStore().order;
const { supportingItems, glassParts, otherParts } = useMainStore().lineItems;
const baseServiceLineItems = [
...(supportingItems ?? []),
...(glassParts ?? []),
...(otherParts ?? [])
];
const { glassToReplace, isRepair } = useMainStore().damage;
const { supportingItems, glassParts, otherParts, vaps } = useMainStore().lineItems;
return {
isRepair,
glassToReplace: glassToReplace ?? [],
vapsInOrder: vaps ?? [],
baseServiceLineItems: [
...(supportingItems ?? []),
...(glassParts ?? []),
...(otherParts ?? [])
],
deductible: currentDeductible,
baseServiceLineItems,
isExpanded: false,
widget: {
amountDue: 'AmountDueTextWidget',
@ -154,24 +195,28 @@ export default {
subtotal: 'SubtotalWidget',
salesTax: 'SalesTaxWidget',
recycleFee: 'RecycleFeeWidget',
mobileFee: 'MobileServiceWidget'
mobileFee: 'MobileServiceWidget',
servicePackage: 'ServicePackageTitle',
vapsItemDescriptions: 'VapsItemDescriptions'
}
};
},
computed: {
showDeductibleCartItem() {
return !useMainStore().isNoComp && !useMainStore().policy.isITAC;
},
baseServicePrice() {
return getPriceOfLineItems(this.baseServiceLineItems);
},
showDeductibleLineItem() {
return !useMainStore().isNoComp && !useMainStore().policy.isITAC;
},
isUnverified() {
return this.showDeductibleLineItem
return !useMainStore().isNoComp && !useMainStore().policy.isITAC
&& (this.deductible == null || !useMainStore().isVerifiedCoverageStatus);
},
subTotal() {
// TODO partial calculation for now
return this.showDeductibleLineItem ? this.deductible : this.baseServicePrice;
const basePrice = !useMainStore().isNoComp && !useMainStore().policy.isITAC
? this.deductible
: this.baseServicePrice;
return basePrice + this.packagePrice;
},
salesTax() {
return 0; // TODO
@ -181,6 +226,53 @@ export default {
? 0
: this.subTotal + this.salesTax;
},
availableLineItems() {
const { glassParts, supportingItems } = useMainStore().lineItems;
return [
...(glassParts ?? []),
...(supportingItems ?? []),
...(this.availableVaps ?? [])
];
},
servicePackageTier() {
return getHighestFullySatisfiedTier(
this.glassToReplace,
this.availableLineItems,
this.isRepair,
this.vapsInOrder
);
},
servicePackageCartItems() {
const packageContentTypes = getPackageContents(
this.glassToReplace,
this.availableLineItems,
this.isRepair,
this.servicePackageTier
) ?? [];
const items = [];
if (packageContentTypes.includes(partTypeStrings.FRONT_WIPER) ?? this.frontWipersCartItem) {
items.push(this.frontWipersCartItem);
}
if (packageContentTypes.includes(partTypeStrings.REAR_WIPER) ?? this.rearWipersCartItem) {
items.push(this.rearWipersCartItem);
}
if (packageContentTypes.includes(partTypeStrings.RAIN_DEFENSE) ?? this.rainDefenseCartItem) {
items.push(this.rainDefenseCartItem);
}
return items;
},
frontWipersCartItem() {
return this.getCartItemForVapsPart(partTypeStrings.FRONT_WIPER);
},
rearWipersCartItem() {
return this.getCartItemForVapsPart(partTypeStrings.REAR_WIPER);
},
rainDefenseCartItem() {
return this.getCartItemForVapsPart(partTypeStrings.RAIN_DEFENSE);
},
packagePrice() {
return getPriceOfLineItems(this.vapsInOrder);
},
amountDueLabel() {
return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
@ -196,17 +288,39 @@ export default {
salesTaxLabel() {
return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
recycleFeeCartItem() {
return this.getCartItem(
this.getCmsContent(this.widget.recycleFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
this.mainStore.order.lineItems.supportingItems?.find((lineItem) => lineItem.partType === partTypeStrings.REPLACE_FEE)
servicePackageNames() {
return this.getCmsContent(
this.widget.servicePackage,
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
);
},
servicePackageLabelWidget() {
if (!this.servicePackageNames) {
return '';
}
const currentPackage = this.servicePackageNames
?.find((entry) => entry?.Name === this.servicePackageTier);
return currentPackage?.SubWidgetName ?? '';
},
recycleFeeCartItem() {
const recycleFeeLineItem = useMainStore().lineItems.supportingItems
?.find((lineItem) => lineItem.partNumber === partNumberStrings.RECYCLE_FEE);
return recycleFeeLineItem
? this.getCartItem(
this.getCmsContent(this.widget.recycleFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
[recycleFeeLineItem]
)
: null;
},
mobileFeeCartItem() {
return this.getCartItem(
this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
this.mainStore.order.lineItems.mobileFee
);
const mobileFeeLineItem = useMainStore().lineItems.mobileFee;
return mobileFeeLineItem
? this.getCartItem(
this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
[mobileFeeLineItem]
)
: null;
}
},
methods: {
@ -217,23 +331,50 @@ export default {
this.isExpanded = !this.isExpanded;
},
getDisplayed(amount) {
return this.isUnverified && this.showDeductibleLineItem
return this.isUnverified
&& !useMainStore().isNoComp
&& !useMainStore().policy.isITAC
? VERIFYING_COVERAGE
: formatAmountInDollars(amount);
},
getCartItem(label, lineItem) {
getCartItem(label, lineItems) {
let cartItem = null;
if (lineItem) {
if (lineItems && lineItems.length > 0) {
cartItem = {
name: label,
subTotal: baseMixin.methods.getTotalLineItemPrice(lineItem, false),
salesTax: lineItem.salesTax ?? 0
subTotal: getPriceOfLineItems(lineItems),
salesTax: lineItems.reduce(
(accumulator, lineItem) => accumulator + lineItem.salesTax,
0
)
};
}
return cartItem;
},
getCartItemForVapsPart(partType) {
const label = this.getCmsContentForVapsType(partType);
const lineItems = this.vapsInOrder
?.filter((vapsLineItem) => vapsLineItem.partType === partType);
return this.getCartItem(label, lineItems);
},
removeItem() {
// TODO in later ticket
},
getCmsContentForVapsType(vapsType) {
const vapsItemDescriptions = this.getCmsContent(
this.widget.vapsItemDescriptions,
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
);
if (!vapsItemDescriptions) {
return '';
}
const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsType);
return vapsTypeDescription?.Text ?? '';
},
formatAmountInDollars
}
};
@ -242,6 +383,10 @@ export default {
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
.force-no-top-margin {
margin-top: 0 !important;
}
.color-black {
color: $black;
}
@ -254,6 +399,11 @@ export default {
color: $darker-gray;
}
.cart-line-item {
padding: 0.25rem 1.5rem;
color: $darker-gray;
}
.cart-table {
max-height: 0;
transition: all 350ms ease-in;
@ -261,6 +411,12 @@ export default {
visibility: hidden;
margin-top: 1rem;
:deep(a) {
padding-bottom: 0 !important;
line-height: 1.625 !important;
text-underline-offset: auto !important;
}
.cart-item {
padding: 0.25rem 1.5rem;
}

View file

@ -425,7 +425,7 @@ describe('coverageStatement.vue-working', () => {
// Assert
expect(result).toBeFalsy();
});
test('returns true when policyLookupSuccessful, isNoComp false, and deductible over service price', () => {
test('returns true when policyLookupSuccessful, isNoComp false, and deductible over service price', () => {
// Arrange
const { wrapper } = getMountedComponent(verifiedDeductibleStoreState);

View file

@ -21,7 +21,10 @@
<hr class="my-0" />
<cartDropdown
:showAsPaid="false"
recyclingModalCmsWidgetName="RecycleModal" />
:readOnly="false"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle"
:availableVaps="availableVaps" />
<hr class="mt-0 mb-5" />
<div>Pia Alert Placeholder</div>
<paymentMethodQuestion
@ -58,7 +61,8 @@ import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { paymentMethods } from '@/constants/payment-method-constants';
import globalRules from '@/constants/global-rules';
import { Form } from 'vee-validate';
import issPageValues from '@/router/router-constants/issPage-values';
import bailoutMessage from '@/constants/bailoutMessage';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
export default {
@ -78,6 +82,8 @@ export default {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const reviewDropdownPromise = reviewDropdown.methods.loadInitialData();
const wipersPromise = useMainStore().getWipers();
const rainDefensePromise = useMainStore().getRainDefense();
const promiseResultMap = [
{
@ -87,21 +93,47 @@ export default {
{
resultKey: 'reviewDropdownData',
promise: reviewDropdownPromise
},
{
resultKey: 'wipers',
promise: wipersPromise
},
{
resultKey: 'rainDefense',
promise: rainDefensePromise
}
];
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
const unpricedVaps = [...resultMap.wipers, resultMap.rainDefense];
vm.updateFooterButtonText(vm.customCallToActionButtonCopy);
let hasBailedOut = false;
const pricedVaps = await useMainStore().getPriceOrderItems(unpricedVaps)
.catch((err) => {
useMainStore().setBailout(
to,
bailoutMessage.pricingResponseError(
unpricedVaps.map((li) => li.partNumber),
{ code: err.code, message: err.message, data: err.data }
)
);
hasBailedOut = true;
next(`/?issPage=${issPageValues.BAILOUT_PAGE}`);
});
vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
});
if (!hasBailedOut) {
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.updateFooterButtonText(vm.customCallToActionButtonCopy);
vm.$refs.reviewDropdown.initializeComponent(resultMap.reviewDropdownData);
vm.setAvailableVaps(pricedVaps ?? []);
});
}
},
data() {
return {
availableVaps: [],
paymentMethodInternalModel: this.getPaymentMethodFromStore(),
rules: {
optionRequired: globalRules.OPTION_REQUIRED
@ -223,6 +255,9 @@ export default {
this.$route
);
}
},
setAvailableVaps(vaps) {
this.availableVaps = vaps;
}
}
};

View file

@ -97,6 +97,7 @@ export default {
}
},
methods: {
// TODO maybe move to shared location "loadAvailableVaps"
loadInitialData() {
const wipersPromise = useMainStore().getWipers();
const rainDefensePromise = useMainStore().getRainDefense();