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', RAIN_DEFENSE: 'RAIN DEFENSE',
RECALIBRATION: 'RECALIBRATION', RECALIBRATION: 'RECALIBRATION',
REPLACE_FEE: 'REPLACE FEE', REPLACE_FEE: 'REPLACE FEE',
RECYCLE_FEE: 'RECYCLE FEE',
MOBILE_FEE: 'MOBILE FEE', MOBILE_FEE: 'MOBILE FEE',
REPAIR_FEE: 'REPAIR 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', () => { describe('getPriceOfLineItems', () => {
test('Returns zero when no line items', () => { 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'; import packageNames from '@/constants/package-names';
export function findLineItemsWithPartType(typeToFind, itemsToSearch) { export function findLineItemsWithPartType(typeToFind, itemsToSearch) {
const partTypeMatches = itemsToSearch?.filter((lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase()); return itemsToSearch
return partTypeMatches; ?.filter((lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase());
} }
export function containsLineItemWithPartType(typeToFind, itemsToSearch) { export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
@ -55,7 +55,6 @@ export function shouldFrontWipersBeAvailable(
export function shouldRearWipersBeAvailable( export function shouldRearWipersBeAvailable(
glassToReplace, glassToReplace,
availableLineItems, availableLineItems,
isRepair,
targetTier targetTier
) { ) {
const rearWiperIsAvailable = containsLineItemWithPartType( const rearWiperIsAvailable = containsLineItemWithPartType(
@ -103,11 +102,13 @@ export function shouldRainDefenseBeAvailable(
const rearWipersInTierTwo = shouldRearWipersBeAvailable( const rearWipersInTierTwo = shouldRearWipersBeAvailable(
glassToReplace, glassToReplace,
availableLineItems, availableLineItems,
isRepair,
packageNames.TIER_TWO packageNames.TIER_TWO
); );
return isTierThree && !(rearWipersInTierTwo && !frontWipersInTierTwo && frontWipersInTierThree); return isTierThree
&& !(rearWipersInTierTwo
&& !frontWipersInTierTwo
&& frontWipersInTierThree);
} }
export function getPackageContents(glassToReplace, availableLineItems, isRepair, targetTier) { export function getPackageContents(glassToReplace, availableLineItems, isRepair, targetTier) {
@ -117,7 +118,7 @@ export function getPackageContents(glassToReplace, availableLineItems, isRepair,
vaps.push(partTypeStrings.FRONT_WIPER); vaps.push(partTypeStrings.FRONT_WIPER);
} }
if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) { if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, targetTier)) {
vaps.push(partTypeStrings.REAR_WIPER); 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 { Object {
"baseServiceLineItems": Array [], "baseServiceLineItems": Array [],
"deductible": null, "deductible": null,
"glassToReplace": Array [],
"isExpanded": false, "isExpanded": false,
"isRepair": null,
"vapsInOrder": Array [],
"widget": Object { "widget": Object {
"amountDue": "AmountDueTextWidget", "amountDue": "AmountDueTextWidget",
"basePrice": "BasePriceWidget", "basePrice": "BasePriceWidget",
@ -12,7 +15,9 @@ Object {
"mobileFee": "MobileServiceWidget", "mobileFee": "MobileServiceWidget",
"recycleFee": "RecycleFeeWidget", "recycleFee": "RecycleFeeWidget",
"salesTax": "SalesTaxWidget", "salesTax": "SalesTaxWidget",
"servicePackage": "ServicePackageTitle",
"subtotal": "SubtotalWidget", "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" id="cart-deductible-or-base-price"
class="cart-item cart-gray"> class="cart-item cart-gray">
<div <div
v-if="showDeductibleLineItem" v-if="showDeductibleCartItem"
id="cart-deductible" id="cart-deductible"
class="d-flex justify-content-between align-items-center"> class="d-flex justify-content-between align-items-center">
<span id="deductible-label">{{ deductibleLabel }}</span> <span id="deductible-label">{{ deductibleLabel }}</span>
@ -39,35 +39,69 @@
</div> </div>
<!-- Packaged Cart Items --> <!-- 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 --> <!-- Nonpackaged Cart Items -->
<!-- This will be a loop when this is implemented --> <!-- This will be a loop when this is implemented -->
<div class="non-packaged-cart-item"> <div class="non-packaged-cart-item">
<div <div
v-if="mobileFeeCartItem"
id="mobile-fee" id="mobile-fee"
class="cart-item" class="cart-item">
v-if="mobileFeeCartItem">
<div <div
id="mobile-fee-item" id="mobile-fee-item"
class="d-flex justify-content-between align-items-center"> 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> <span id="mobile-fee-value">{{ getDisplayed(mobileFeeCartItem.subTotal) }}</span>
</div> </div>
</div> </div>
<div <div
v-if="recycleFeeCartItem"
id="recycle-fee" id="recycle-fee"
class="cart-item cart-gray" class="cart-item cart-gray">
v-if="recycleFeeCartItem">
<div <div
id="recycle-fee-item" id="recycle-fee-item"
class="d-flex justify-content-between align-items-center"> class="d-flex justify-content-between align-items-center">
<span id="recycle-fee-label"> <span id="recycle-fee-label">
<textLink <textLink
linkType="text" linkType="text"
:text="recycleFeeCartItem.name" :text="recycleFeeCartItem?.name ?? ''"
href="javascript:void(0)" href="javascript:void(0)"
@clickEvent="openModal(recyclingModalCmsWidgetName)" /> @clickEvent="openModal(recyclingModalCmsWidgetName)" />
</span> </span>
<span id="recycle-fee-value">{{ getDisplayed(recycleFeeCartItem.subTotal) }}</span> <span id="recycle-fee-value">{{ getDisplayed(recycleFeeCartItem.subTotal) }}</span>
</div> </div>
@ -102,7 +136,7 @@
</div> </div>
<contentGroupModal <contentGroupModal
ref="RecycleModal" ref="RecycleModal"
cmsWidgetName="RecycleModal"> :cmsWidgetName="recyclingModalCmsWidgetName">
<textBlock cmsWidgetName="RecycleTextBlock" /> <textBlock cmsWidgetName="RecycleTextBlock" />
</contentGroupModal> </contentGroupModal>
</div> </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 textLink from '@/ux-components/text-link/text-link.vue';
import getPriceOfLineItems from '@/helpers/price-calculator.js'; import getPriceOfLineItems from '@/helpers/price-calculator.js';
import { formatAmountInDollars } from '@/helpers/text-helper.js'; import { formatAmountInDollars } from '@/helpers/text-helper.js';
import baseMixin from '@/mixins/base-mixin'; import { getHighestFullySatisfiedTier, getPackageContents } from '@/helpers/service-package-helper.js';
import partTypeStrings from '@/constants/part-type-strings';
// 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'; import widgetFields from '@/constants/cms-widget-fields.js';
const VERIFYING_COVERAGE = 'Verifying coverage'; const VERIFYING_COVERAGE = 'Verifying coverage';
export default { export default {
name: 'cart-dropdown', name: 'cart-dropdown',
components: { contentGroupModal, components: {
contentGroupModal,
textBlock, textBlock,
textLink }, textLink
props: {
recyclingModalCmsWidgetName: String,
showAsPaid: Boolean
}, },
setup() { props: {
const mainStore = useMainStore(); showAsPaid: Boolean,
return { mainStore }; readOnly: Boolean,
recyclingModalCmsWidgetName: String,
availableVaps: Array
}, },
data() { data() {
const { currentDeductible } = useMainStore().order; const { currentDeductible } = useMainStore().order;
const { supportingItems, glassParts, otherParts } = useMainStore().lineItems; const { glassToReplace, isRepair } = useMainStore().damage;
const baseServiceLineItems = [ const { supportingItems, glassParts, otherParts, vaps } = useMainStore().lineItems;
...(supportingItems ?? []),
...(glassParts ?? []),
...(otherParts ?? [])
];
return { return {
isRepair,
glassToReplace: glassToReplace ?? [],
vapsInOrder: vaps ?? [],
baseServiceLineItems: [
...(supportingItems ?? []),
...(glassParts ?? []),
...(otherParts ?? [])
],
deductible: currentDeductible, deductible: currentDeductible,
baseServiceLineItems,
isExpanded: false, isExpanded: false,
widget: { widget: {
amountDue: 'AmountDueTextWidget', amountDue: 'AmountDueTextWidget',
@ -154,24 +195,28 @@ export default {
subtotal: 'SubtotalWidget', subtotal: 'SubtotalWidget',
salesTax: 'SalesTaxWidget', salesTax: 'SalesTaxWidget',
recycleFee: 'RecycleFeeWidget', recycleFee: 'RecycleFeeWidget',
mobileFee: 'MobileServiceWidget' mobileFee: 'MobileServiceWidget',
servicePackage: 'ServicePackageTitle',
vapsItemDescriptions: 'VapsItemDescriptions'
} }
}; };
}, },
computed: { computed: {
showDeductibleCartItem() {
return !useMainStore().isNoComp && !useMainStore().policy.isITAC;
},
baseServicePrice() { baseServicePrice() {
return getPriceOfLineItems(this.baseServiceLineItems); return getPriceOfLineItems(this.baseServiceLineItems);
}, },
showDeductibleLineItem() {
return !useMainStore().isNoComp && !useMainStore().policy.isITAC;
},
isUnverified() { isUnverified() {
return this.showDeductibleLineItem return !useMainStore().isNoComp && !useMainStore().policy.isITAC
&& (this.deductible == null || !useMainStore().isVerifiedCoverageStatus); && (this.deductible == null || !useMainStore().isVerifiedCoverageStatus);
}, },
subTotal() { subTotal() {
// TODO partial calculation for now const basePrice = !useMainStore().isNoComp && !useMainStore().policy.isITAC
return this.showDeductibleLineItem ? this.deductible : this.baseServicePrice; ? this.deductible
: this.baseServicePrice;
return basePrice + this.packagePrice;
}, },
salesTax() { salesTax() {
return 0; // TODO return 0; // TODO
@ -181,6 +226,53 @@ export default {
? 0 ? 0
: this.subTotal + this.salesTax; : 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() { amountDueLabel() {
return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT); return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}, },
@ -196,17 +288,39 @@ export default {
salesTaxLabel() { salesTaxLabel() {
return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT); return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}, },
recycleFeeCartItem() { servicePackageNames() {
return this.getCartItem( return this.getCmsContent(
this.getCmsContent(this.widget.recycleFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), this.widget.servicePackage,
this.mainStore.order.lineItems.supportingItems?.find((lineItem) => lineItem.partType === partTypeStrings.REPLACE_FEE) 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() { mobileFeeCartItem() {
return this.getCartItem( const mobileFeeLineItem = useMainStore().lineItems.mobileFee;
this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), return mobileFeeLineItem
this.mainStore.order.lineItems.mobileFee ? this.getCartItem(
); this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
[mobileFeeLineItem]
)
: null;
} }
}, },
methods: { methods: {
@ -217,23 +331,50 @@ export default {
this.isExpanded = !this.isExpanded; this.isExpanded = !this.isExpanded;
}, },
getDisplayed(amount) { getDisplayed(amount) {
return this.isUnverified && this.showDeductibleLineItem return this.isUnverified
&& !useMainStore().isNoComp
&& !useMainStore().policy.isITAC
? VERIFYING_COVERAGE ? VERIFYING_COVERAGE
: formatAmountInDollars(amount); : formatAmountInDollars(amount);
}, },
getCartItem(label, lineItem) { getCartItem(label, lineItems) {
let cartItem = null; let cartItem = null;
if (lineItem) { if (lineItems && lineItems.length > 0) {
cartItem = { cartItem = {
name: label, name: label,
subTotal: baseMixin.methods.getTotalLineItemPrice(lineItem, false), subTotal: getPriceOfLineItems(lineItems),
salesTax: lineItem.salesTax ?? 0 salesTax: lineItems.reduce(
(accumulator, lineItem) => accumulator + lineItem.salesTax,
0
)
}; };
} }
return cartItem; 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 formatAmountInDollars
} }
}; };
@ -242,6 +383,10 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss"; @import "@/styles/ux-variables-svg-strings.scss";
.force-no-top-margin {
margin-top: 0 !important;
}
.color-black { .color-black {
color: $black; color: $black;
} }
@ -254,6 +399,11 @@ export default {
color: $darker-gray; color: $darker-gray;
} }
.cart-line-item {
padding: 0.25rem 1.5rem;
color: $darker-gray;
}
.cart-table { .cart-table {
max-height: 0; max-height: 0;
transition: all 350ms ease-in; transition: all 350ms ease-in;
@ -261,6 +411,12 @@ export default {
visibility: hidden; visibility: hidden;
margin-top: 1rem; margin-top: 1rem;
:deep(a) {
padding-bottom: 0 !important;
line-height: 1.625 !important;
text-underline-offset: auto !important;
}
.cart-item { .cart-item {
padding: 0.25rem 1.5rem; padding: 0.25rem 1.5rem;
} }

View file

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

View file

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