Merge branch 'develop' into feature/jnou/fix-iss-regression

This commit is contained in:
JennyNou 2026-02-12 15:56:39 -05:00
commit 2bedd33d56
27 changed files with 846 additions and 999 deletions

View file

@ -56,4 +56,4 @@ module.exports = {
}
}
}
};
};

View file

@ -18,7 +18,7 @@ pr:
resources:
containers:
- container: node
image: ghcr.io/safelite/node-build:18
image: ghcr.io/safelite/node-build:20
endpoint: GitHub Docker
- container: awscli
image: ghcr.io/safelite/awscli2-build:3.9

View file

@ -1,7 +1,8 @@
const cartItemType = Object.freeze({
RECYCLE_FEE: 'RECYCLE FEE',
MOBILE_FEE: 'MOBILE FEE',
VAP: 'VAP'
VAP: 'VAP',
WARRANTY: 'WARRANTY'
});
export default cartItemType;

View file

@ -81,7 +81,16 @@ const endpoints = Object.freeze({
method: 'POST'
},
GetProviders: {
url: `${LOCATION_BASE_URL}/providers`,
url: (
zipCode,
damageType,
radius,
parentAccountNumber,
safeliteOnly,
carId,
recalPartNumber,
isBigTruck
) => `${LOCATION_BASE_URL}/providers/${zipCode}/${damageType}/${radius}/${parentAccountNumber}/${safeliteOnly}/${carId}/${recalPartNumber != null ? `${recalPartNumber}/` : ''}${isBigTruck}`,
method: 'GET'
},
GetCapabilityQuestions: {

View file

@ -7,7 +7,9 @@
RECALIBRATION: 'RECALIBRATION',
REPLACE_FEE: 'REPLACE FEE',
MOBILE_FEE: 'MOBILE FEE',
REPAIR_FEE: 'REPAIR FEE'
REPAIR_FEE: 'REPAIR FEE',
WARRANTY: 'WARRANTY',
MOLDING: 'MOULDING'
});
export default partTypeStrings;

View file

@ -71,6 +71,26 @@ export async function isGlassAvailableForCarId(carId) {
}
}
export function getGlassList(glassPieces) {
let names = '';
switch (glassPieces.length)
{
case 0:
break;
case 1:
names = glassPieces[0];
break;
case 2:
names = glassPieces.join(' and ');
break;
default:
names = glassPieces.slice(0, -1).join(', ') + ', and ' + glassPieces.slice(-1);
break;
}
return names.toLowerCase();
}
/**
* Commented code are copied directly from DigitalConsumer.FixMyGlass
* and have not been adjusted for ISS.

View file

@ -7,19 +7,21 @@ Object {
"MOBILE_FEE": "MOBILE FEE",
"RECYCLE_FEE": "RECYCLE FEE",
"VAP": "VAP",
"WARRANTY": "WARRANTY",
},
"isExpanded": false,
"widget": Object {
"amountDue": "AmountDueTextWidget",
"amountPaid": "AmountPaidTextWidget",
"basePrice": "BasePriceWidget",
"deductible": "DeductibleWidget",
"guaranteeText": "GuaranteeCartItemTextWidget",
"mobileFee": "MobileServiceWidget",
"recycleFee": "RecycleFeeWidget",
"salesTax": "SalesTaxWidget",
"servicePackage": "ServicePackageTitle",
"subtotal": "SubtotalWidget",
"vapsItemDescriptions": "VapsItemDescriptions",
"warrantyText": "WarrantyCartItemTextWidget",
},
}
`;

View file

@ -73,47 +73,10 @@ describe('cart-dropdown component', () => {
expect(wrapper.vm.$data).toMatchSnapshot();
});
describe('displays', () => {
test('cart dropdown head', () => {
// Arrange
const reference = '#cart-dropdown-head';
const { wrapper } = getMountedComponent(cartDropdown);
// Act
const head = wrapper.find(reference);
// Assert
expect(head.exists()).toBeTruthy();
});
test('cart dropdown head is visible', () => {
// Arrange
const reference = '#cart-dropdown-head';
const { wrapper } = getMountedComponent(cartDropdown, {}, { showDropdownHeader: true });
// Act
const head = wrapper.find(reference);
// Assert
expect(head.exists()).toBeTruthy();
expect(head.classes()).toContain('cart-toggle');
});
test('cart dropdown head is not visible', () => {
// Arrange
const reference = '#cart-dropdown-head';
const { wrapper } = getMountedComponent(cartDropdown, {}, { showDropdownHeader: false });
// Act
const head = wrapper.find(reference);
// Assert
expect(head.exists()).toBeTruthy();
expect(head.classes()).not.toContain('cart-toggle');
});
test('cart table', () => {
// Arrange
const reference = '#cart-table';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
const reference = '.cart-table';
const { wrapper } = getMountedComponent({}, {}, {});
// Act
const cartTable = wrapper.find(reference);
@ -121,11 +84,9 @@ describe('cart-dropdown component', () => {
// Assert
expect(cartTable.exists()).toBeTruthy();
});
test('cart deductible when showDeductibleLineItem is true', () => {
test('cart deductible when showDeductibleCartItem is true', () => {
// Arrange
const reference = '#cart-deductible';
const isExpanded = true;
const initialData = { isExpanded };
const reference = '#deductible-label';
const storeData = {
order: {
insuranceCoverage: {
@ -134,7 +95,7 @@ describe('cart-dropdown component', () => {
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, initialData);
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act
const cartDeductible = wrapper.find(reference);
@ -144,9 +105,7 @@ describe('cart-dropdown component', () => {
});
test('cart base price when showDeductibleLineItem is false', () => {
// Arrange
const reference = '#cart-base-price';
const isExpanded = true;
const initialData = { isExpanded };
const reference = '#base-price-label';
const storeData = {
order: {
currentDeductible: 0,
@ -160,7 +119,7 @@ describe('cart-dropdown component', () => {
isClaimRegistrationRequired: true
}
};
const { wrapper } = getMountedComponent(storeData, {}, initialData);
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act
const cartBasePrice = wrapper.find(reference);
@ -168,25 +127,32 @@ describe('cart-dropdown component', () => {
// Assert
expect(cartBasePrice.exists()).toBeTruthy();
});
test('service package', () => {
// Arrange
const reference = '#cart-service-package';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
// Act
const servicePackage = wrapper.find(reference);
// Assert
expect(servicePackage.exists()).toBeTruthy();
});
test('non service package cart items', () => {
// Arrange
const reference = '#non-packaged-cart-items';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
const reference = '.non-packaged.cart-item';
const part1 = { partType: 'apple' };
const part2 = { partType: 'banana' };
const part3 = { partType: 'orange' };
const mobileFee = { partType: 'mobile' };
const storeData = {
order: {
currentDeductible: 0,
lineItems: {
glassParts: [part1],
supportingItems: [part2, part3],
otherParts: [],
feeItems: [mobileFee]
},
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.ITAC
}
},
issConfig: {
isClaimRegistrationRequired: true
}
};
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act
const nonPackagedCartItems = wrapper.find(reference);
@ -220,9 +186,7 @@ describe('cart-dropdown component', () => {
test('cart footer', () => {
// Arrange
const reference = '#cart-footer';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
const { wrapper } = getMountedComponent({}, {}, {});
// Act
const cartFooter = wrapper.find(reference);
@ -233,9 +197,7 @@ describe('cart-dropdown component', () => {
test('subtotal', () => {
// Arrange
const reference = '#cart-subtotal';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
const { wrapper } = getMountedComponent({}, {}, {});
// Act
const subtotal = wrapper.find(reference);
@ -246,9 +208,7 @@ describe('cart-dropdown component', () => {
test('tax', () => {
// Arrange
const reference = '#cart-sales-tax';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
const { wrapper } = getMountedComponent({}, {}, {});
// Act
const salesTax = wrapper.find(reference);
@ -280,9 +240,7 @@ describe('cart-dropdown component', () => {
test('bottom amount due', () => {
// Arrange
const reference = '#bottom-amount-due';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
const { wrapper } = getMountedComponent({}, {}, {});
// Act
const bottomAmountDue = wrapper.find(reference);
@ -295,8 +253,6 @@ describe('cart-dropdown component', () => {
test('cart deductible when showDeductibleLineItem is false', () => {
// Arrange
const reference = '#cart-deductible';
const isExpanded = true;
const initialData = { isExpanded };
const storeData = {
order: {
currentDeductible: 0,
@ -310,7 +266,7 @@ describe('cart-dropdown component', () => {
isClaimRegistrationRequired: true
}
};
const { wrapper } = getMountedComponent(storeData, {}, initialData);
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act
const cartDeductible = wrapper.find(reference);
@ -321,8 +277,6 @@ describe('cart-dropdown component', () => {
test('cart base price when showDeductibleLineItem is true', () => {
// Arrange
const reference = '#cart-base-price';
const isExpanded = true;
const initialData = { isExpanded };
const storeData = {
order: {
insuranceCoverage: {
@ -331,7 +285,7 @@ describe('cart-dropdown component', () => {
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, initialData);
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act
const cartBasePrice = wrapper.find(reference);
@ -1223,91 +1177,6 @@ describe('cart-dropdown component', () => {
expect(result.name).toBe(expectedName);
});
});
describe('packagePrice', () => {
test('returns 0 when servicePackageCartItems is empty', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => []);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.REAR_WIPER
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.packagePrice;
// Assert
expect(result).toBe(0);
});
test('returns expected when servicePackageCartItems not empty', () => {
// Arrange
const rearPrice = 101;
const frontPrice = 9;
getPriceOfLineItems.mockImplementation((lineItems) => {
if (lineItems.some((item) => item?.partType === partTypeStrings.FRONT_WIPER)) {
return frontPrice;
}
if (lineItems.some((item) => item?.partType === partTypeStrings.REAR_WIPER)) {
return rearPrice;
}
return 1;
});
getPackageContents.mockImplementationOnce(() => [partTypeStrings.FRONT_WIPER, partTypeStrings.REAR_WIPER]);
const storeData = {
order: {
lineItems: {
vaps: [
{ partType: partTypeStrings.FRONT_WIPER },
{ partType: partTypeStrings.REAR_WIPER }
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
const expectedPrice = 110;
// Act
const result = wrapper.vm.packagePrice;
// Assert
expect(result).toBe(expectedPrice);
});
test('returns expected when servicePackageCartItems has one item', () => {
// Arrange
const frontPrice = 9;
getPriceOfLineItems.mockImplementation((lineItems) => {
if (lineItems.some((item) => item?.partType === partTypeStrings.FRONT_WIPER)) {
return frontPrice;
}
return 1;
});
getPackageContents.mockImplementationOnce(() => [partTypeStrings.FRONT_WIPER, partTypeStrings.REAR_WIPER]);
const storeData = {
order: {
lineItems: {
vaps: [
{ partType: partTypeStrings.FRONT_WIPER }
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.packagePrice;
// Assert
expect(result).toBe(frontPrice);
});
});
});
describe('method', () => {
const dollarAmount = '$84.00';

View file

@ -1,156 +1,113 @@
<template>
<div>
<div
v-if="showDropdownHeader"
id="cart-dropdown-head"
class="row flex align-items-center pt-4 cart-header cart-toggle"
:class="[isExpanded ? 'expanded' : '']"
@click="toggleIsExpanded">
<a
aria-label="expand cart details"
href="javascript:void(0)"
class="col d-flex justify-content-between py-0">
<span class="color-black">{{ amountDueLabel }}</span>
<span class="color-green">{{ getDisplayed(amountDue) }}</span>
</a>
</div>
<div
v-else
id="cart-dropdown-head"
class="expanded cart-header">
</div>
<div
id="cart-table"
class="cart-table">
<div class="cart-table">
<div class="cart-item-list">
<div
id="cart-line-items"
class="color-darker-gray">
<div
id="cart-deductible-or-base-price"
class="cart-item cart-gray">
<div
v-if="showDeductibleCartItem"
id="cart-deductible"
class="d-flex justify-content-between align-items-center py-2">
id="cart-deductible-or-base-price"
class="cart-item">
<div class="price-row">
<template
v-if="showDeductibleCartItem">
<span id="deductible-label">{{ deductibleLabel }}</span>
<span id="deductible-value">{{ getDisplayed(deductible) }}</span>
</div>
<div
v-else
id="cart-base-price"
class="d-flex justify-content-between align-items-center">
</template>
<template
v-else>
<span id="base-price-label">{{ basePriceLabel }}</span>
<span id="base-price-value">{{ getDisplayed(servicePrice) }}</span>
</div>
</div>
<!-- Packaged Cart Items -->
<div
id="cart-service-package"
class="px-5">
<div
id="service-package-name"
class="py-2 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">
<div class="py-2 d-flex justify-content-between align-items-center">
<span>{{ item?.name ?? '' }}</span>
<textLink
v-if="!readOnly"
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="removeVap(item.partType)">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
</div>
</div>
</div>
</div>
<!-- NonPackaged Cart Items -->
<div id="non-packaged-cart-items">
<div
v-for="(item, i) in nonServicePackageCartItems"
:key="i"
class="px-5 py-2 non-packaged-cart-item">
<div class="d-flex justify-content-between align-items-center">
<span
v-if="item.cartItemType === cartItemType.RECYCLE_FEE"
id="recycle-fee-label">
<textLink
linkType="text"
:text="item?.name ?? ''"
href="javascript:void(0)"
@clickEvent="openModal(RECYCLING_MODAL_REF_NAME)" />
</span>
<span v-else>{{ item?.name ?? '' }}</span>
<span>{{ formatAmountInDollars(item?.subTotal ?? 0) }}</span>
</div>
<textLink
v-if="item.cartItemType === cartItemType.VAP
&& !readOnly"
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="removeVap(item.partType)">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
</div>
</div>
<div
id="cart-footer"
class="cart-footer pb-5">
<div
id="cart-subtotal-and-tax"
class="subtotal-and-tax">
<div
id="cart-subtotal"
class="py-2 col d-flex justify-content-between">
<span id="subtotal-label">{{ subtotalLabel }}</span>
<span id="subtotal-value">{{ getDisplayed(subTotal) }}</span>
</div>
<div
id="cart-sales-tax"
class="py-2 col d-flex justify-content-between">
<span id="sales-tax-label">{{ salesTaxLabel }}</span>
<span id="sales-tax-value">{{ formatAmountInDollars(salesTax) }}</span>
</div>
<div
v-if="showAsPaid"
id="cart-amount-paid"
class="py-2 col d-flex justify-content-between">
<span id="amount-paid-label">{{ amountPaidLabel }}</span>
<span if="amount-paid-value">{{ getDisplayed(amountPaid) }}</span>
</div>
</div>
<div
id="bottom-amount-due"
class="bottom-amount-due col d-flex justify-content-between">
<span id="bottom-amount-due-label">{{ amountDueLabel }}</span>
<span id="bottom-amount-due-value">{{ getDisplayed(amountDue) }}</span>
</div>
</template>
</div>
</div>
<contentGroupModal
:ref="RECYCLING_MODAL_REF_NAME"
:cmsWidgetName="recyclingModalCmsWidgetName">
<textBlock cmsWidgetName="RecycleTextBlock" />
</contentGroupModal>
<!-- NonPackaged Cart Items -->
<div
v-for="(item, i) in nonServicePackageCartItems"
:key="i"
class="cart-item non-packaged">
<div class="price-row">
<span
v-if="item.cartItemType === cartItemType.RECYCLE_FEE"
id="recycle-fee-label">
<textLink
linkType="text"
:text="item?.name ?? ''"
href="javascript:void(0)"
@clickEvent="openModal(RECYCLING_MODAL_REF_NAME)" />
</span>
<span v-else>{{ item?.name ?? '' }}</span>
<span>{{ formatAmountInDollars(item?.subTotal ?? 0) }}</span>
</div>
<textLink
v-if="item.cartItemType === cartItemType.VAP
&& !readOnly"
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="removeVap(item.partType)">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
</div>
<!-- Packaged Cart Items -->
<div
v-for="(item, i) in servicePackageCartItems"
:key="i"
class="cart-item packaged">
<div class="price-row">
<span>{{ item?.name ?? '' }}</span>
<span>{{ formatAmountInDollars(item?.subTotal ?? 0) }}</span>
</div>
<textLink
v-if="!readOnly"
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="removeVap(item.partType)">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
</div>
</div>
<div
id="cart-footer"
class="cart-footer pb-5">
<div
id="cart-subtotal-and-tax"
class="subtotal-and-tax">
<div
id="cart-subtotal"
class="py-2 col d-flex justify-content-between">
<span id="subtotal-label">{{ subtotalLabel }}</span>
<span id="subtotal-value">{{ getDisplayed(subTotal) }}</span>
</div>
<div
id="cart-sales-tax"
class="py-2 col d-flex justify-content-between">
<span id="sales-tax-label">{{ salesTaxLabel }}</span>
<span id="sales-tax-value">{{ formatAmountInDollars(salesTax) }}</span>
</div>
<div
v-if="showAsPaid"
id="cart-amount-paid"
class="py-2 col d-flex justify-content-between">
<span id="amount-paid-label">{{ amountPaidLabel }}</span>
<span if="amount-paid-value">{{ getDisplayed(amountPaid) }}</span>
</div>
</div>
<div
id="bottom-amount-due"
class="bottom-amount-due col d-flex justify-content-between">
<span id="bottom-amount-due-label">{{ amountDueLabel }}</span>
<span id="bottom-amount-due-value">{{ getDisplayed(amountDue) }}</span>
</div>
</div>
<contentGroupModal
:ref="RECYCLING_MODAL_REF_NAME"
:cmsWidgetName="recyclingModalCmsWidgetName">
<textBlock cmsWidgetName="RecycleTextBlock" />
</contentGroupModal>
</div>
</template>
@ -176,28 +133,26 @@ import {
isOrderUnverified
} from '@/helpers/cart-helper';
import { getPriceOfLineItems, getTaxOfLineItems } from '@/helpers/price-calculator';
import { processIfStatements } from '@/helpers/cms-content-helper';
const VERIFYING_COVERAGE = 'Verifying coverage';
const RECYCLING_MODAL_REF_NAME = 'RecycleModal';
export default {
name: 'cart-dropdown',
name: 'confirmation-cart',
components: {
contentGroupModal,
textBlock,
textLink
},
props: {
showAsPaid: Boolean,
readOnly: Boolean,
showAsPaid: Boolean,
recyclingModalCmsWidgetName: String,
showDropdownHeader: Boolean,
isInitiallyExpanded: Boolean,
submittedOrder: Object
},
data() {
return {
isExpanded: this.isInitiallyExpanded,
widget: {
amountDue: 'AmountDueTextWidget',
amountPaid: 'AmountPaidTextWidget',
@ -208,7 +163,9 @@ export default {
recycleFee: 'RecycleFeeWidget',
mobileFee: 'MobileServiceWidget',
servicePackage: 'ServicePackageTitle',
vapsItemDescriptions: 'VapsItemDescriptions'
vapsItemDescriptions: 'VapsItemDescriptions',
warrantyText: 'WarrantyCartItemTextWidget',
guaranteeText: 'GuaranteeCartItemTextWidget'
},
cartItemType,
RECYCLING_MODAL_REF_NAME
@ -273,7 +230,7 @@ export default {
return result;
},
vehicleDamage() {
return this.submittedOrder ? this.submittedOrder.damage : useMainStore().damage;
return this.cartOrder.damage;
},
servicePackageTier() {
const { glassToReplace, isRepair } = this.vehicleDamage;
@ -333,6 +290,9 @@ export default {
if (this.mobileFeeCartItem && !isMobileFeeHidden) {
items.push(this.mobileFeeCartItem);
}
if (this.isITAC || this.isNoComp) {
items.push(this.warrantyCartItem);
}
return items;
},
frontWipersCartItem() {
@ -350,12 +310,6 @@ export default {
...this.nonServicePackageCartItems
];
},
packagePrice() {
return this.servicePackageCartItems?.reduce(
(accumulator, cartItem) => accumulator + (cartItem?.subTotal ?? 0),
0
) ?? 0;
},
amountDueLabel() {
return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
@ -366,7 +320,12 @@ export default {
return this.getCmsContent(this.widget.deductible, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
basePriceLabel() {
return this.getCmsContent(this.widget.basePrice, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
const content = this.getCmsContent(this.widget.basePrice, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
return processIfStatements(
content,
'custom',
this.getCustomValueFromString
);
},
subtotalLabel() {
return this.getCmsContent(this.widget.subtotal, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
@ -409,15 +368,29 @@ export default {
partTypeStrings.MOBILE_FEE
)
: null;
},
warrantyCartItem() {
const label = this.cartOrder.damage.glassToReplace?.length > 0
? this.getCmsContent(this.widget.warrantyText, widgetFields.TEXT_BLOCK_WIDGET.TEXT)
: this.getCmsContent(this.widget.guaranteeText, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
return {
name: label,
cartItemType: cartItemType.WARRANTY,
partType: partTypeStrings.WARRANTY,
subTotal: 0,
salesTax: 0
}
},
hasMoldingPart() {
return this.cartOrder.lineItems.glassParts?.some((glassPart) => {
return glassPart.childParts?.some((childPart) => childPart.partType === partTypeStrings.MOLDING);
});
}
},
methods: {
openModal(modalName) {
this.$refs[modalName].openModal();
},
toggleIsExpanded() {
this.isExpanded = !this.isExpanded;
},
getDisplayed(amount) {
return this.isUnverified
? VERIFYING_COVERAGE
@ -461,6 +434,16 @@ export default {
const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsPartType);
return vapsTypeDescription?.Text ?? '';
},
getCustomValueFromString(str) {
switch (str) {
case 'isRepair':
return this.cartOrder.damage.isRepair;
case 'hasMoldingPart':
return this.hasMoldingPart;
default:
return null;
}
},
formatAmountInDollars
}
};
@ -471,45 +454,38 @@ export default {
$RECYCLING_MODAL_REF_NAME: 'RecycleModal';
.force-no-top-margin {
margin-top: 0 !important;
}
.color-black {
color: $black;
}
.color-green {
color: $green;
}
.color-darker-gray {
color: $darker-gray;
}
.cart-table {
max-height: 0;
transition: all 350ms ease-in;
overflow: hidden;
visibility: hidden;
margin-top: 1rem;
:deep(a) {
padding-bottom: 0 !important;
line-height: 1.625rem !important;
text-underline-offset: auto !important;
}
.non-packaged-cart-item:nth-child(odd){
background-color: $gray-100;
}
max-height: 50rem;
transition: all 150ms ease-in;
min-width: fit-content;
.cart-item {
padding: 0.25rem 1.5rem;
}
display: flex;
flex-direction: column;
padding: .5rem 0rem;
width: 100%;
min-width: fit-content;
&:nth-child(odd) {
background-color: #f4f4f4;
}
&:last-child {
border-bottom: 1px solid #d4d6d8;
}
.cart-gray {
background-color: $gray-100;
.price-row {
display: flex;
justify-content: space-between;
}
span, a {
white-space: nowrap;
padding: 0rem 1rem;
}
a:hover {
color: $heritage-blue-secondary;
}
}
.cart-footer {
@ -520,13 +496,16 @@ $RECYCLING_MODAL_REF_NAME: 'RecycleModal';
background-color: $green-150;
font-weight: $font-weight-bold;
color: $black;
padding: 0.25rem 1.5rem;
>div {
padding: .5rem 1rem;
}
}
.bottom-amount-due {
color: $white;
font-weight: 600;
background-color: $green;
padding: 0.5rem 1.5rem;
padding: .5rem 1rem;
}
}
@ -544,39 +523,6 @@ $RECYCLING_MODAL_REF_NAME: 'RecycleModal';
text-align: center;
}
.cart-header {
&.cart-toggle {
font-weight: $font-weight-bold;
&:after {
content: "";
transition: all 0.5s ease;
background-image: url($svg-payment-method-review-toggle);
background-repeat: no-repeat;
background-position: right center;
width: 1rem;
height: 0.5625rem;
display: inline-flex;
position: relative;
right: 0.75rem;
margin: 0.5rem 0 0.5rem 1rem;
cursor: pointer;
}
&.expanded:after {
transform: rotate(180deg);
}
a {
text-decoration: none;
}
}
&.expanded + .cart-table {
max-height: 50rem;
transition: all 150ms ease-in;
overflow: hidden;
visibility: visible;
}
}
:deep(#recycle-fee-label a){
line-height: initial;
}

View file

@ -2,11 +2,10 @@
import calendarOptions from '@/constants/calendar-options';
// Supporting Files
import { shallowMount, mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store';
import addToCalendar from '@/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue';
import { createTestingPinia } from '@pinia/testing';
const appointmentText = 'Appointment content';
function setupMocks({ customMountOptions }) {
@ -21,49 +20,6 @@ function setupMocks({ customMountOptions }) {
}
];
const wrapper = shallowMount(addToCalendar, mountOptions);
wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn();
return { wrapper };
}
const calendarModalQuestionStub = {
render: () => {}
};
const mockMixin = {
methods: {
getCmsContent: jest.fn().mockImplementation(() => appointmentText)
}
};
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}, mixin = mockMixin) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn(),
navigateToExternalUrl: jest.fn()
}
});
mountOptions.global.stubs = {
calendarModalQuestion: calendarModalQuestionStub
};
const testingPinia = createTestingPinia({
initialState: {
main: mainInitialState
}
});
useMainStore(testingPinia);
methodToRun();
mountOptions.global.mixins[0].methods.getSettingValue = jest.fn(() => 'false');
mountOptions.global.plugins = [testingPinia];
mountOptions.mixins = [mixin];
mountOptions.data = () => (
initialData
);
const wrapper = shallowMount(addToCalendar, mountOptions);
wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn();
return { wrapper };
}
@ -119,51 +75,8 @@ afterEach(() => {
jest.clearAllMocks();
});
const sessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00',
routeCode: '000'
},
lineItems: {
glassParts: [
{
partNumber: 'ABC123'
}
],
supportingItems: []
},
serviceLocation: {
address: '',
address2: null,
city: '',
state: 'AZ',
appointmentType: 'Inshop',
zipCode: '12345',
zipCodeCtu: '01234',
provider: {
providerNumber: '123',
address: {
streetAddress: 'test1',
city: 'test',
state: 'AZ',
zipCode: '12345',
zipCodeCtu: '01234'
}
}
},
damage: {
isRepair: false
},
referralNumber: '1234567',
payment: {
isInsurance: true
}
};
describe('Add-to-calendar methods...', () => {
test('Add-to-calendar should trigger openModal method', () => {
test('Add-to-calendar should trigger open options', async () => {
// Arrange
const { wrapper } = setupMocks({
customMountOptions: {
@ -183,10 +96,11 @@ describe('Add-to-calendar methods...', () => {
});
// Act
wrapper.vm.openCalendarModal();
wrapper.vm.openCalendarOptions();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled();
expect(wrapper.vm.$refs.calendarOptions.open).toEqual(true);
});
test('getCalendarData should return expected model value.', () => {

View file

@ -1,7 +1,7 @@
<template>
<div class="calendar-section">
<div class="add-to-calendar">
<button @click="openCalendarModal">
<button @click="toggleCalendarOptionsOpen">
<span class="add-to-calendar-span">
<img
src="@/assets/img/icons/add-to-calendar.svg"
@ -10,14 +10,12 @@
</span>
</button>
</div>
<calendarModalQuestion
ref="calendarModalQuestion"
v-model="selectedCalendarOption"
customComponentId="calendarModalQuestion"
cmsWidgetName="CalendarModalQuestion"
<calendarOptionSelection
ref="calendarOptions"
:open="calendarOptionsOpen"
:calendarOptionsData="calendarValues"
@calendarModalClosed="calendarModalClosed">
</calendarModalQuestion>
@calendarOptionSelected="calendarOptionSelected">
</calendarOptionSelection>
</div>
</template>
<script>
@ -32,12 +30,12 @@ import { AppointmentTypeStrings, RouteCodeFlags } from '@/constants/schedule-con
import { getCalendarFile, download } from '@/helpers/add-to-calendar-helper';
import serviceType from '@/constants/service-type';
import applicationConfig from '@/constants/application-config.js';
import calendarModalQuestion from '@/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue';
import calendarOptionSelection from '@/layouts/order-confirmation/add-to-calendar/calendar-option-selection/calendar-option-selection.vue';
export default {
name: 'add-to-calendar',
components: {
calendarModalQuestion
calendarOptionSelection
},
props: {
mobileWidgetName: String,
@ -59,7 +57,7 @@ export default {
},
data() {
return {
selectedCalendarOption: null
calendarOptionsOpen: false
};
},
computed: {
@ -84,7 +82,7 @@ export default {
return this.getCmsContent(this.mobileWidgetName, 'BodyText');
},
AddToCalendar_InShop_Subject() {
return this.getCmsContent('AddToCalendar_InShop', 'HeaderText');
return this.getCmsContent(this.inShopWidgetName, 'HeaderText');
},
AddToCalendar_InShop_Body() {
return this.getCmsContent(this.inShopWidgetName, 'BodyText');
@ -192,14 +190,6 @@ export default {
};
}
},
watch: {
selectedCalendarOption(newValue) {
if (newValue) {
this.setCalendarAppointment(newValue);
}
}
},
methods: {
setCalendarAppointment(calendarOption) {
if (
@ -211,12 +201,20 @@ export default {
window.open(calendarOption.url);
}
},
calendarModalClosed() {
// Clear the selectedOption if close the modal
this.selectedCalendarOption = null;
toggleCalendarOptionsOpen() {
if (this.calendarOptionsOpen) {
this.closeCalendarOptions();
} else {
this.openCalendarOptions();
}
},
openCalendarModal() {
this.$refs.calendarModalQuestion.openModal();
openCalendarOptions() {
this.calendarOptionsOpen = true;
window.addEventListener('click', this.clickListener);
},
calendarOptionSelected(calendarOptionIndex) {
this.setCalendarAppointment(this.calendarValues[calendarOptionIndex]);
this.closeCalendarOptions();
},
getCalendarData(type) {
let URL = '';
@ -253,29 +251,39 @@ export default {
};
const calBody = getCalendarFile(calFile);
download('SafeliteAppointment.ics', calBody);
},
closeCalendarOptions() {
this.calendarOptionsOpen = false;
window.removeEventListener('click', this.clickListener);
},
clickListener(event) {
const calendarSection = document.querySelector('.calendar-section');
if (calendarSection && !calendarSection.contains(event.target)) {
this.closeCalendarOptions();
}
}
},
unmounted() {
window.removeEventListener('click', this.clickListener);
}
};
</script>
<style scoped>
<style scoped lang="scss">
div.calendar-section {
position: relative;
top: 17px;
display: inline;
cursor: pointer;
}
.add-to-calendar-text {
margin: 0 0 0 0.4375rem;
color: #1574a1;
color: $heritage-blue-secondary;
vertical-align: middle;
}
.add-to-calendar button {
border: 0;
background-color: #fff;
}
.calendar-section {
display: table;
position: relative;
margin: 0 auto;
}
.calendar-section .add-to-calendar {
padding-top: 1rem;
padding-bottom: 1rem;
}
.calendar-section .add-to-calendar .add-to-calendar-span {
cursor: pointer;
}

View file

@ -1,81 +0,0 @@
<template>
<baseInputButton
v-bind="$props"
v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2">
<div
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
<span
class="m-0 position-relative"
:class="textPosition">
<img :src="buttonLabel" />
<span class="calendar-text">{{ value }}</span>
</span>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from '@/digital-components/base-input-button/base-input-button.vue';
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin.js';
export default {
name: 'calendar-modal-list-button',
components: {
baseInputButton
},
mixins: [inputButtonWrapperMixin]
};
</script>
<style lang="scss" scoped>
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
color: $gray-600;
}
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 0.063rem solid $gray-500;
width: 100%;
outline: none;
span.calendar-text {
margin-left: 0.313rem;
padding: 0.125rem 0.5rem;
}
}
.position-relative {
position: relative;
}
</style>

View file

@ -1,41 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import calendarModalQuestion from '@/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue';
describe('calendar-modal-question.vue', () => {
test('When selected calendar option is changed, a correctly selectedCalendarOption should be emitted', async () => {
// Arrange
const url = 'iCal url';
const name = 'iCal';
const icon = 'iCal.svg';
const expectedEmit = [
[
{
url,
name,
icon
}
]
];
const wrapper = shallowMount(calendarModalQuestion, {
propsData: {
calendarOptionsData: [
{
url,
name,
icon
}
]
}
});
wrapper.vm.$refs.calendarOptions.closeModal = jest.fn();
wrapper.vm.selectedCalendarOption = name;
// Act
wrapper.vm.setSelectedCalendarOption();
// Assert
expect(wrapper.emitted()['update:modelValue']).toEqual(expectedEmit);
});
});

View file

@ -1,180 +0,0 @@
<template>
<modal
:ref="modalName"
:fontSize="true"
:headerText="modalHeaderText"
:footerButtonText="footerCloseButtonText"
:onModalClosedCallback="onModalClosed"
class="calendar-modal"
@isModalOpened="setModalStatus"
@footerButtonEvent="setSelectedCalendarOption">
<template v-if="isModalOpened">
<buttonQuestion
ref="buttonQuestion"
v-model="selectedCalendarOption"
buttonTypeString="calendarModalListButton"
:buttonTypeObject="calendarModalListButton"
:answers="getCalendarOptionData"
groupName="chooseCalendarType"
class="mt-1"
textPosition="text-center"
isRequired
validationRules="calendar-option-required" />
</template>
</modal>
</template>
<script>
import modal from '@/digital-components/modal/modal.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import { defineRule, useField } from 'vee-validate';
import errorMessages from '@/constants/error-messages.js';
import { required } from '@/helpers/validation-rules';
import { deepClone } from '@/helpers/object-helper';
// eslint-disable-next-line import/no-extraneous-dependencies
import { v4 as uuidv4 } from 'uuid';
import calendarModalListButton from './calendar-modal-list-button/calendar-modal-list-button.vue';
// Validation for the modal button
defineRule('calendar-option-required', required(errorMessages.OPTION_REQUIRED));
export default {
name: 'calendar-modal-question',
components: {
modal,
buttonQuestion
},
props: {
calendarOptionsData: Object,
modelValue: {
type: Object,
default: () => ({
url: null,
name: null,
icon: null
})
}
},
emits: ['calendar-modal-closed', 'update:modelValue'],
setup(props) {
const uuid = uuidv4();
const componentId = !props.customComponentId
? `component-${uuid}`
: props.customComponentId;
const { modelValue } = deepClone(props);
const initialValue = modelValue;
const fieldOptions = {
value: modelValue,
initialValue
};
const { errorMessage, handleChange, meta, validate, errors } = useField(
componentId,
props.validationRules,
fieldOptions
);
return {
componentId,
errorMessage,
handleChange,
validate,
meta,
errors
};
},
data() {
return {
isModalOpened: false,
selectedValue: null,
calendarModalListButton
};
},
computed: {
modalName() {
return 'calendarOptions';
},
modalHeaderText() {
return 'Add to Calendar';
},
footerCloseButtonText() {
return 'Add to Calendar';
},
modal() {
return this.$refs[this.modalName];
},
selectedCalendarOption: {
get() {
return this.modelValue?.name;
},
set(newValue) {
// Button Question only supports Number, or String data types so we must convert to the full object before emitting
this.selectedValue = this.getSelectedCalendarObject(newValue);
}
},
getCalendarOptionData() {
return this.calendarOptionsData.map((item) => ({
value: item.name,
buttonLabel: item.icon
}));
},
watch: {
modelValue: {
handler(newValue) {
this.handleChange(newValue);
},
deep: true
}
}
},
methods: {
openModal() {
this.modal.openModal();
},
setModalStatus(isOpened) {
this.isModalOpened = isOpened;
},
closeModal() {
this.modal.closeModal();
},
onModalClosed() {
this.$emit('calendar-modal-closed');
},
async setSelectedCalendarOption() {
this.$emit('update:modelValue', this.selectedValue);
this.closeModal();
},
getSelectedCalendarObject(calendarOptionId) {
// eslint-disable-next-line no-shadow
const calendarOption = this.calendarOptionsData?.find((calendarOption) => calendarOption.name === calendarOptionId);
if (calendarOption) {
return {
url: calendarOption.url,
name: calendarOption.name,
icon: calendarOption.icon
};
}
return {
url: null,
name: null,
icon: null
};
}
}
};
</script>
<style lang="scss" scoped>
.calendar-modal.modal.modal-component {
> .modal-dialog > .modal-content {
margin-top: 1.5rem;
}
.modal-header {
padding-bottom: 0;
margin-bottom: 0 !important;
}
overflow: hidden;
}
</style>

View file

@ -0,0 +1,41 @@
<template>
<a
:aria-label="buttonLabel"
class="d-flex flex-row option-button">
<img :src="buttonLabel" />
<span class="calendar-text">{{ value }}</span>
</a>
</template>
<script>
export default {
name: 'calendar-option-button',
props: {
buttonLabel: String,
value: String
}
};
</script>
<style lang="scss" scoped>
.option-button {
text-decoration: none;
color: #4d4e53;
font-weight: 400;
padding: 0.5rem;
border-radius: 4px;
padding: .75rem 3.125rem .0625rem 1rem;
height: 2.75rem;
&:hover {
background-color: $background-color-selected;
}
img {
width: 1.25rem;
height: 1.25rem;
}
.calendar-text {
margin-left: 1rem;
}
}
</style>

View file

@ -0,0 +1,42 @@
import { shallowMount } from '@vue/test-utils';
import calendarOptionSelection from '@/layouts/order-confirmation/add-to-calendar/calendar-option-selection/calendar-option-selection.vue';
describe('calendar-option-selection.vue', () => {
test('When selected calendar option is changed, a correctly selectedCalendarOption should be emitted', async () => {
// Arrange
const url = 'iCal url';
const name = 'iCal';
const icon = 'iCal.svg';
const url2 = 'Google url';
const name2 = 'Google';
const icon2 = 'Google.svg';
const expectedEmit = [
[
1
]
];
const wrapper = shallowMount(calendarOptionSelection, {
propsData: {
calendarOptionsData: [
{
url,
name,
icon
},
{
url: url2,
name: name2,
icon: icon2
}
]
}
});
// Act
wrapper.vm.setSelectedCalendarOption(1);
// Assert
expect(wrapper.emitted()['calendar-option-selected']).toEqual(expectedEmit);
});
});

View file

@ -0,0 +1,53 @@
<template>
<div
v-if="open"
class="calendar-selection">
<template
v-for="(option, index) in getCalendarOptionData"
:key="option.value">
<calendarOptionButton
:buttonLabel="option.buttonLabel"
:value="option.value"
@click="setSelectedCalendarOption(index)" />
</template>
</div>
</template>
<script>
import calendarOptionButton from './calendar-option-button/calendar-option-button.vue';
export default {
name: 'calendar-option-selection',
components: {
calendarOptionButton
},
props: {
calendarOptionsData: Array,
open: Boolean
},
emits: ['calendar-option-selected'],
computed: {
getCalendarOptionData() {
return this.calendarOptionsData.map((item) => ({
value: item.name,
buttonLabel: item.icon
}));
}
},
methods: {
async setSelectedCalendarOption(index) {
this.$emit('calendar-option-selected', index);
}
}
};
</script>
<style lang="scss" scoped>
.calendar-selection {
position: absolute;
z-index: 1;
display: table;
border: solid 1px #d4d6d8;
border-radius: .25rem;
background-color: #ffffff;
box-shadow: 0 0 10px 0 rgba(88, 188, 233, 0.2);
}
</style>

View file

@ -206,6 +206,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => sessionStorage);
};
const { wrapper } = getMountedComponent(initialStore, {}, mockStoreActions);
console.log(wrapper.vm.$refs);
// Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
@ -363,7 +364,7 @@ describe('OrderConfirmation.vue', () => {
expect(actualEmail).toBe(emailAddress);
});
});
describe('confirmationEmailText', () => {
describe('orderConfirmationUpdateAppointmentText', () => {
const greaterId = '&gt;';
const lessId = '&lt;';
test.each([
@ -383,7 +384,7 @@ describe('OrderConfirmation.vue', () => {
const { wrapper } = getMountedComponent(initialStore, {}, mockStoreActions);
// Act
const text = wrapper.vm.confirmationEmailText;
const text = wrapper.vm.orderConfirmationUpdateAppointmentText;
// Assert
expect(text).toBe(expected);
@ -391,13 +392,13 @@ describe('OrderConfirmation.vue', () => {
});
describe('appointmentTimeText', () => {
test.each([
['Between 9 AM - 10 AM', '09:00', '10:00'],
['Between 1 PM - 5 PM', '13:00', '17:00'],
['Between 8:30 AM - 3 PM', '08:30', '15:00'],
['Between 1 AM - 2 AM', '01:00', '02:00'],
['Between 12 AM - 2 AM', '00:00', '02:00'],
['Between 12 PM - 2 AM', '12:00', '02:00'],
['Between 11 PM - 12 AM', '23:00', '00:00']
['Your appointment is at 9 AM - 10 AM', '09:00', '10:00'],
['Your appointment is at 1 PM - 5 PM', '13:00', '17:00'],
['Your appointment is at 8:30 AM - 3 PM', '08:30', '15:00'],
['Your appointment is at 1 AM - 2 AM', '01:00', '02:00'],
['Your appointment is at 12 AM - 2 AM', '00:00', '02:00'],
['Your appointment is at 12 PM - 2 AM', '12:00', '02:00'],
['Your appointment is at 11 PM - 12 AM', '23:00', '00:00']
])('should return Mobile time in expected format "%p" when start time "%p" and end time "%p"', (expected, startTime, endTime) => {
// Arrange
const order = deepClone(sessionStorage);
@ -438,12 +439,12 @@ describe('OrderConfirmation.vue', () => {
expect(testValue).toEqual('Drop off before 9:30 AM');
});
test.each([
['at 9:00 AM', '09:00', '10:00'],
['at 1:00 PM', '13:00', '17:00'],
['at 8:30 AM', '08:30', '15:00'],
['at 1:00 AM', '01:00', '02:00'],
['at 12:00 AM', '00:00', '02:00'],
['at 12:00 PM', '12:00', '02:00']
['Your appointment is at 9:00 AM', '09:00', '10:00'],
['Your appointment is at 1:00 PM', '13:00', '17:00'],
['Your appointment is at 8:30 AM', '08:30', '15:00'],
['Your appointment is at 1:00 AM', '01:00', '02:00'],
['Your appointment is at 12:00 AM', '00:00', '02:00'],
['Your appointment is at 12:00 PM', '12:00', '02:00']
])('should return In Shop time in expected format "%p" when start time "%p"', (expected, startTime, endTime) => {
// Arrange
const order = deepClone(sessionStorage);
@ -479,8 +480,8 @@ describe('OrderConfirmation.vue', () => {
}
}
};
const expectedServiceLocation = '123 Service Rd., Box 4,<br/> Columbus, OH 12345';
const expectedShopLocation = '304 Provider Ln.,<br/> Pittsburg, PA 16001';
const expectedServiceLocation = '123 Service Rd., Box 4, Columbus, OH 12345';
const expectedShopLocation = '304 Provider Ln., Pittsburg, PA 16001';
test.each([
AppointmentTypeStrings.MOBILE,
AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP
@ -554,7 +555,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '123 Service Rd., Box 4,<br/> Columbus, OH 12345';
const expected = '123 Service Rd., Box 4, Columbus, OH 12345';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -568,7 +569,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = ', Box 4,<br/> Columbus, OH 12345';
const expected = ', Box 4, Columbus, OH 12345';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -582,7 +583,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '123 Service Rd., <br/> Columbus, OH 12345';
const expected = '123 Service Rd., Columbus, OH 12345';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -596,7 +597,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '123 Service Rd., Box 4,<br/> , OH 12345';
const expected = '123 Service Rd., Box 4, , OH 12345';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -610,7 +611,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '123 Service Rd., Box 4,<br/> Columbus, 12345';
const expected = '123 Service Rd., Box 4, Columbus, 12345';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -624,7 +625,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '123 Service Rd., Box 4,<br/> Columbus, OH ';
const expected = '123 Service Rd., Box 4, Columbus, OH ';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -642,7 +643,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = ', <br/> , ';
const expected = ', , ';
// Act
const text = wrapper.vm.serviceLocationFullAddress;
@ -675,7 +676,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '304 Provider Ln.,<br/> Pittsburg, PA 16001';
const expected = '304 Provider Ln., Pittsburg, PA 16001';
// Act
const text = wrapper.vm.providerFullAddress;
@ -703,7 +704,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '304 Provider Ln.,<br/> , PA 16001';
const expected = '304 Provider Ln., , PA 16001';
// Act
const text = wrapper.vm.providerFullAddress;
@ -717,7 +718,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '304 Provider Ln.,<br/> Pittsburg, 16001';
const expected = '304 Provider Ln., Pittsburg, 16001';
// Act
const text = wrapper.vm.providerFullAddress;
@ -731,7 +732,7 @@ describe('OrderConfirmation.vue', () => {
useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
};
const { wrapper } = getMountedComponent({}, {}, mockStoreActions);
const expected = '304 Provider Ln.,<br/> Pittsburg, PA ';
const expected = '304 Provider Ln., Pittsburg, PA ';
// Act
const text = wrapper.vm.providerFullAddress;
@ -956,9 +957,9 @@ describe('OrderConfirmation.vue', () => {
describe('Methods', () => {
describe('formatDate', () => {
test.each([
['Wednesday, April 22', '2020-04-22'],
['Thursday, February 1', '2018-02-01'],
['Wednesday, December 30', '2026-12-30']
['Wednesday, April 22, 2020', '2020-04-22'],
['Thursday, February 1, 2018', '2018-02-01'],
['Wednesday, December 30, 2026', '2026-12-30']
])('returns %p given %p', (expected, date) => {
// Arrange
const order = deepClone(sessionStorage);

View file

@ -8,21 +8,34 @@
<div class="justify-content-center">
<siteHeader :cmsWidgetName="widgets.siteHeader" />
</div>
<div class="iss-heritage-container-width">
<div class="order-confirmation-container iss-heritage-content-container-width">
<div class="text-center text-color--black pt-5 pb-5 fs-5">
<div class="iss-heritage-container-width order-confirmation-container row">
<div class="appointment-info-container col-lg-7">
<div class="header">
<img :src="orderConfirmationImage" />
<span
class="ms-2"
class="header-text"
v-html="orderConfirmationHeaderText"></span>
</div>
<div class="appointment-details">
<div class="appointment-date-time text-center mt-4">
<p>{{ formatDate(schedule.date) }}</p>
<p class="mt-2">
{{ appointmentTimeText }}
</p>
</div>
<div class="subheader">
<span
class="subheader-text"
v-html="orderConfirmationSubheaderText"></span>
<span
class="update-appointment-text"
v-html="orderConfirmationUpdateAppointmentText"></span>
</div>
<div class="service-description confirmation-section">
<span>{{ serviceDescriptionText }}</span>
</div>
<div class="work-order-details confirmation-section">
<span class="work-order-title">{{ workOrderTitle }}</span>
<span class="work-order-description">{{ workOrderNumber }}</span>
</div>
<div class="appointment-details confirmation-section">
<span class="appointment-title">{{ appointmentWordingText }}</span>
<span>{{ formatDate(schedule.date) }}</span>
<span>{{ appointmentTimeText }}</span>
<span class="appointment-location">{{ appointmentLocation }}</span>
<addToCalendar
mobileWidgetName="AddToCalendar_Mobile"
inShopWidgetName="AddToCalendar_InShop"
@ -40,29 +53,32 @@
:hasRecalibrationPart="hasRecalibrationPart"
:uniqueId="referralNumber"
:isRepair="isRepair" />
<div
class="appointment-text text-center lh-base"
v-html="appointmentWordingText"></div>
<div
class="appointment-text text-center lh-base mt-2"
v-html="appointmentWordingText2"></div>
</div>
<hr class="mb-0" />
<div>
<cartDropdown
v-if="showCart"
:showAsPaid="payment.isPayInAdvance"
:readOnly="true"
:isInitiallyExpanded="false"
:showDropdownHeader="true"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle"
:submittedOrder="submittedOrder" />
<div
v-if="displayWipers"
class="wiper-details confirmation-section">
<span class="wipers-title">{{ wipersTitle }}</span>
<span
v-for="(description, index) in wipersBody"
:key="index"
class="wipers-body">{{ description }}</span>
</div>
<div
v-if="displayRainRepel"
class="rain-repel-details confirmation-section">
<span class="rain-repel-title">{{ rainRepelTitle }}</span>
<span class="rain-repel-body">{{ rainRepelBody }}</span>
</div>
<hr class="mt-0 mb-5" />
<div
class="email-confirmation-text"
v-html="confirmationEmailText" />
v-if="displaySMSPhone"
class="sms-phone-details confirmation-section">
<span class="sms-phone-title">{{ smsTitle }}</span>
<span class="sms-phone-body">{{ contactInfo.servicePhone }}</span>
</div>
<div class="contact-details confirmation-section">
<span class="contact-title">{{ contactDetailsTitle }}</span>
<span class="contact-body">{{ contactDetailsBody }}</span>
</div>
<siteFooter
v-if="carrierUrl"
ref="siteFooter"
@ -72,6 +88,18 @@
@ForwardClicked="forwardButtonAction"
@backClicked="navigateBack" />
</div>
<div class="cart-container col-lg-4 offset-lg-1 col-md-5">
<div class="cart-header">
{{ cartHeaderText }}
</div>
<cartDropdown
v-if="showCart"
:showAsPaid="payment.isPayInAdvance"
:readOnly="true"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle"
:submittedOrder="submittedOrder" />
</div>
</div>
</div>
</Form>
@ -103,6 +131,9 @@ import { toTitleCase } from '@/helpers/text-helper.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import applicationConfig from '@/constants/application-config';
import widgetFields from '@/constants/cms-widget-fields.js';
import { getGlassList } from '@/helpers/damage-helper';
import partTypeStrings from '@/constants/part-type-strings';
import coverageType from '@/constants/coverage-type';
export default {
name: 'order-confirmation',
@ -144,18 +175,21 @@ export default {
serviceLocation,
schedule,
customer,
contactInfo,
payment,
customerPortalLoginToken,
damage,
referralNumber
referralNumber,
hasRecalibrationPart
} = this.submittedOrder;
const { issConfig, hasRecalibrationPart } = this.mainStore;
const { issConfig } = this.mainStore;
return {
vehicle,
customerEmail: customer?.emailAddress,
payment,
schedule,
serviceLocation,
contactInfo,
appointmentType: serviceLocation.appointmentType,
providerAddress: serviceLocation?.provider?.address,
customerPortalLoginToken,
@ -164,12 +198,23 @@ export default {
isRepair: damage.isRepair,
hasRecalibrationPart,
referralNumber: referralNumber?.toString(),
isNoComp: this.submittedOrder.insuranceCoverage.coverageType === coverageType.NO_COMP,
widgets: {
siteHeader: 'SiteHeaderWidget',
emailConfirmation: 'EmailConfirmationWordingWidget',
emailConfirmationNoComp: 'EmailConfirmationNoCompWordingWidget',
orderConfirmation: 'OrderConfirmationContent',
serviceDescription: 'ServiceDescriptionTextWidget',
workOrderNumberTitle: 'WorkOrderNumberTextWidget',
mobile: 'MobileWordingWidget',
dropOffAndInShop: 'DropOffAndInShopWordingWidget',
wipersText: 'WipersTextWidget',
rainRepel: 'RainRepelWidget',
smsUpdates: 'SmsUpdatesWidget',
contactDetails: 'ContactDetailsWidget',
vapsItemDescriptions: 'VapsItemDescriptions',
payAtAppointment: 'PayAtAppointmentTextWidget',
orderDetails: 'OrderDetailsTextWidget',
siteFooter: 'SiteFooterWidget'
}
};
@ -182,18 +227,29 @@ export default {
vehicleYear: this.vehicle?.year,
vehicleMake: this.vehicle?.make,
vehicleModel: this.vehicle?.model,
serviceType: this.serviceTypeText,
address: this.appointmentLocation,
inShopDuration: this.inShopAppointmentDuration,
email: this.customerEmail,
phone: this.contactInfo.servicePhone,
workOrderNumber: this.workOrderNumber,
CUSTOMER_PORTAL_URL: applicationConfig.CUSTOMER_PORTAL_URL,
CUSTOMER_PORTAL_LOGIN_TOKEN: this.customerPortalLoginToken
};
},
confirmationEmailText() {
const content = this.getCmsContentWithCustomValues(
this.widgets.emailConfirmation,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
orderConfirmationUpdateAppointmentText() {
let content = '';
if(this.isNoComp) {
content = this.getCmsContentWithCustomValues(
this.widgets.emailConfirmationNoComp,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
} else {
content = this.getCmsContentWithCustomValues(
this.widgets.emailConfirmation,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
}
return content
?.replaceAll('&lt;', '<')
?.replaceAll('&gt;', '>');
@ -210,10 +266,51 @@ export default {
widgetFields.CONTENT_GROUP_WIDGET.IMAGE
);
},
orderConfirmationSubheaderText() {
return this.getCmsContent(
this.widgets.orderConfirmation,
widgetFields.CONTENT_GROUP_WIDGET.SUBHEADER_TEXT
);
},
serviceDescriptionText() {
const content = this.getCmsContentWithCustomValues(
this.widgets.serviceDescription,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
return content;
},
serviceTypeText() {
const glassPieces = this.submittedOrder.lineItems.glassParts?.map((part) => part.partType.toLowerCase()) ?? [];
let glassList = getGlassList(glassPieces);
let workType = '';
if (this.isRepair) {
workType = 'repair';
} else {
workType = 'replacement';
if (this.hasRecalibrationPart) {
if (glassList === 'windshield') {
workType = 'replacement and recalibration';
}
else {
glassList = glassList.replace('windshield', 'windshield replacement, recalibration');
}
}
}
return `${glassList} ${workType}`;
},
workOrderTitle() {
return this.getCmsContent(
this.widgets.workOrderNumberTitle,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
workOrderNumber() {
return this.submittedOrder.workOrderNumber;
},
mobileWordingText() {
return this.getCmsContentWithCustomValues(
this.widgets.mobile,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
},
mobileWordingText2() {
@ -225,7 +322,7 @@ export default {
nonMobileWordingText() {
return this.getCmsContentWithCustomValues(
this.widgets.dropOffAndInShop,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
},
nonMobileWordingText2() {
@ -241,12 +338,12 @@ export default {
}
if (this.isInShopAppointment) {
const formattedStartTime = get12HourTimeFormat(startTime);
return `at ${formattedStartTime}`;
return `Your appointment is at ${formattedStartTime}`;
}
if (this.isMobileAppointment) {
const mobileStartTime = get12HourTimeMobileFormat(startTime);
const mobileEndTime = get12HourTimeMobileFormat(endTime);
return `Between ${mobileStartTime} - ${mobileEndTime}`;
return `Your appointment is at ${mobileStartTime} - ${mobileEndTime}`;
}
return null;
},
@ -261,12 +358,12 @@ export default {
},
serviceLocationFullAddress() {
const { address, address2, city, state, zipCode } = this.serviceLocation;
return `${address ?? ''}, ${address2 ? `${address2},` : ''}<br/> ${city ?? ''}, ${state ?? ''} ${zipCode ?? ''}`;
return `${address ?? ''}, ${address2 ? `${address2},` : ''} ${city ?? ''}, ${state ?? ''} ${zipCode ?? ''}`;
},
providerFullAddress() {
const { streetAddress, city, state, zipCode } = this.providerAddress;
return streetAddress
? `${toTitleCase(streetAddress)},<br/> ${toTitleCase(city)}, ${state ?? ''} ${zipCode ?? ''}`
? `${toTitleCase(streetAddress)}, ${toTitleCase(city)}, ${state ?? ''} ${zipCode ?? ''}`
: '';
},
appointmentWordingText() {
@ -318,6 +415,84 @@ export default {
// settleTenderAmount always shows 0 via localhost or dev.
// Temporarily set return true to see cart in localhost or dev environment
return false;
},
displayWipers() {
const hasWiperPart = this.submittedOrder.lineItems.vaps.some((part) => part.partType.toLowerCase().includes('wiper'));
return hasWiperPart;
},
wipersTitle() {
return this.getCmsContent(
this.widgets.wipersText,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
wipersBody() {
const wiperTypesOnOrder = [];
const hasFrontWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER);
const hasRearWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER);
if (hasFrontWiper) {
wiperTypesOnOrder.push(partTypeStrings.FRONT_WIPER);
}
if (hasRearWiper) {
wiperTypesOnOrder.push(partTypeStrings.REAR_WIPER);
}
const wiperDescriptions = wiperTypesOnOrder.map((type) => {
return this.getCmsContentForVapsType(type);
});
return wiperDescriptions;
},
displayRainRepel() {
return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.RAIN_DEFENSE);
},
rainRepelTitle() {
return this.getCmsContent(
this.widgets.rainRepel,
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
},
rainRepelBody() {
return this.getCmsContent(
this.widgets.rainRepel,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
},
displaySMSPhone() {
return this.contactInfo.requestTextUpdates && this.contactInfo.servicePhone;
},
smsTitle() {
return this.getCmsContent(
this.widgets.smsUpdates,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
contactDetailsTitle() {
return this.getCmsContent(
this.widgets.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
},
contactDetailsBody() {
return this.getCmsContentWithCustomValues(
this.widgets.contactDetails,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
},
cartHeaderText() {
if (this.payment.isPayInAdvance) {
return this.getCmsContent(
this.widgets.orderDetails,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
} else {
return this.getCmsContent(
this.widgets.payAtAppointment,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
}
}
},
mounted() {
@ -411,68 +586,138 @@ export default {
return dateObject.toLocaleDateString('en-us', {
weekday: 'long',
month: 'long',
day: 'numeric'
day: 'numeric',
year: 'numeric'
});
},
getCmsContentForVapsType(vapsPartType) {
const vapsItemDescriptions = this.getCmsContent(
this.widgets.vapsItemDescriptions,
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
);
if (!vapsItemDescriptions) {
return '';
}
const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsPartType);
return vapsTypeDescription?.Text ?? '';
}
}
};
</script>
<style lang="scss" scoped>
.iss-heritage-container-width {
.order-confirmation-container {
position: relative;
min-height: 1px;
padding-left: .9375rem;
padding-right: .9375rem;
.order-confirmation-container {
.appointment-info-container
{
padding: 0 .9375rem;
margin-bottom: 1.875rem;
}
}
.page-container-grouped-styles {
overflow: auto;
}
.text-color--black {
color: $black;
}
.appointment-details {
margin-bottom: 1.5rem;
padding: 1rem 1.5rem 1.5rem;
box-shadow: 0 0.188rem 0.625rem rgb(0 0 0 / 0.2);
border-radius: 0.313rem;
}
.appointment-date-time P {
color: $black;
font-size: $h5-font-size;
line-height: map-get($spacers, 6);
margin-bottom: 0;
+ p {
font-size: map-get($spacers, 4);
line-height: 1.625rem;
font-weight: $font-weight-bold;
.cart-container {
padding: 0 .9375rem;
}
}
.appointment-text {
:deep(p) {
margin: 0;
}
:deep(strong) {
font-weight: $font-weight-bold;
.header {
flex-direction: row;
margin-top: 1.25rem;
margin-bottom: .625rem;
font-size: 1.25rem;
color: $black;
}
}
.email-confirmation-text {
:deep(a) {
font-weight: $font-weight-bold;
text-decoration: underline;
img {
width: 2.25rem;
height: 2.25rem;
margin-right: 1rem;
}
}
:deep(strong) {
font-weight: $font-weight-bold;
.subheader {
display: flex;
flex-direction: column;
margin-top: 1.25rem;
.subheader-text {
margin-bottom: .625rem;
font-size: 1rem;
font-weight: 500;
color: $black;
}
.update-appointment-text {
:deep(.text-primary) {
color: $heritage-blue-primary;
}
:deep(a) {
font-weight: $font-weight-bold;
color: $heritage-blue-primary;
text-decoration: none;
&:hover {
color: $heritage-blue-secondary;
text-decoration: underline;
}
}
}
}
.confirmation-section {
display: flex;
flex-direction: column;
padding: 1.875rem 0;
border-bottom: 1px solid lightgray;
>:first-child {
font-weight: 600;
color: $black;
}
}
.service-description {
>span:first-child {
font-weight: $font-weight-bold;
}
}
.work-order-title {
margin-bottom: .625rem;
}
.appointment-details {
padding-bottom: 2.0625rem;
.appointment-title {
margin-bottom: .625rem;
}
.appointment-location {
margin-top: .625rem;
}
}
.rain-repel-details {
.rain-repel-title {
color: #4d4e53;
}
}
.sms-phone-details {
.sms-phone-title {
color: #4d4e53;
}
}
.contact-details {
.contact-title {
margin-bottom: .625rem;
}
}
.cart-header {
margin-top: 1.25rem;
margin-bottom: .625rem;
color: $black;
font-weight: $font-weight-bold;
}
}
</style>

View file

@ -24,8 +24,6 @@
<cartDropdown
:showAsPaid="false"
:readOnly="false"
:showDropdownHeader="true"
:isInitiallyExpanded="false"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle" />
<hr class="mt-0 mb-5" />

View file

@ -42,8 +42,7 @@
<cartDropdown
ref="cart"
:readOnly="true"
:showDropdownHeader="true"
:isInitiallyExpanded="true"
:showAsPaid="false"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle" />
<hr class="my-0">

View file

@ -237,7 +237,7 @@ export default {
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getProviders(serviceZipCode);
const providersPromise = useMainStore().getSafeliteProviders(serviceZipCode);
const premiumFeePromise = useMainStore().getMobilePremiumFee();
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {

View file

@ -147,7 +147,7 @@ function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
}
}
})];
useMainStore().getProviders = jest.fn().mockImplementation(() => mockProviders());
useMainStore().getSafeliteProviders = jest.fn().mockImplementation(() => mockProviders());
const wrapper = mount(serviceLocation, mountOptions);
wrapper.vm.$router.navigateWithSpinner = jest.fn();

View file

@ -243,6 +243,10 @@ export default {
return this.isGlassServiceableInshop;
},
isServiceableMobile() {
if (this.isBigTruck) {
return false;
}
if (this.isRecalibrationServiceableMobile !== null) {
return (
this.isGlassServiceableMobile
@ -553,7 +557,7 @@ export default {
}
});
const providersPromise = useMainStore().getProviders(this.mobileZipCode);
const providersPromise = useMainStore().getSafeliteProviders(this.mobileZipCode);
await providersPromise.then((result) => {
const providers = result.data;
if (providers) {

View file

@ -314,7 +314,7 @@ export default {
}
},
async getNearbyShops(radiusInMiles) {
const result = await useMainStore().getProviders(this.internalZipcode, radiusInMiles);
const result = await useMainStore().getSafeliteProviders(this.internalZipcode, radiusInMiles);
return result.data.shopProviders;
},
getFullProviderAddress(provider) {

View file

@ -299,6 +299,11 @@ export const useMainStore = defineStore({
const nonWindshieldItems = damage.glassToReplace?.filter((glass) => glass.glassLocation !== damageLocationsSelected.WINDSHIELD);
return !!nonWindshieldItems?.length;
},
hasMoldingPart: (s) => {
return s.order.lineItems.glassParts?.some((glassPart) => {
return glassPart.childParts?.some((childPart) => childPart.partType === partTypeStrings.MOLDING);
});
},
isMobileAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|| state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP,
isDropOffAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.DROP_OFF,
@ -1029,46 +1034,35 @@ export const useMainStore = defineStore({
});
},
getProviders(serviceZipCode, shopRadiusInMiles = 100) {
const { carId, isBigTruck } = this.order.vehicle;
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
const { parentAccountNumber } = this.order;
const partsWithRecal = getTopLevelGlassPartsWithRecal(this.order.lineItems.glassParts);
const windshieldPartWithRecal = partsWithRecal?.length > 0 ? partsWithRecal[0] : null;
const safeliteOnly = true;
let url = `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}/${parentAccountNumber}/${safeliteOnly}/${carId}`;
if (windshieldPartWithRecal) {
url += `/${windshieldPartWithRecal.partNumber}`;
}
url += `/${isBigTruck}`;
return globalMethods.callHttpClient({
method: endpoints.GetProviders.method,
endpoint: url
});
getSafeliteProviders(zipCode, radius = 100) {
return this.getProviders(zipCode, radius, true)
},
getTpaProviders(zipCode, radius) {
return this.getProviders(zipCode, radius, false)
},
getProviders(zipcode, radius, safeliteOnly) {
const { carId, isBigTruck } = this.order.vehicle;
const damageType = this.damage.isRepair ? 'Repair' : 'Replace';
const { parentAccountNumber } = this.order;
const partsWithRecal = getTopLevelGlassPartsWithRecal(this.order.lineItems.glassParts);
const windshieldPartWithRecal = partsWithRecal?.length > 0 ? partsWithRecal[0] : null;
const safeliteOnly = false;
let url = `${endpoints.GetProviders.url}/${zipCode}/${damageType}/${radius}/${parentAccountNumber}/${safeliteOnly}/${carId}`;
if (windshieldPartWithRecal) {
url += `/${windshieldPartWithRecal.partNumber}`;
}
url += `/${isBigTruck}`;
return new Promise((resolve, reject) => {
globalMethods.callHttpClient({
method: endpoints.GetProviders.method,
endpoint: url
}).then((response) => resolve(response), (error) => reject(error));
return globalMethods.callHttpClient({
method: endpoints.GetProviders.method,
endpoint: endpoints.GetProviders.url(
zipcode,
damageType,
radius,
parentAccountNumber,
safeliteOnly,
carId,
windshieldPartWithRecal?.partNumber,
isBigTruck
)
});
},
getCarrierAccountInfo() {
return new Promise((resolve, reject) => {
globalMethods.callHttpClient({
@ -2897,12 +2891,13 @@ export const useMainStore = defineStore({
await this.getCarrierAccountInfo();
const submittedOrder = this.order;
const { experiments } = this.applicationUser;
const { issConfig } = this;
const { issConfig, hasRecalibrationPart } = this;
submittedOrder.isUnverified = this.isUnverified;
submittedOrder.isVerified = this.isVerified;
submittedOrder.submitType = submitType;
submittedOrder.payment.isPayInAdvance = this.isPayInAdvance;
submittedOrder.hasRecalibrationPart = hasRecalibrationPart;
// set to sessionStorage
window.sessionStorage.setItem(webStorageConstants.SUBMITTED_ORDER, JSON.stringify(submittedOrder));

View file

@ -1983,16 +1983,16 @@ describe('Store', () => {
const radius = getRandomInt(5, 100);
const isBigTruck = false;
store.order.vehicle.isBigTruck = isBigTruck;
const expectedEndpoint = [
endpoints.GetProviders.url,
const expectedEndpoint = endpoints.GetProviders.url(
zipCode,
damageType,
radius,
parentAccountNumber,
'false',
carId,
null,
isBigTruck
].join('/');
);
// Act
await store.getTpaProviders(zipCode, radius);