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

This commit is contained in:
JennyNou 2026-02-23 12:40:46 -05:00
commit 531c5c7cd3
82 changed files with 2053 additions and 3372 deletions

62
package-lock.json generated
View file

@ -6057,7 +6057,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -6088,14 +6090,16 @@
} }
}, },
"node_modules/ajv-formats/node_modules/ajv": { "node_modules/ajv-formats/node_modules/ajv": {
"version": "8.11.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2"
"uri-js": "^4.2.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -8088,14 +8092,16 @@
} }
}, },
"node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": {
"version": "8.11.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2"
"uri-js": "^4.2.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -15565,14 +15571,16 @@
} }
}, },
"node_modules/mini-css-extract-plugin/node_modules/ajv": { "node_modules/mini-css-extract-plugin/node_modules/ajv": {
"version": "8.11.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2"
"uri-js": "^4.2.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -19434,7 +19442,9 @@
} }
}, },
"node_modules/terser-webpack-plugin/node_modules/ajv": { "node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "8.17.1", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -20952,14 +20962,16 @@
} }
}, },
"node_modules/webpack-dev-middleware/node_modules/ajv": { "node_modules/webpack-dev-middleware/node_modules/ajv": {
"version": "8.11.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2"
"uri-js": "^4.2.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -21055,14 +21067,16 @@
} }
}, },
"node_modules/webpack-dev-server/node_modules/ajv": { "node_modules/webpack-dev-server/node_modules/ajv": {
"version": "8.11.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0", "json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2"
"uri-js": "^4.2.2"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@ -21129,7 +21143,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/webpack/node_modules/ajv": { "node_modules/webpack/node_modules/ajv": {
"version": "8.17.1", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -60,10 +60,11 @@ export const pageProgressMapper = {
percent: 85 percent: 85
}, },
'payment-method': { 'payment-method': {
percent: 90 percent: 95
}, },
'payment-page': { 'payment-page': {
percent: 95 // No progress bar on this page
percent: 0
}, },
'tpa-search': { 'tpa-search': {
percent: 80 percent: 80

View file

@ -10,7 +10,7 @@
When would you like service? When would you like service?
</div> </div>
<div class="appointment-estimate-header"> <div class="appointment-estimate-header">
Your service will take approximately <span>{{ appointmentEstimate }}</span> Your service will take approximately <span>{{ appointmentEstimate }}</span>.
</div> </div>
<div <div
class="appointment-date-picker-container" class="appointment-date-picker-container"
@ -558,8 +558,12 @@ export default {
this.selectedDate = morningDateString; this.selectedDate = morningDateString;
this.selectedTimeOfDayGrouping = 'morning'; this.selectedTimeOfDayGrouping = 'morning';
this.selectableTimeSlotsData = selectableDateObj.morningTimeSlots; this.selectableTimeSlotsData = selectableDateObj.morningTimeSlots;
this.selectedTime = null; if (selectableDateObj.morningTimeSlots.length === 1) {
this.selectedTimeSlot = null; this.selectTimeSlotForDay(selectableDateObj.morningTimeSlots[0]);
} else {
this.selectedTime = null;
this.selectedTimeSlot = null;
}
} }
}, },
showAfternoonAvailabilityForIndex(index) { showAfternoonAvailabilityForIndex(index) {
@ -582,8 +586,12 @@ export default {
this.selectedDate = afternoonDateString; this.selectedDate = afternoonDateString;
this.selectedTimeOfDayGrouping = 'afternoon'; this.selectedTimeOfDayGrouping = 'afternoon';
this.selectableTimeSlotsData = selectableDateObj.afternoonTimeSlots; this.selectableTimeSlotsData = selectableDateObj.afternoonTimeSlots;
this.selectedTimeSlot = null; if (selectableDateObj.afternoonTimeSlots.length === 1) {
this.selectedTime = null; this.selectTimeSlotForDay(selectableDateObj.afternoonTimeSlots[0]);
} else {
this.selectedTime = null;
this.selectedTimeSlot = null;
}
} }
}, },
timeSlotFocused(timeSlot) { timeSlotFocused(timeSlot) {

View file

@ -47,7 +47,7 @@
:data-bs-toggle="includeSelectIcon ? 'modal' : ''" :data-bs-toggle="includeSelectIcon ? 'modal' : ''"
:data-bs-target="'#' + cmsWidgetName" :data-bs-target="'#' + cmsWidgetName"
@change="handleChange" @change="handleChange"
@blur="handleChange" @blur="blurPlus"
@focus="$emit('focus', $event.target.value)" @focus="$emit('focus', $event.target.value)"
@paste="trimOnPaste" @paste="trimOnPaste"
@drop="trimOnPaste" /> @drop="trimOnPaste" />
@ -92,6 +92,10 @@ export default {
buttonMain buttonMain
}, },
props: { props: {
alwaysEmitFieldErrorOnEvent: {
type: Boolean,
default: false
},
type: { type: {
type: String, type: String,
default: 'text' default: 'text'
@ -136,7 +140,7 @@ export default {
default: () => {} default: () => {}
} }
}, },
emits: ['focus', 'update:modelValue', 'textboxQuestionEvent.inputIdAssigned', 'click-event'], emits: ['focus', 'update:modelValue', 'textboxQuestionEvent.inputIdAssigned', 'click-event', 'field-has-error'],
setup(props) { setup(props) {
const propsClone = { ...props }; const propsClone = { ...props };
const { modelValue } = propsClone; const { modelValue } = propsClone;
@ -218,6 +222,12 @@ export default {
this.$emit('textboxQuestionEvent.inputIdAssigned', this.inputId); this.$emit('textboxQuestionEvent.inputIdAssigned', this.inputId);
}, },
methods: { methods: {
blurPlus() {
this.handleChange(this.value);
if (this.alwaysEmitFieldErrorOnEvent) {
this.$emit('field-has-error', !this.meta.valid);
}
},
trimOnPaste(evt) { trimOnPaste(evt) {
evt.stopPropagation(); evt.stopPropagation();
evt.preventDefault(); evt.preventDefault();
@ -254,6 +264,8 @@ export default {
if (result.valid) { if (result.valid) {
this.handleChange(this.value); this.handleChange(this.value);
this.$emit('click-event'); this.$emit('click-event');
} else if (this.alwaysEmitFieldErrorOnEvent) {
this.$emit('field-has-error', true);
} }
} }
} }
@ -291,6 +303,18 @@ input::-webkit-date-and-time-value {
} }
.textbox-question { .textbox-question {
&.has-error {
.input-wrapper.has-search-icon {
input.form-control[type='text'] {
border-color: #db0020;
}
button[type='submit'] {
border-color: #db0020;
background-color: #fbe5e9;
background-image: url($svg-error-search-icon);
}
}
}
label { label {
color: $black; color: $black;
font-weight: 600; font-weight: 600;

View file

@ -94,10 +94,11 @@ export default {
} }
if (error.response.status !== 404) { if (error.response.status !== 404) {
global.$logger.logError(`${method}: ${endpoint}: ${error.message}`, error.response); const errorData = { method, url, payload, error };
global.$logger.logError(`${method}: ${endpoint}`, errorData);
if (bailoutOnError && global.bailoutOnAxiosError !== undefined) if (bailoutOnError && global.bailoutOnAxiosError !== undefined)
{ {
global.bailoutOnAxiosError({ url, error }); global.bailoutOnAxiosError(errorData);
} }
} }
return reject(error.response); return reject(error.response);

View file

@ -21,7 +21,8 @@ global.$logger = {
function setupMocksForHttpClient({ function setupMocksForHttpClient({
endpoint = null, endpoint = null,
isError = false, isError = false,
additionalData = null additionalData = null,
bailoutOnError = false
}) { }) {
// Clear node module // Clear node module
axios.mockClear(); axios.mockClear();
@ -57,7 +58,8 @@ function setupMocksForHttpClient({
return { return {
endpoint, endpoint,
logApiCall: true logApiCall: true,
bailoutOnError
}; };
} }
@ -92,3 +94,21 @@ it('Global Methods - Call Http Client - Should Reject Promise', () => {
expect(err.status).toEqual(500); expect(err.status).toEqual(500);
}); });
}); });
it('Global Methods - Call Http Client - Rejected Promised - bailoutOnError: true - Calls global.bailoutOnAxiosError', () => {
// Arrange
const endpoint = 'https://mock.safelite.com';
const httpArgs = setupMocksForHttpClient({
endpoint,
isError: true,
bailoutOnError: true
});
analyticsMixIn.methods.pushEventToGA = jest.fn();
global.bailoutOnAxiosError = jest.fn();
// Act
globalMethods.callHttpClient(httpArgs).catch((err) => {
// Assert
expect(global.bailoutOnAxiosError).toHaveBeenCalledTimes(1);
});
});

View file

@ -19,11 +19,12 @@ export function getLineItems(order) {
* @returns {[]} array of service line items * @returns {[]} array of service line items
*/ */
export function getServiceLineItems(order) { export function getServiceLineItems(order) {
const { supportingItems, glassParts, otherParts } = getLineItems(order); const { supportingItems, glassParts, otherParts, feeItems } = getLineItems(order);
return [ return [
...(supportingItems ?? []), ...(supportingItems ?? []),
...(glassParts ?? []), ...(glassParts ?? []),
...(otherParts ?? []) ...(otherParts ?? []),
...(feeItems ?? [])
]; ];
} }
@ -33,10 +34,9 @@ export function getServiceLineItems(order) {
* @returns {[]} array of non-service line items * @returns {[]} array of non-service line items
*/ */
export function getNonServiceLineItems(order) { export function getNonServiceLineItems(order) {
const { vaps, feeItems } = getLineItems(order); const { vaps } = getLineItems(order);
return [ return [
...(vaps ?? []), ...(vaps ?? [])
...(feeItems ?? [])
]; ];
} }
@ -142,3 +142,18 @@ export function getSalesTax(order) {
export function getCartTotal(order) { export function getCartTotal(order) {
return Math.round((getSubtotal(order) + getSalesTax(order)) * 100) / 100; return Math.round((getSubtotal(order) + getSalesTax(order)) * 100) / 100;
} }
/**
* Returns the total price of all recal child parts on the order
* @param {object} order order object
* @returns {number} total price of all recal child parts on the order
*/
export function getRecalibrationTotal(order) {
const itemsWithRecalibration = getServiceLineItems(order)
.filter((item) => item.requiresRecalibration);
const recalibrationLineItems = itemsWithRecalibration.map((item) => {
return item.childParts?.find((child) => child.partType === partTypeStrings.RECALIBRATION || child.partType === partTypeStrings.ADAS_RECALIBRATION) ?? {};
});
return getPriceOfLineItems(recalibrationLineItems);
}

View file

@ -93,10 +93,12 @@ describe('cart-helper', () => {
const result = getServiceLineItems(order); const result = getServiceLineItems(order);
// Assert // Assert
expect(result).toHaveLength(3); expect(result).toHaveLength(5);
expect(result).toContain(supportingLineItem); expect(result).toContain(supportingLineItem);
expect(result).toContain(glassLineItem); expect(result).toContain(glassLineItem);
expect(result).toContain(otherLineItem); expect(result).toContain(otherLineItem);
expect(result).toContain(mobileFeeLineItem);
expect(result).toContain(recycleFeeLineItem);
}); });
test('Returns empty array when line items are null', () => { test('Returns empty array when line items are null', () => {
@ -124,10 +126,8 @@ describe('cart-helper', () => {
const result = getNonServiceLineItems(order); const result = getNonServiceLineItems(order);
// Assert // Assert
expect(result).toHaveLength(3); expect(result).toHaveLength(1);
expect(result).toContain(vapsLineItem); expect(result).toContain(vapsLineItem);
expect(result).toContain(mobileFeeLineItem);
expect(result).toContain(recycleFeeLineItem);
}); });
test('Returns empty array when line items are null', () => { test('Returns empty array when line items are null', () => {
@ -403,7 +403,7 @@ describe('cart-helper', () => {
describe.each([ describe.each([
[330, coverageType.ITAC], [330, coverageType.ITAC],
[330, coverageType.NO_COMP], [330, coverageType.NO_COMP],
[310, coverageType.Deductible]])('getSubtotal with line items', (expected, type) => { [160, coverageType.Deductible]])('getSubtotal with line items', (expected, type) => {
test(`Returns ${expected} cart subtotal when verified ${getEnumName(coverageType, type)}`, () => { test(`Returns ${expected} cart subtotal when verified ${getEnumName(coverageType, type)}`, () => {
// Arrange // Arrange
const order = { const order = {
@ -480,7 +480,7 @@ describe('cart-helper', () => {
describe.each([ describe.each([
[21, coverageType.ITAC], [21, coverageType.ITAC],
[21, coverageType.NO_COMP], [21, coverageType.NO_COMP],
[15, coverageType.Deductible]])('getSalesTax with line items', (expected, type) => { [4, coverageType.Deductible]])('getSalesTax with line items', (expected, type) => {
test(`Returns ${expected} when coverageType is ${getEnumName(coverageType, type)}`, () => { test(`Returns ${expected} when coverageType is ${getEnumName(coverageType, type)}`, () => {
// Arrange // Arrange
const order = { const order = {
@ -565,7 +565,7 @@ describe('cart-helper', () => {
describe.each([ describe.each([
[351, coverageType.ITAC], [351, coverageType.ITAC],
[351, coverageType.NO_COMP], [351, coverageType.NO_COMP],
[325, coverageType.Deductible] [164, coverageType.Deductible]
])('getCartTotal items', (expected, type) => { ])('getCartTotal items', (expected, type) => {
test(`Returns ${expected} cart total when verified ${getEnumName(coverageType, type)}`, () => { test(`Returns ${expected} cart total when verified ${getEnumName(coverageType, type)}`, () => {
// Arrange // Arrange

View file

@ -32,9 +32,16 @@ export class Logger {
} }
formatLogEntry(message, details) { formatLogEntry(message, details) {
return `Application: ${applicationConfig.APPLICATION_NAME}\n${message}\n${ let json = '';
details ? JSON.stringify(details, undefined, 2) : '' if (details) {
}`; if (details instanceof Error) {
json = JSON.stringify(details, Object.getOwnPropertyNames(details), 2);
} else {
json = JSON.stringify(details, undefined, 2)
}
}
return `Application: ${applicationConfig.APPLICATION_NAME} \n${message} \n${json}`;
} }
writeLogEntry(endpoint, logEntry) { writeLogEntry(endpoint, logEntry) {

View file

@ -4,8 +4,8 @@ import submitType from '@/constants/submit-type';
/* /*
Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing
*/ */
async function saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA }) { async function saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError }) {
const savedSessionInfo = await store.saveSession({ submitAfterSave, createWorkOrderNumberForPIA }); const savedSessionInfo = await store.saveSession({ submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError });
if (savedSessionInfo) { if (savedSessionInfo) {
store.setSaveSessionInfo(savedSessionInfo.data); store.setSaveSessionInfo(savedSessionInfo.data);
} }
@ -16,11 +16,11 @@ async function saveSessionHelper(store, { submitAfterSave, createWorkOrderNumber
This will also set Referral information in the store after saving, and then This will also set Referral information in the store after saving, and then
update the cookie. To force synchronous behavior pass in 'true' for shouldAwaitSaveSessionQueue update the cookie. To force synchronous behavior pass in 'true' for shouldAwaitSaveSessionQueue
*/ */
export async function saveSession({ shouldAwaitSaveSessionQueue = false, submitAfterSave = false, createWorkOrderNumberForPIA = false }) { export async function saveSession({ shouldAwaitSaveSessionQueue = false, submitAfterSave = false, createWorkOrderNumberForPIA = false, bailoutOnError = false }) {
const store = useMainStore(); const store = useMainStore();
const saveSessionPromise = store.applicationUser.saveSessionPromise const saveSessionPromise = store.applicationUser.saveSessionPromise
? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA })) ? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError }))
: saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA }); : saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError });
store.setSaveSessionPromise(saveSessionPromise); store.setSaveSessionPromise(saveSessionPromise);
@ -38,7 +38,8 @@ export async function submitWorkOrder({ submitType }) {
store.resetSubmittedOrder(); store.resetSubmittedOrder();
await saveSession({ await saveSession({
shouldAwaitSaveSessionQueue: true, shouldAwaitSaveSessionQueue: true,
submitAfterSave: true submitAfterSave: true,
bailoutOnError: true
}); });
await store.createSubmittedOrder(submitType); await store.createSubmittedOrder(submitType);
} }

View file

@ -1,4 +1,6 @@
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import settleAllPromises from '@/helpers/layout-helper';
function processZipCodeResults(request) { function processZipCodeResults(request) {
return request.then((serviceZipValidationResponse) => ({ return request.then((serviceZipValidationResponse) => ({
@ -29,7 +31,7 @@ export async function getPricedMobileFeePart(serviceZipCode) {
// Get the Mobile Fee Part // Get the Mobile Fee Part
const mobileFeePart = await useMainStore().getMobileFeePart(); const mobileFeePart = await useMainStore().getMobileFeePart();
if (mobileFeePart.data == null || mobileFeePart.data === '') { if (mobileFeePart?.data == null || mobileFeePart.data === '') {
return Promise.resolve(null); return Promise.resolve(null);
} }
@ -40,17 +42,6 @@ export async function getPricedMobileFeePart(serviceZipCode) {
return Promise.resolve(pricingResults[0]); return Promise.resolve(pricingResults[0]);
} }
export async function getServiceabilityDetails(serviceZipCode, lineItems) {
const serviceabilityDetails = await useMainStore().getServiceabilityDetails(
{
serviceZipCode,
lineItems
},
false
);
return Promise.resolve(serviceabilityDetails);
}
export async function getAvailabilityRating( export async function getAvailabilityRating(
startDate, startDate,
endDate, endDate,
@ -69,3 +60,45 @@ export async function getAvailabilityRating(
return Promise.resolve(shopStatus); return Promise.resolve(shopStatus);
} }
export function getProviderData() {
const mainStore = useMainStore();
const storeServiceLocation = mainStore.order.serviceLocation;
const storeSelectedAppointmentType = storeServiceLocation?.appointmentType;
const isMobileAppointment = storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE
|| storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const storeSelectedProvider = storeServiceLocation?.provider;
const serviceZipCode = storeServiceLocation?.zipCode || mainStore.order.customer.address.zipCode;
return { storeServiceLocation, isMobileAppointment, storeSelectedProvider, serviceZipCode }
}
export async function onProviderChanged() {
const { isMobileAppointment, storeSelectedProvider, serviceZipCode } = getProviderData();
if (!storeSelectedProvider) {
return;
}
const mainStore = useMainStore();
await mainStore.getBillToInfo(storeSelectedProvider.providerNumber);
const promiseResultMap = [
{
resultKey: 'glassFees',
promise: mainStore.getGlassFees()
.then(async (glassFees) => await mainStore.getCombinedQuote(glassFees.data))
.then(async (quotedGlassFees) => mainStore.updateGlassFees(quotedGlassFees))
},
];
if (isMobileAppointment && (mainStore.isNoComp || mainStore.isITAC)) {
promiseResultMap.push({
resultKey: 'priceMobileFee',
promise: getPricedMobileFeePart(serviceZipCode).then(mobileFee => mainStore.updateMobileFee(mobileFee))
});
} else {
mainStore.updateMobileFee(null);
}
await settleAllPromises(promiseResultMap);
}

View file

@ -2,7 +2,6 @@
exports[`cart-dropdown component initial data rendered as expected 1`] = ` exports[`cart-dropdown component initial data rendered as expected 1`] = `
Object { Object {
"RECYCLING_MODAL_REF_NAME": "RecycleModal",
"cartItemType": Object { "cartItemType": Object {
"MOBILE_FEE": "MOBILE FEE", "MOBILE_FEE": "MOBILE FEE",
"RECYCLE_FEE": "RECYCLE FEE", "RECYCLE_FEE": "RECYCLE FEE",
@ -16,6 +15,7 @@ Object {
"deductible": "DeductibleWidget", "deductible": "DeductibleWidget",
"guaranteeText": "GuaranteeCartItemTextWidget", "guaranteeText": "GuaranteeCartItemTextWidget",
"mobileFee": "MobileServiceWidget", "mobileFee": "MobileServiceWidget",
"recalibration": "RecalibrationWidget",
"recycleFee": "RecycleFeeWidget", "recycleFee": "RecycleFeeWidget",
"salesTax": "SalesTaxWidget", "salesTax": "SalesTaxWidget",
"servicePackage": "ServicePackageTitle", "servicePackage": "ServicePackageTitle",

View file

@ -73,10 +73,22 @@ describe('cart-dropdown component', () => {
expect(wrapper.vm.$data).toMatchSnapshot(); expect(wrapper.vm.$data).toMatchSnapshot();
}); });
describe('displays', () => { describe('displays', () => {
test('cart table', () => { test('cart table when not deductible only', () => {
// Arrange // Arrange
const reference = '.cart-table'; const reference = '.cart-table';
const { wrapper } = getMountedComponent({}, {}, {}); const part1 = { partType: partTypeStrings.FRONT_WIPER };
const storeData = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
lineItems: {
vaps: [part1],
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act // Act
const cartTable = wrapper.find(reference); const cartTable = wrapper.find(reference);
@ -84,9 +96,9 @@ describe('cart-dropdown component', () => {
// Assert // Assert
expect(cartTable.exists()).toBeTruthy(); expect(cartTable.exists()).toBeTruthy();
}); });
test('cart deductible when showDeductibleCartItem is true', () => { test('deductible-box when deductible only', () => {
// Arrange // Arrange
const reference = '#deductible-label'; const reference = '.deductible-box';
const storeData = { const storeData = {
order: { order: {
insuranceCoverage: { insuranceCoverage: {
@ -127,9 +139,9 @@ describe('cart-dropdown component', () => {
// Assert // Assert
expect(cartBasePrice.exists()).toBeTruthy(); expect(cartBasePrice.exists()).toBeTruthy();
}); });
test('non service package cart items', () => { test('additional cart items', () => {
// Arrange // Arrange
const reference = '.non-packaged.cart-item'; const reference = '.cart-item';
const part1 = { partType: 'apple' }; const part1 = { partType: 'apple' };
const part2 = { partType: 'banana' }; const part2 = { partType: 'banana' };
const part3 = { partType: 'orange' }; const part3 = { partType: 'orange' };
@ -155,38 +167,27 @@ describe('cart-dropdown component', () => {
const { wrapper } = getMountedComponent(storeData, {}, {}); const { wrapper } = getMountedComponent(storeData, {}, {});
// Act // Act
const nonPackagedCartItems = wrapper.find(reference); const cartItems = wrapper.findAll(reference);
// Assert // Assert
expect(nonPackagedCartItems.exists()).toBeTruthy(); expect(cartItems.length).toBeGreaterThan(1);
}); });
test('recycle cart item label when isRecycle true for some cart item', () => { test('cart footer when not deductible only', () => {
// Arrange // Arrange
const reference = '#recycle-fee-label'; const reference = '.cart-footer';
const isExpanded = true; const part1 = { partType: partTypeStrings.FRONT_WIPER };
const initialPropsData = { isExpanded };
const storeData = { const storeData = {
order: { order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
lineItems: { lineItems: {
feeItems: [ vaps: [part1],
{ partNumber: partNumberStrings.RECYCLE_FEE }
]
} }
} }
}; };
const { wrapper } = getMountedComponent(storeData, {}, initialPropsData); const { wrapper } = getMountedComponent(storeData, {}, {});
// Act
const recycleFeeLabel = wrapper.find(reference);
// Assert
expect(wrapper.vm.recycleFeeCartItem).not.toBeNull();
expect(recycleFeeLabel.exists()).toBeTruthy();
});
test('cart footer', () => {
// Arrange
const reference = '#cart-footer';
const { wrapper } = getMountedComponent({}, {}, {});
// Act // Act
const cartFooter = wrapper.find(reference); const cartFooter = wrapper.find(reference);
@ -197,7 +198,19 @@ describe('cart-dropdown component', () => {
test('subtotal', () => { test('subtotal', () => {
// Arrange // Arrange
const reference = '#cart-subtotal'; const reference = '#cart-subtotal';
const { wrapper } = getMountedComponent({}, {}, {}); const part1 = { partType: partTypeStrings.FRONT_WIPER };
const storeData = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
lineItems: {
vaps: [part1],
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act // Act
const subtotal = wrapper.find(reference); const subtotal = wrapper.find(reference);
@ -208,7 +221,19 @@ describe('cart-dropdown component', () => {
test('tax', () => { test('tax', () => {
// Arrange // Arrange
const reference = '#cart-sales-tax'; const reference = '#cart-sales-tax';
const { wrapper } = getMountedComponent({}, {}, {}); const part1 = { partType: partTypeStrings.FRONT_WIPER };
const storeData = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
lineItems: {
vaps: [part1],
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act // Act
const salesTax = wrapper.find(reference); const salesTax = wrapper.find(reference);
@ -222,8 +247,16 @@ describe('cart-dropdown component', () => {
const isExpanded = true; const isExpanded = true;
const showAsPaid = true; const showAsPaid = true;
const initialPropsData = { isExpanded, showAsPaid }; const initialPropsData = { isExpanded, showAsPaid };
const part1 = { partType: partTypeStrings.FRONT_WIPER };
const storeData = { const storeData = {
order: { order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
lineItems: {
vaps: [part1],
},
payment: { payment: {
isPayInAdvance: true isPayInAdvance: true
} }
@ -240,7 +273,19 @@ describe('cart-dropdown component', () => {
test('bottom amount due', () => { test('bottom amount due', () => {
// Arrange // Arrange
const reference = '#bottom-amount-due'; const reference = '#bottom-amount-due';
const { wrapper } = getMountedComponent({}, {}, {}); const part1 = { partType: partTypeStrings.FRONT_WIPER };
const storeData = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
lineItems: {
vaps: [part1],
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, {});
// Act // Act
const bottomAmountDue = wrapper.find(reference); const bottomAmountDue = wrapper.find(reference);
@ -293,27 +338,6 @@ describe('cart-dropdown component', () => {
// Assert // Assert
expect(cartBasePrice.exists()).toBeFalsy(); expect(cartBasePrice.exists()).toBeFalsy();
}); });
test('recycle cart item label when isRecycle false for all cart items', () => {
// Arrange
const reference = '#recycle-fee-label';
const isExpanded = true;
const initialPropsData = { isExpanded };
const storeData = {
order: {
lineItems: {
supportingItems: []
}
}
};
const { wrapper } = getMountedComponent(storeData, {}, initialPropsData);
// Act
const recycleFeeLabel = wrapper.find(reference);
// Assert
expect(wrapper.vm.recycleFeeCartItem).toBeNull();
expect(recycleFeeLabel.exists()).toBeFalsy();
});
}); });
describe('computed', () => { describe('computed', () => {
describe('amountDue', () => { describe('amountDue', () => {
@ -475,7 +499,7 @@ describe('cart-dropdown component', () => {
expect(result.sort(sortByPartType)).toStrictEqual(expected.sort(sortByPartType)); expect(result.sort(sortByPartType)).toStrictEqual(expected.sort(sortByPartType));
}); });
}); });
describe('servicePackageCartItems', () => { describe('cartItems', () => {
test.each([ test.each([
[undefined], [null], [[]] [undefined], [null], [[]]
])('returns empty list when package contents %p', (packageContents) => { ])('returns empty list when package contents %p', (packageContents) => {
@ -485,7 +509,7 @@ describe('cart-dropdown component', () => {
const expected = []; const expected = [];
// Act // Act
const result = wrapper.vm.servicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result).toStrictEqual(expected); expect(result).toStrictEqual(expected);
@ -531,7 +555,7 @@ describe('cart-dropdown component', () => {
const wrapper = shallowMount(cartDropdown, mountOptions); const wrapper = shallowMount(cartDropdown, mountOptions);
// Act // Act
const result = wrapper.vm.servicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result.length).toBe(1); expect(result.length).toBe(1);
@ -577,7 +601,7 @@ describe('cart-dropdown component', () => {
const wrapper = shallowMount(cartDropdown, mountOptions); const wrapper = shallowMount(cartDropdown, mountOptions);
// Act // Act
const result = wrapper.vm.servicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result.length).toBe(1); expect(result.length).toBe(1);
@ -623,7 +647,7 @@ describe('cart-dropdown component', () => {
const wrapper = shallowMount(cartDropdown, mountOptions); const wrapper = shallowMount(cartDropdown, mountOptions);
// Act // Act
const result = wrapper.vm.servicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result.length).toBe(1); expect(result.length).toBe(1);
@ -685,7 +709,7 @@ describe('cart-dropdown component', () => {
const wrapper = shallowMount(cartDropdown, mountOptions); const wrapper = shallowMount(cartDropdown, mountOptions);
// Act // Act
const result = wrapper.vm.servicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result.length).toBe(3); expect(result.length).toBe(3);
@ -699,8 +723,6 @@ describe('cart-dropdown component', () => {
name: expectedRainDefenseName name: expectedRainDefenseName
})); }));
}); });
});
describe('nonServicePackageCartItems', () => {
test('when recycleFee, includes recycle fee', () => { test('when recycleFee, includes recycle fee', () => {
// Arrange // Arrange
const storeData = { const storeData = {
@ -717,7 +739,9 @@ describe('cart-dropdown component', () => {
const { wrapper } = getMountedComponent(storeData); const { wrapper } = getMountedComponent(storeData);
// Act // Act
const result = wrapper.vm.nonServicePackageCartItems; const result = wrapper.vm.cartItems;
console.log(result);
// Assert // Assert
expect(result).toContainEqual(expect.objectContaining({ expect(result).toContainEqual(expect.objectContaining({
@ -736,163 +760,13 @@ describe('cart-dropdown component', () => {
const { wrapper } = getMountedComponent(storeData); const { wrapper } = getMountedComponent(storeData);
// Act // Act
const result = wrapper.vm.nonServicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result).toContainEqual(expect.objectContaining({ expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.MOBILE_FEE partType: partTypeStrings.MOBILE_FEE
})); }));
}); });
test('front wiper not included when front wiper in service package', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => [partTypeStrings.FRONT_WIPER]);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.FRONT_WIPER
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result).not.toContainEqual(expect.objectContaining({
partType: partTypeStrings.FRONT_WIPER
}));
});
test('rear wiper not included when rear wiper in service package', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => [partTypeStrings.REAR_WIPER]);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.REAR_WIPER
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result).not.toContainEqual(expect.objectContaining({
partType: partTypeStrings.REAR_WIPER
}));
});
test('rain defense not included when rain defense in service package', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => [partTypeStrings.RAIN_DEFENSE]);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.RAIN_DEFENSE
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result).not.toContainEqual(expect.objectContaining({
partType: partTypeStrings.RAIN_DEFENSE
}));
});
test('front wiper included when not in service package and in vaps', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => []);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.FRONT_WIPER
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.FRONT_WIPER
}));
});
test('rear wiper included when not in service package and in vaps', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => []);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.REAR_WIPER
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.REAR_WIPER
}));
});
test('rain defense included when not in service package and in vaps', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => []);
const storeData = {
order: {
lineItems: {
vaps: [
{
partType: partTypeStrings.RAIN_DEFENSE
}
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.RAIN_DEFENSE
}));
});
test('item in vaps outside front wiper, rear wiper, and rain defense never included', () => { test('item in vaps outside front wiper, rear wiper, and rain defense never included', () => {
// Arrange // Arrange
getPriceOfLineItems.mockImplementation(() => 123); getPriceOfLineItems.mockImplementation(() => 123);
@ -911,143 +785,13 @@ describe('cart-dropdown component', () => {
const { wrapper } = getMountedComponent(storeData); const { wrapper } = getMountedComponent(storeData);
// Act // Act
const result = wrapper.vm.nonServicePackageCartItems; const result = wrapper.vm.cartItems;
// Assert // Assert
expect(result).not.toContainEqual(expect.objectContaining({ expect(result).not.toContainEqual(expect.objectContaining({
partType: partTypeStrings.RECALIBRATION partType: partTypeStrings.RECALIBRATION
})); }));
}); });
test('returns expected when multiple variables apply', () => {
// Arrange
getPriceOfLineItems.mockImplementation(() => 123);
getPackageContents.mockImplementationOnce(() => [partTypeStrings.FRONT_WIPER]);
const storeData = {
order: {
lineItems: {
feeItems: [
{ partType: partTypeStrings.MOBILE_FEE },
{ partNumber: partNumberStrings.RECYCLE_FEE }
],
vaps: [
{ partType: partTypeStrings.RECALIBRATION },
{ partType: partTypeStrings.FRONT_WIPER },
{ partType: partTypeStrings.REAR_WIPER }
]
}
}
};
const { wrapper } = getMountedComponent(storeData);
// Act
const result = wrapper.vm.nonServicePackageCartItems;
// Assert
expect(result.length).toBe(3);
expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.REPLACE_FEE
}));
expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.MOBILE_FEE
}));
expect(result).toContainEqual(expect.objectContaining({
partType: partTypeStrings.REAR_WIPER
}));
});
});
describe('servicePackageLabelWidget', () => {
test.each([
[null],
[undefined]
])('returns "" when servicePackageNames %p', (servicePackageNames) => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => servicePackageNames)
}
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(cartDropdown, mountOptions);
// Act
const result = wrapper.vm.servicePackageLabelWidget;
// Assert
expect(result).toBe('');
});
test('returns "" when servicePackageTier not found in servicePackageNames', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
getHighestFullySatisfiedTier.mockImplementationOnce(() => 'some other tier');
const servicePackageNames = [{ Name: 'some tier' }];
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => servicePackageNames)
}
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(cartDropdown, mountOptions);
// Act
const result = wrapper.vm.servicePackageLabelWidget;
// Assert
expect(result).toBe('');
});
test('returns "" when servicePackageTier has no SubWidgetName', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const tier = 'tier1';
getHighestFullySatisfiedTier.mockImplementationOnce(() => tier);
const servicePackageNames = [{ Name: tier }];
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => servicePackageNames)
}
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(cartDropdown, mountOptions);
// Act
const result = wrapper.vm.servicePackageLabelWidget;
// Assert
expect(result).toBe('');
});
test('returns expected when servicePackageTier has SubWidgetName', () => {
// Arrange
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
});
const tier = 'tier1';
getHighestFullySatisfiedTier.mockImplementationOnce(() => tier);
const expected = 'name of sub widget';
const servicePackageNames = [{
Name: tier,
SubWidgetName: expected
}];
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => servicePackageNames)
}
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(cartDropdown, mountOptions);
// Act
const result = wrapper.vm.servicePackageLabelWidget;
// Assert
expect(result).toBe(expected);
});
}); });
describe('recycleFeeCartItem', () => { describe('recycleFeeCartItem', () => {
test.each([ test.each([

View file

@ -1,39 +1,39 @@
<template> <template>
<div class="cart-table"> <div
v-if="!isDeductibleOnly"
class="cart-table">
<div class="cart-item-list"> <div class="cart-item-list">
<div <div
id="cart-deductible-or-base-price" v-if="showDeductibleCartItem"
class="cart-item"> class="cart-item">
<div class="price-row"> <div class="price-row">
<template <span id="deductible-label">{{ deductibleLabel }}</span>
v-if="showDeductibleCartItem"> <span id="deductible-value">{{ getDisplayed(deductible) }}</span>
<span id="deductible-label">{{ deductibleLabel }}</span> </div>
<span id="deductible-value">{{ getDisplayed(deductible) }}</span> </div>
</template> <div
<template v-else
v-else> class="cart-item">
<span id="base-price-label">{{ basePriceLabel }}</span> <div class="price-row">
<span id="base-price-value">{{ getDisplayed(servicePrice) }}</span> <span id="base-price-label">{{ basePriceLabel }}</span>
</template> <span id="base-price-value">{{ getDisplayed(servicePrice) }}</span>
</div>
</div>
<div
v-if="showRecalibrationCartItem"
class="cart-item">
<div class="price-row">
<span id="recalibration-label">{{ recalibrationLabel }}</span>
<span id="recalibration-value">{{ formatAmountInDollars(recalibrationPrice) }}</span>
</div> </div>
</div> </div>
<!-- NonPackaged Cart Items -->
<div <div
v-for="(item, i) in nonServicePackageCartItems" v-for="(item, i) in cartItems"
:key="i" :key="i"
class="cart-item non-packaged"> class="cart-item packaged">
<div class="price-row"> <div class="price-row">
<span <span>{{ item?.name ?? '' }}</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> <span>{{ formatAmountInDollars(item?.subTotal ?? 0) }}</span>
</div> </div>
<textLink <textLink
@ -48,32 +48,12 @@
</template> </template>
</textLink> </textLink>
</div> </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>
<div <div
id="cart-footer" id="cart-footer"
class="cart-footer pb-5"> class="cart-footer">
<div <div
v-if="!isUnverified"
id="cart-subtotal-and-tax" id="cart-subtotal-and-tax"
class="subtotal-and-tax"> class="subtotal-and-tax">
<div <div
@ -103,11 +83,10 @@
<span id="bottom-amount-due-value">{{ getDisplayed(amountDue) }}</span> <span id="bottom-amount-due-value">{{ getDisplayed(amountDue) }}</span>
</div> </div>
</div> </div>
<contentGroupModal </div>
:ref="RECYCLING_MODAL_REF_NAME" <div v-else class="deductible-box">
:cmsWidgetName="recyclingModalCmsWidgetName"> <span class="deductible-label">{{ deductibleLabel }}:</span>
<textBlock cmsWidgetName="RecycleTextBlock" /> <span class="deductible-value">{{ getDisplayed(deductible) }}</span>
</contentGroupModal>
</div> </div>
</template> </template>
@ -117,7 +96,6 @@ import contentGroupModal from '@/iss-components/content-group-modal/content-grou
import textBlock from '@/digital-components/text-block/text-block.vue'; 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 { formatAmountInDollars } from '@/helpers/text-helper.js'; import { formatAmountInDollars } from '@/helpers/text-helper.js';
import { getHighestFullySatisfiedTier, getPackageContents } from '@/helpers/service-package-helper.js';
// Constants // Constants
import { experimentSettings } from '@/constants/experiments'; import { experimentSettings } from '@/constants/experiments';
@ -128,6 +106,7 @@ import {
getCartTotal, getCartTotal,
getDeductible, getDeductible,
getLineItems, getMobileFeeLineItem, getLineItems, getMobileFeeLineItem,
getRecalibrationTotal,
getRecycleFeeLineItem, getSalesTax, getRecycleFeeLineItem, getSalesTax,
getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp, getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp,
isOrderUnverified isOrderUnverified
@ -136,19 +115,15 @@ import { getPriceOfLineItems, getTaxOfLineItems } from '@/helpers/price-calculat
import { processIfStatements } from '@/helpers/cms-content-helper'; import { processIfStatements } from '@/helpers/cms-content-helper';
const VERIFYING_COVERAGE = 'Verifying coverage'; const VERIFYING_COVERAGE = 'Verifying coverage';
const RECYCLING_MODAL_REF_NAME = 'RecycleModal';
export default { export default {
name: 'confirmation-cart', name: 'confirmation-cart',
components: { components: {
contentGroupModal,
textBlock,
textLink textLink
}, },
props: { props: {
readOnly: Boolean, readOnly: Boolean,
showAsPaid: Boolean, showAsPaid: Boolean,
recyclingModalCmsWidgetName: String,
submittedOrder: Object submittedOrder: Object
}, },
data() { data() {
@ -158,6 +133,7 @@ export default {
amountPaid: 'AmountPaidTextWidget', amountPaid: 'AmountPaidTextWidget',
deductible: 'DeductibleWidget', deductible: 'DeductibleWidget',
basePrice: 'BasePriceWidget', basePrice: 'BasePriceWidget',
recalibration: 'RecalibrationWidget',
subtotal: 'SubtotalWidget', subtotal: 'SubtotalWidget',
salesTax: 'SalesTaxWidget', salesTax: 'SalesTaxWidget',
recycleFee: 'RecycleFeeWidget', recycleFee: 'RecycleFeeWidget',
@ -167,8 +143,7 @@ export default {
warrantyText: 'WarrantyCartItemTextWidget', warrantyText: 'WarrantyCartItemTextWidget',
guaranteeText: 'GuaranteeCartItemTextWidget' guaranteeText: 'GuaranteeCartItemTextWidget'
}, },
cartItemType, cartItemType
RECYCLING_MODAL_REF_NAME
}; };
}, },
computed: { computed: {
@ -181,9 +156,18 @@ export default {
deductible() { deductible() {
return getDeductible(this.cartOrder); return getDeductible(this.cartOrder);
}, },
isDeductibleOnly() {
return this.cartItems.length === 0;
},
showDeductibleCartItem() { showDeductibleCartItem() {
return this.isUnverified || (!this.isNoComp && !this.isITAC); return this.isUnverified || (!this.isNoComp && !this.isITAC);
}, },
showRecalibrationCartItem() {
return this.recalibrationPrice > 0;
},
recalibrationPrice() {
return getRecalibrationTotal(this.cartOrder);
},
lineItems() { lineItems() {
return getLineItems(this.cartOrder); return getLineItems(this.cartOrder);
}, },
@ -191,7 +175,8 @@ export default {
return getRecycleFeeLineItem(this.cartOrder); return getRecycleFeeLineItem(this.cartOrder);
}, },
servicePrice() { servicePrice() {
return getPriceOfLineItems(getServiceLineItems(this.cartOrder)) ?? 0; const price = getPriceOfLineItems(getServiceLineItems(this.cartOrder)) ?? 0;
return price - this.recalibrationPrice;
}, },
isUnverified() { isUnverified() {
return isOrderUnverified(this.cartOrder); return isOrderUnverified(this.cartOrder);
@ -229,47 +214,21 @@ export default {
]; ];
return result; return result;
}, },
vehicleDamage() { cartItems() {
return this.cartOrder.damage;
},
servicePackageTier() {
const { glassToReplace, isRepair } = this.vehicleDamage;
const { vaps } = this.lineItems;
return getHighestFullySatisfiedTier(
glassToReplace ?? [],
this.availableLineItems,
isRepair,
vaps ?? []
);
},
partTypesInServicePackage() {
const { glassToReplace, isRepair } = this.vehicleDamage;
return getPackageContents(
glassToReplace ?? [],
this.availableLineItems,
isRepair,
this.servicePackageTier
) ?? [];
},
servicePackageCartItems() {
const items = []; const items = [];
if (this.partTypesInServicePackage.includes(partTypeStrings.FRONT_WIPER) ?? this.frontWipersCartItem) { const isRecycleFeeHidden = this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_HIDDEN) === 'true';
items.push(this.frontWipersCartItem); const isMobileFeeHidden = this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN) === 'true';
if (this.isITAC || this.isNoComp) {
items.push(this.warrantyCartItem);
} }
if (this.partTypesInServicePackage.includes(partTypeStrings.REAR_WIPER) ?? this.rearWipersCartItem) { if (this.recycleFeeCartItem && !isRecycleFeeHidden) {
items.push(this.rearWipersCartItem); items.push(this.recycleFeeCartItem);
} }
if (this.partTypesInServicePackage.includes(partTypeStrings.RAIN_DEFENSE) ?? this.rainDefenseCartItem) { if (this.mobileFeeCartItem && !isMobileFeeHidden) {
items.push(this.rainDefenseCartItem); items.push(this.mobileFeeCartItem);
} }
return items;
},
nonServicePackageCartItems() {
const vapPartTypesInOrder = Array.from(new Set(this.lineItems.vaps?.map((vap) => vap.partType) ?? [])); const vapPartTypesInOrder = Array.from(new Set(this.lineItems.vaps?.map((vap) => vap.partType) ?? []));
const vapPartTypesInOrderButNotPackage = vapPartTypesInOrder const vapsCartItems = vapPartTypesInOrder.map((partType) => {
.filter((partType) => !this.partTypesInServicePackage.includes(partType))
?? [];
const vapsCartItemsNotInPackage = vapPartTypesInOrderButNotPackage.map((partType) => {
switch (partType) { switch (partType) {
case partTypeStrings.FRONT_WIPER: case partTypeStrings.FRONT_WIPER:
return this.frontWipersCartItem; return this.frontWipersCartItem;
@ -281,18 +240,7 @@ export default {
return null; return null;
} }
}); });
const items = vapsCartItemsNotInPackage.filter((item) => item != null); items.push(...vapsCartItems.filter((item) => item != null));
const isRecycleFeeHidden = this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_HIDDEN) === 'true';
const isMobileFeeHidden = this.getSettingValue(experimentSettings.ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN) === 'true';
if (this.recycleFeeCartItem && !isRecycleFeeHidden) {
items.push(this.recycleFeeCartItem);
}
if (this.mobileFeeCartItem && !isMobileFeeHidden) {
items.push(this.mobileFeeCartItem);
}
if (this.isITAC || this.isNoComp) {
items.push(this.warrantyCartItem);
}
return items; return items;
}, },
frontWipersCartItem() { frontWipersCartItem() {
@ -304,12 +252,6 @@ export default {
rainDefenseCartItem() { rainDefenseCartItem() {
return this.getCartItemForVapsPart(partTypeStrings.RAIN_DEFENSE); return this.getCartItemForVapsPart(partTypeStrings.RAIN_DEFENSE);
}, },
allCartItems() {
return [
...this.servicePackageCartItems,
...this.nonServicePackageCartItems
];
},
amountDueLabel() { amountDueLabel() {
return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT); return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}, },
@ -327,27 +269,15 @@ export default {
this.getCustomValueFromString this.getCustomValueFromString
); );
}, },
recalibrationLabel() {
return this.getCmsContent(this.widget.recalibration, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
subtotalLabel() { subtotalLabel() {
return this.getCmsContent(this.widget.subtotal, widgetFields.TEXT_BLOCK_WIDGET.TEXT); return this.getCmsContent(this.widget.subtotal, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}, },
salesTaxLabel() { salesTaxLabel() {
return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT); return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}, },
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() { recycleFeeCartItem() {
return this.recycleFeeLineItem return this.recycleFeeLineItem
? this.getCartItem( ? this.getCartItem(
@ -452,8 +382,6 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss"; @import "@/styles/ux-variables-svg-strings.scss";
$RECYCLING_MODAL_REF_NAME: 'RecycleModal';
.cart-table { .cart-table {
max-height: 50rem; max-height: 50rem;
transition: all 150ms ease-in; transition: all 150ms ease-in;
@ -508,22 +436,18 @@ $RECYCLING_MODAL_REF_NAME: 'RecycleModal';
padding: .5rem 1rem; padding: .5rem 1rem;
} }
} }
.deductible-box {
:deep(.modal-content a.external-text) { display: flex;
text-underline-offset: 0.25rem; align-items: center;
line-height: 2; gap: .25rem;
padding: 0 0 0.25rem 0;
font-weight: 500;
max-width: -webkit-fit-content;
max-width: -moz-fit-content;
max-width: fit-content;
} }
.deductible-label {
:deep(##{$RECYCLING_MODAL_REF_NAME} h5) { color: $black;
text-align: center; font-weight: 600;
} }
.deductible-value {
:deep(#recycle-fee-label a){ color: $green;
line-height: initial; font-weight: $font-weight-bold;
font-size: 1.625rem;
} }
</style> </style>

View file

@ -2,6 +2,7 @@
exports[`contact-details-drawer snapshot matches returns the initial data 1`] = ` exports[`contact-details-drawer snapshot matches returns the initial data 1`] = `
Object { Object {
"detailsSaved": false,
"emailAddress": "fred.tay@gmail.com", "emailAddress": "fred.tay@gmail.com",
"extension": null, "extension": null,
"firstName": "Frederick", "firstName": "Frederick",
@ -15,8 +16,8 @@ Object {
"rules": Object { "rules": Object {
"emailAddress": "email-required|email-address-format", "emailAddress": "email-required|email-address-format",
"extension": "extension-format", "extension": "extension-format",
"firstName": "first-name-required", "firstName": "policyholder-first-name-required",
"lastName": "last-name-required", "lastName": "policyholder-last-name-required",
"phoneNumber": "phone-number-required|phone-number-format", "phoneNumber": "phone-number-required|phone-number-format",
}, },
"widget": Object { "widget": Object {

View file

@ -1,7 +1,7 @@
// Components // Components
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import contactDetailsDrawer from '@/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue'; import contactDetailsDrawer from '@/iss-components/contact-details-drawer/contact-details-drawer.vue';
// Supporting Files // Supporting Files
import { getMountOptions } from '@/helpers/unit-test-helper.js'; import { getMountOptions } from '@/helpers/unit-test-helper.js';

View file

@ -9,25 +9,29 @@
@isModalOpened="setModalStatus" @isModalOpened="setModalStatus"
@footerButtonEvent="clickFooterButtonEvent"> @footerButtonEvent="clickFooterButtonEvent">
<template v-if="isModalOpened"> <template v-if="isModalOpened">
<div class="row"> <span class="name-section-label">{{ nameSectionLabel }}</span>
<div class="row form-group">
<textboxQuestion <textboxQuestion
class="col" class="col-lg-6"
ref="firstNameQuestion" ref="firstNameQuestion"
v-model="firstName" v-model="firstName"
inputId="firstName" inputId="firstName"
:displayQuestionText="false"
:cmsWidgetName="widget.firstNameQuestion" :cmsWidgetName="widget.firstNameQuestion"
isRequired isRequired
:validationRules="rules.firstName" /> :validationRules="rules.firstName" />
<textboxQuestion <textboxQuestion
class="col" class="col-lg-6 last-name-question"
ref="lastNameQuestion" ref="lastNameQuestion"
v-model="lastName" v-model="lastName"
inputId="lastName" inputId="lastName"
:displayQuestionText="false"
:cmsWidgetName="widget.lastNameQuestion" :cmsWidgetName="widget.lastNameQuestion"
isRequired isRequired
:validationRules="rules.lastName" /> :validationRules="rules.lastName" />
</div> </div>
<textboxQuestion <textboxQuestion
class="form-group"
ref="emailQuestion" ref="emailQuestion"
v-model="emailAddress" v-model="emailAddress"
inputId="emailAddress" inputId="emailAddress"
@ -35,6 +39,7 @@
isRequired isRequired
:validationRules="rules.emailAddress" /> :validationRules="rules.emailAddress" />
<textboxQuestion <textboxQuestion
class="form-group"
ref="phoneNumberQuestion" ref="phoneNumberQuestion"
v-model="phoneNumber" v-model="phoneNumber"
inputId="phoneNumber" inputId="phoneNumber"
@ -44,6 +49,7 @@
disableAutoFill disableAutoFill
:validationRules="rules.phoneNumber" /> :validationRules="rules.phoneNumber" />
<textboxQuestion <textboxQuestion
class="form-group"
ref="extensionQuestion" ref="extensionQuestion"
v-model="extension" v-model="extension"
inputId="extension" inputId="extension"
@ -73,7 +79,7 @@ export default {
modal, modal,
textboxQuestion textboxQuestion
}, },
emits: ['update-contact-details'], emits: ['update-contact-details', 'close-without-saving'],
data() { data() {
const { firstName, const { firstName,
lastName, lastName,
@ -97,13 +103,14 @@ export default {
drawerFooter: 'ContactDetailsDrawerFooterWidget' drawerFooter: 'ContactDetailsDrawerFooterWidget'
}, },
rules: { rules: {
firstName: globalRules.FIRST_NAME_REQUIRED, firstName: globalRules.POLICYHOLDER_FIRST_NAME_REQUIRED,
lastName: globalRules.LAST_NAME_REQUIRED, lastName: globalRules.POLICYHOLDER_LAST_NAME_REQUIRED,
emailAddress: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`, emailAddress: `${globalRules.EMAIL_ADDRESS_REQUIRED}|${globalRules.EMAIL_ADDRESS_FORMAT}`,
phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`, phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`,
extension: `${globalRules.EXTENSION_FORMAT}` extension: `${globalRules.EXTENSION_FORMAT}`
}, },
modalPositions modalPositions,
detailsSaved: false
}; };
}, },
computed: { computed: {
@ -121,13 +128,17 @@ export default {
}, },
phoneMask() { phoneMask() {
return MaskaFormattedMasks.PHONE_NUMBER; return MaskaFormattedMasks.PHONE_NUMBER;
} },
nameSectionLabel() {
return this.getCmsContent(this.widget.firstNameQuestion, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
},
}, },
methods: { methods: {
setModalStatus(isOpened) { setModalStatus(isOpened) {
this.isModalOpened = isOpened; this.isModalOpened = isOpened;
}, },
clickFooterButtonEvent() { clickFooterButtonEvent() {
this.detailsSaved = true;
this.saveContactDetails(); this.saveContactDetails();
this.closeModal(); this.closeModal();
}, },
@ -146,6 +157,9 @@ export default {
this.$emit('update-contact-details'); this.$emit('update-contact-details');
}, },
resetFormValues() { resetFormValues() {
if (!this.detailsSaved) {
this.$emit('close-without-saving');
}
const { firstName, const { firstName,
lastName, lastName,
emailAddress, emailAddress,
@ -156,6 +170,7 @@ export default {
this.emailAddress = emailAddress; this.emailAddress = emailAddress;
this.phoneNumber = servicePhone; this.phoneNumber = servicePhone;
this.extension = extension; this.extension = extension;
this.detailsSaved = false;
}, },
openModal() { openModal() {
this.modal.openModal(); this.modal.openModal();
@ -174,13 +189,18 @@ export default {
font-size: $h5-font-size; font-size: $h5-font-size;
font-weight: $font-weight-normal !important; font-weight: $font-weight-normal !important;
} }
.textbox-question { .form-group {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
:deep(label:has(b)) { align-items: start;
font-weight: $font-weight-normal; }
b { .name-section-label {
font-weight: 600; color: $black;
} font-weight: 600;
margin-bottom: .25rem;
}
.last-name-question {
@include media-breakpoint-down(lg) {
margin-top: .625rem;
} }
} }
} }

View file

@ -9,13 +9,12 @@
v-model="selectedAnswers" v-model="selectedAnswers"
isRequired isRequired
:isMetaValid="meta.valid" :isMetaValid="meta.valid"
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData" :questionsData="questionsData"
:validationRules="rules.optionRequired" :validationRules="rules.optionRequired"
:index="currentGlassIndex" :index="currentGlassIndex"
@forwardButtonAction="forwardButtonAction" @forwardButtonAction="forwardButtonAction"
@backClick="navigateBackByVehicleQuestions" /> @backClick="navigateBackByVehicleQuestions"
@needHelpClick="requestCallbackBailout" />
</Form> </Form>
</template> </template>
<script> <script>
@ -31,6 +30,8 @@ import { useMainStore } from '@/store';
import globalRules from '@/constants/global-rules'; import globalRules from '@/constants/global-rules';
import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin'; import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin';
import questionsPageLayout from '@/iss-components/questions-page-layout/questions-page-layout.vue'; import questionsPageLayout from '@/iss-components/questions-page-layout/questions-page-layout.vue';
import bailoutMessage from '@/constants/bailoutMessage';
import navigationScenarios from '@/router/router-constants/navigation-scenarios';
export default { export default {
name: 'capability-questions', name: 'capability-questions',
@ -67,18 +68,6 @@ export default {
}; };
}, },
computed: { computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent(
'AdditionalPartsQuestionsAlert',
'HeadlineText'
);
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent(
'AdditionalPartsQuestionsAlert',
'BodyText'
);
},
partsOrQuestionsData() { partsOrQuestionsData() {
return useMainStore().pageData(issPageValues.CAPABILITY_QUESTIONS) return useMainStore().pageData(issPageValues.CAPABILITY_QUESTIONS)
.partsOrQuestions; .partsOrQuestions;
@ -166,6 +155,9 @@ export default {
}); });
this.navigateForward(this.partsOrQuestionsData, null); this.navigateForward(this.partsOrQuestionsData, null);
},
requestCallbackBailout() {
this.$router.navigateBailout(bailoutMessage.RequestCallback());
} }
} }
}; };

View file

@ -5,6 +5,7 @@ Object {
"CANCEL_CLAIM_REF_NAME": "CancelClaimModal", "CANCEL_CLAIM_REF_NAME": "CancelClaimModal",
"RECAL_MODAL_REF_NAME": "RecalModal", "RECAL_MODAL_REF_NAME": "RecalModal",
"baseServiceLineItems": Array [], "baseServiceLineItems": Array [],
"isPageLoading": true,
"widget": Object { "widget": Object {
"disclaimerText": "DisclaimerWidget", "disclaimerText": "DisclaimerWidget",
"explanatoryText": "ExplanatoryTextWidget", "explanatoryText": "ExplanatoryTextWidget",

View file

@ -0,0 +1,116 @@
<template>
<div class="afterpay-modal-banner" role="alert">
<component
src="https://js.squarecdn.com/square-marketplace.js"
async
:is="'script'">
</component>
<div class="afterpay-content">
<span class="afterpay-copy" v-html="afterpayCopy"></span>
<span class="afterpay-copy">{{ afterpayPaymentAmount }}</span>
<span class="afterpay-copy" v-html="afterpayCopyTwo"></span>
<img :src="imageUrl" class="image" alt="Afterpay logo"/>
<span class="afterpay-copy">. &nbsp;</span>
<a
id="afterpay-learnmore"
href="#"
data-afterpay-modal="en_US"
data-bind="click:afterpayLearnMore"
class="afterpay-learn-more"
@click.prevent>
<span>Learn more</span>
</a>
</div>
</div>
</template>
<script>
import { setupModalLinks } from "@/helpers/cms-content-helper";
import { formatAmountInDollars } from '@/helpers/text-helper.js';
import widgetFields from '@/constants/cms-widget-fields.js';
import { useMainStore } from '@/store/index.js';
export default {
name: "afterpay-banner",
components: {
},
props: {
cmsWidgetName: String,
totalServicePrice: Number
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
return {};
},
computed: {
imageUrl() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.IMAGE);
},
afterpayCopy() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
afterpayCopyTwo() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.SUBHEADER_TEXT);
},
deductibleValue() {
return this.mainStore.currentDeductible;
},
afterpayPaymentAmount() {
return this.calculateAfterpayPaymentAmount;
},
isDeductibleScenario() {
return this.mainStore.isVerified && this.mainStore.isDeductible;
},
calculateAfterpayPaymentAmount() {
const totalAmount = this.isDeductibleScenario ? this.deductibleValue : this.totalServicePrice;
const afterpayAmount = (totalAmount / 4).toFixed(2);
return formatAmountInDollars(afterpayAmount);
}
}
};
</script>
<style lang="scss" scoped>
.afterpay-modal-banner {
background-color: $blue-100;
justify-content: center;
align-items: center;
padding: 0.5rem 1rem 0.5rem 1rem;
border-radius: 0.25rem;
margin-top: 20px;
}
.afterpay-content {
display: block;
text-align: center;
}
.afterpay-copy {
font-size: 0.875rem;
color: $blue-700;
font-weight: $font-weight-bold;
line-height: 1.5rem;
}
.image-and-link {
justify-content: center;
align-items: center;
}
.image {
border: 0;
height: 20px;
width: 83px;
}
:deep(a.afterpay-learn-more) {
color: $blue-700;
fill: $blue-700;
text-decoration: underline;
font-size: 0.875rem;
}
</style>

View file

@ -63,7 +63,8 @@ const loadingModalStub = {
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) { function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
router: { router: {
navigate: jest.fn() navigate: jest.fn(),
navigateBailout: jest.fn()
} }
}); });
@ -134,7 +135,10 @@ describe('coverageStatement.vue', () => {
}); });
test('Should render site subheader', () => { test('Should render site subheader', () => {
// Arrange // Arrange
const { wrapper } = getMountedComponent({}); const initialDataForTest = {
isPageLoading: false
};
const { wrapper } = getMountedComponent({}, initialDataForTest);
// Act // Act
const subheader = wrapper.find({ ref: 'siteSubHeader' }); const subheader = wrapper.find({ ref: 'siteSubHeader' });
@ -145,7 +149,10 @@ describe('coverageStatement.vue', () => {
test('Should render explanatory text', () => { test('Should render explanatory text', () => {
// Arrange // Arrange
const { wrapper } = getMountedComponent({}); const initialDataForTest = {
isPageLoading: false
};
const { wrapper } = getMountedComponent({}, initialDataForTest);
// Act // Act
const explanatoryText = wrapper.find({ ref: 'explanatoryText' }); const explanatoryText = wrapper.find({ ref: 'explanatoryText' });
@ -155,7 +162,10 @@ describe('coverageStatement.vue', () => {
}); });
test('Should render secondary text', () => { test('Should render secondary text', () => {
// Arrange // Arrange
const { wrapper } = getMountedComponent({}); const initialDataForTest = {
isPageLoading: false
};
const { wrapper } = getMountedComponent({}, initialDataForTest);
// Act // Act
const secondaryText = wrapper.find({ ref: 'secondaryText' }); const secondaryText = wrapper.find({ ref: 'secondaryText' });
@ -430,6 +440,109 @@ describe('coverageStatement.vue', () => {
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
}); });
describe('displayAfterpayBanner', () => {
test('returns true if ITAC', () => {
// Arrange
const mainInitialState = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.ITAC
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
// Act
const result = wrapper.vm.displayAfterpayBanner;
// Assert
expect(result).toBeTruthy();
});
test('returns true if No Comp', () => {
// Arrange
const mainInitialState = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.NO_COMP
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
// Act
const result = wrapper.vm.displayAfterpayBanner;
// Assert
expect(result).toBeTruthy();
});
test('returns true if deductible is > $0', () => {
// Arrange
const mainInitialState = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
damage: {
isRepair: false
},
currentDeductible: {
replace: 500
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
// Act
const result = wrapper.vm.displayAfterpayBanner;
// Assert
expect(result).toBeTruthy();
});
test('returns false if deductible is <= $0', () => {
// Arrange
const mainInitialState = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.VERIFIED,
coverageType: coverageType.Deductible
},
damage: {
isRepair: false
},
currentDeductible: {
replace: 0
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
// Act
const result = wrapper.vm.displayAfterpayBanner;
// Assert
expect(result).toBeFalsy();
});
test('returns false if Unverified', () => {
// Arrange
const mainInitialState = {
order: {
insuranceCoverage: {
coverageStatus: coverageStatuses.UNVERIFIED,
}
}
};
const { wrapper } = getMountedComponent(mainInitialState);
// Act
const result = wrapper.vm.displayAfterpayBanner;
// Assert
expect(result).toBeFalsy();
});
});
}); });
describe('methods', () => { describe('methods', () => {
describe('arePagePrerequisitesValid', () => { describe('arePagePrerequisitesValid', () => {
@ -578,7 +691,7 @@ describe('coverageStatement.vue', () => {
undefined undefined
); );
}); });
test('If Verified ITAC, selected Cancel, navigate forward w/ CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP', () => { test('If Verified ITAC, selected Cancel, navigateBailout', () => {
// Arrange // Arrange
const deductible = servicePrice + 1; const deductible = servicePrice + 1;
const mainInitialState = { const mainInitialState = {
@ -604,11 +717,7 @@ describe('coverageStatement.vue', () => {
wrapper.vm.cancelClaim(); wrapper.vm.cancelClaim();
// Assert // Assert
expect(wrapper.vm.$router.navigate) expect(wrapper.vm.$router.navigateBailout).toHaveBeenCalled();
.toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,
undefined
);
}); });
test('If No Comp and selected Safelite, navigate forward with CLICKED_FORWARD_WITH_SAFELITE scenario', () => { test('If No Comp and selected Safelite, navigate forward with CLICKED_FORWARD_WITH_SAFELITE scenario', () => {
// Arrange // Arrange
@ -641,7 +750,7 @@ describe('coverageStatement.vue', () => {
undefined undefined
); );
}); });
test('If No comp and selected other shop, navigate forward with CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP ', () => { test('If No comp and selected other shop, navigateBailout ', () => {
// Arrange // Arrange
const mainInitialState = { const mainInitialState = {
order: { order: {
@ -666,11 +775,7 @@ describe('coverageStatement.vue', () => {
wrapper.vm.cancelClaim(); wrapper.vm.cancelClaim();
// Assert // Assert
expect(wrapper.vm.$router.navigate) expect(wrapper.vm.$router.navigateBailout).toHaveBeenCalled();
.toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,
undefined
);
}); });
}); });
describe('openCancelClaimModal', () => { describe('openCancelClaimModal', () => {

View file

@ -10,7 +10,8 @@
cmsWidgetName="SiteHeaderWidget" /> cmsWidgetName="SiteHeaderWidget" />
</div> </div>
<div class="iss-heritage-container-width"> <div class="iss-heritage-container-width">
<div class="coverage-statement-container iss-heritage-content-container-width"> <div v-if="!isPageLoading"
class="coverage-statement-container iss-heritage-content-container-width">
<h5 <h5
ref="siteSubHeader" ref="siteSubHeader"
class="sub-header text-black" class="sub-header text-black"
@ -69,6 +70,10 @@
class="d-flex justify-content-center cost mb-0 cost-underline"> class="d-flex justify-content-center cost mb-0 cost-underline">
{{ formatAmountInDollars(totalServicePrice) }} {{ formatAmountInDollars(totalServicePrice) }}
</div> </div>
<afterpayModalBanner
v-if="displayAfterpayBanner"
cmsWidgetName="AfterpayModalWidget"
:totalServicePrice="totalServicePrice" />
<buttonMain <buttonMain
ref="buttonMain" ref="buttonMain"
class="full-width-button mt-5 mb-5" class="full-width-button mt-5 mb-5"
@ -117,6 +122,7 @@ 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 buttonMain from '@/ux-components/button-main/button-main.vue'; import buttonMain from '@/ux-components/button-main/button-main.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import afterpayModalBanner from '@/layouts/coverage-statement/afterpay-modal-banner/afterpay-modal-banner.vue';
// Import Supporting Files // Import Supporting Files
import { import {
fetchCmsContentForPage, fetchCmsContentForPage,
@ -151,7 +157,8 @@ export default {
textBlock, textBlock,
textLink, textLink,
siteFooter, siteFooter,
cancelClaimModal cancelClaimModal,
afterpayModalBanner
}, },
mixins: [baseFormMixin, vehicleQuestionsMixin], mixins: [baseFormMixin, vehicleQuestionsMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -206,6 +213,7 @@ export default {
data() { data() {
return { return {
baseServiceLineItems: [], baseServiceLineItems: [],
isPageLoading: true,
widget: { widget: {
disclaimerText: 'DisclaimerWidget', disclaimerText: 'DisclaimerWidget',
subheader: 'SiteSubHeaderWidget', subheader: 'SiteSubHeaderWidget',
@ -337,6 +345,11 @@ export default {
return 'centered-back-button'; return 'centered-back-button';
} }
return ''; return '';
},
displayAfterpayBanner() {
return this.isITACQuoteVisible
|| this.isNoCompQuoteVisible
|| (this.showDeductibleOnly && this.deductibleValue > 0);
} }
}, },
watch: { watch: {
@ -347,6 +360,9 @@ export default {
} }
}, },
mounted() { mounted() {
if (this.isPageLoading) {
showIssLoadingModal(true);
}
setupModalLinks(this); setupModalLinks(this);
}, },
methods: { methods: {
@ -370,6 +386,7 @@ export default {
} }
showIssLoadingModal(false); showIssLoadingModal(false);
this.isPageLoading = false;
}, },
async getPricedParts() { async getPricedParts() {
const { glassParts, supportingItems } = this.mainStore.order.lineItems; const { glassParts, supportingItems } = this.mainStore.order.lineItems;
@ -380,18 +397,7 @@ export default {
// We only call the ITAC pricing endpoint if we are not repair or we are NoComp // We only call the ITAC pricing endpoint if we are not repair or we are NoComp
if (!this.isRepair || this.mainStore.isNoComp) { if (!this.isRepair || this.mainStore.isNoComp) {
const pricingResults = await useMainStore().getITACPriceOrderItems(availableLineItems) const pricingResults = await useMainStore().getITACPriceOrderItems(availableLineItems);
.catch((err) => {
useMainStore().setBailout(bailoutMessage.pricingResponseError(
availableLineItems.map((li) => li.partNumber),
{
code: err.code,
message: err.message,
data: err.data
}
));
this.navigateWithScenario(navigationScenarios.PRICING_LOOKUP_ERROR);
});
this.setBaseServiceLineItems(pricingResults); this.setBaseServiceLineItems(pricingResults);
} }
}, },
@ -402,8 +408,7 @@ export default {
this.mainStore.updateIsSafeliteProvider(true); this.mainStore.updateIsSafeliteProvider(true);
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE); this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE);
} else { } else {
this.mainStore.setBailout(bailoutMessage.coverageStatementInvalidState()); this.$router.navigateBailout(bailoutMessage.coverageStatementInvalidState());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_INVALID_STATE);
} }
}, },
navigateWithScenario(scenario) { navigateWithScenario(scenario) {
@ -457,8 +462,7 @@ export default {
}, },
cancelClaim() { cancelClaim() {
this.mainStore.updateIsSafeliteProvider(false); this.mainStore.updateIsSafeliteProvider(false);
this.mainStore.setBailout(bailoutMessage.RequestCallback()); this.$router.navigateBailout(bailoutMessage.RequestCallback());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP);
} }
} }
}; };

View file

@ -357,11 +357,15 @@ describe('duplicateCheck.vue', () => {
useMainStore().loadSession = jest.fn().mockImplementation(() => Promise.reject(error)); useMainStore().loadSession = jest.fn().mockImplementation(() => Promise.reject(error));
// Act // Act
await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect.assertions(2);
try {
await wrapper.vm.forwardButtonAction();
} catch (e) {
expect(e).toMatch(error);
}
expect(wrapper.vm.mainStore.loadSession).toHaveBeenCalledTimes(1); expect(wrapper.vm.mainStore.loadSession).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
}); });
test('coverageType deductible and policy vehicles returned => CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES', async () => { test('coverageType deductible and policy vehicles returned => CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES', async () => {
// Arrange // Arrange

View file

@ -140,18 +140,14 @@ export default {
return; return;
} }
try { const selectedReferral =
const selectedReferral = this.mainStore.applicationUser.duplicateOrders.find((o) => o.correlationId === this.selectedAnswer);
this.mainStore.applicationUser.duplicateOrders.find((o) => o.correlationId === this.selectedAnswer);
if (selectedReferral) { if (selectedReferral) {
await this.mainStore.loadSession(selectedReferral); await this.mainStore.loadSession(selectedReferral);
}
} catch (err) {
console.error(`Error on loading session from duplicate check ${err}`);
} finally {
this.navigateForward();
} }
this.navigateForward();
}, },
navigateForward() { navigateForward() {
this.mainStore.updateDuplicateCheckVisited(true); this.mainStore.updateDuplicateCheckVisited(true);

View file

@ -34,44 +34,51 @@ export default {
computed: { computed: {
}, },
async mounted() { async mounted() {
const queryStringParams = this.parseQueryParms();
const { isAuthorized, clientData, decryptedParams } = await this.validateClientTagOnEntry(queryStringParams);
this.unauthorized = !isAuthorized;
if (!isAuthorized) {
// Remove the loading animation if client tag validation fails so users can see the Unauthorized Access message.
showIssLoadingModal(false);
return;
}
this.populateISSConfigValues(clientData);
try { try {
// Check cookie const queryStringParams = this.parseQueryParms();
const issCookie = getISSCookie();
if (issCookie !== null && issCookie.VehicleMake && issCookie.VehicleModel) {
const clientParentAccountNumber = clientData.parentAccountNumber;
const cookieParentAccountNumber = issCookie.ReferralParentAccountNumber;
if (clientParentAccountNumber === cookieParentAccountNumber) { const { isAuthorized, clientData, decryptedParams } = await this.validateClientTagOnEntry(queryStringParams);
const savedSessionTimeStamp = new Date(issCookie.SavedSessionTimeoutDate);
const isSavedSessionTimedOut = new Date(new Date().toUTCString()) > savedSessionTimeStamp;
if (!isSavedSessionTimedOut) { this.unauthorized = !isAuthorized;
this.mainStore.issConfig.enableContinueFromCookie = true; if (!isAuthorized) {
// Remove the loading animation if client tag validation fails so users can see the Unauthorized Access message.
showIssLoadingModal(false);
return;
}
this.populateISSConfigValues(clientData);
try {
// Check cookie
const issCookie = getISSCookie();
if (issCookie !== null && issCookie.VehicleMake && issCookie.VehicleModel) {
const clientParentAccountNumber = clientData.parentAccountNumber;
const cookieParentAccountNumber = issCookie.ReferralParentAccountNumber;
if (clientParentAccountNumber === cookieParentAccountNumber) {
const savedSessionTimeStamp = new Date(issCookie.SavedSessionTimeoutDate);
const isSavedSessionTimedOut = new Date(new Date().toUTCString()) > savedSessionTimeStamp;
if (!isSavedSessionTimedOut) {
this.mainStore.issConfig.enableContinueFromCookie = true;
}
} }
} else {
updateOrCreateISSCookie(true);
} }
} else { } catch {
updateOrCreateISSCookie(true); updateOrCreateISSCookie(true);
} }
} catch {
updateOrCreateISSCookie(true);
}
if (clientData.parameters?.length > 0) { if (clientData.parameters?.length > 0) {
const finalParams = this.combineClientParameters(clientData.parameters, { ...queryStringParams, ...decryptedParams }); const finalParams = this.combineClientParameters(clientData.parameters, { ...queryStringParams, ...decryptedParams });
this.populateStoreItemsFromParams(finalParams); this.populateStoreItemsFromParams(finalParams);
}
} catch (e) {
global.$logger.logError('[Entry Page] Client Setup Error:', e);
this.unauthorized = true;
showIssLoadingModal(false);
return;
} }
this.mainStore.applicationUser.coverageAttempts = 0; this.mainStore.applicationUser.coverageAttempts = 0;
@ -151,37 +158,33 @@ export default {
this.mainStore.issConfig.isCoverageEnabled = data.coverageEnabled; this.mainStore.issConfig.isCoverageEnabled = data.coverageEnabled;
this.mainStore.issConfig.siteType = data.siteType; this.mainStore.issConfig.siteType = data.siteType;
try { if (data.clientFlags) {
if (data.clientFlags) { const clientFlags = JSON.parse(data.clientFlags);
const clientFlags = JSON.parse(data.clientFlags);
if (clientFlags.TPAEnabled) { if (clientFlags.TPAEnabled) {
this.mainStore.issConfig.enableTPAFlow = true; this.mainStore.issConfig.enableTPAFlow = true;
} }
if (clientFlags.ClientFullName != null) { if (clientFlags.ClientFullName != null) {
this.mainStore.issConfig.clientFullName = clientFlags.ClientFullName; this.mainStore.issConfig.clientFullName = clientFlags.ClientFullName;
} }
if (clientFlags.ClientDisplayName != null) { if (clientFlags.ClientDisplayName != null) {
this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName; this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName;
this.mainStore.issConfig.clientPossessiveName = toPossessive(clientFlags.ClientDisplayName); this.mainStore.issConfig.clientPossessiveName = toPossessive(clientFlags.ClientDisplayName);
} }
if (clientFlags.ClientPossessiveName != null) { if (clientFlags.ClientPossessiveName != null) {
this.mainStore.issConfig.clientPossessiveName = clientFlags.ClientPossessiveName; this.mainStore.issConfig.clientPossessiveName = clientFlags.ClientPossessiveName;
} }
if (clientFlags.ClaimRegistrationRequired) { if (clientFlags.ClaimRegistrationRequired) {
this.mainStore.issConfig.isClaimRegistrationRequired = true; this.mainStore.issConfig.isClaimRegistrationRequired = true;
} }
if (clientFlags.EnableNoCompQuote) { if (clientFlags.EnableNoCompQuote) {
this.mainStore.issConfig.enableNoCompQuote = true; this.mainStore.issConfig.enableNoCompQuote = true;
}
} }
} catch (e) {
console.error(`Error parsing client flags: ${e}`);
} }
}, },
combineClientParameters(configParams, queryStringParams) { combineClientParameters(configParams, queryStringParams) {

View file

@ -10,8 +10,6 @@
v-model="selectedAnswers" v-model="selectedAnswers"
isRequired isRequired
:isMetaValid="meta.valid" :isMetaValid="meta.valid"
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData" :questionsData="questionsData"
:validationRules="rules.optionRequired" :validationRules="rules.optionRequired"
:index="currentGlassIndex" :index="currentGlassIndex"
@ -71,18 +69,6 @@ export default {
}; };
}, },
computed: { computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent(
'AdditionalPartsQuestionsAlert',
'HeadlineText'
);
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent(
'AdditionalPartsQuestionsAlert',
'BodyText'
);
},
partsOrQuestionsData() { partsOrQuestionsData() {
return useMainStore().pageData(issPageValues.MOLDING_QUESTIONS).partsOrQuestions; return useMainStore().pageData(issPageValues.MOLDING_QUESTIONS).partsOrQuestions;
} }
@ -162,8 +148,7 @@ export default {
this.navigateForward(partsOrQuestions, null); this.navigateForward(partsOrQuestions, null);
}, },
requestCallbackBailout() { requestCallbackBailout() {
this.mainStore.setBailout(bailoutMessage.RequestCallback()); this.$router.navigateBailout(bailoutMessage.RequestCallback());
this.$router.navigate(navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT, this.$route);
} }
} }
}; };

View file

@ -94,6 +94,7 @@
</div> </div>
<cartDropdown <cartDropdown
v-if="showCart" v-if="showCart"
class="cart-dropdown"
:showAsPaid="payment.isPayInAdvance" :showAsPaid="payment.isPayInAdvance"
:readOnly="true" :readOnly="true"
recyclingModalCmsWidgetName="RecycleModal" recyclingModalCmsWidgetName="RecycleModal"
@ -230,7 +231,7 @@ export default {
address: this.appointmentLocation, address: this.appointmentLocation,
inShopDuration: this.inShopAppointmentDuration, inShopDuration: this.inShopAppointmentDuration,
email: this.customerEmail, email: this.customerEmail,
phone: this.contactInfo.servicePhone, phone: this.contactPhone,
workOrderNumber: this.workOrderNumber, workOrderNumber: this.workOrderNumber,
CUSTOMER_PORTAL_URL: applicationConfig.CUSTOMER_PORTAL_URL, CUSTOMER_PORTAL_URL: applicationConfig.CUSTOMER_PORTAL_URL,
CUSTOMER_PORTAL_LOGIN_TOKEN: this.customerPortalLoginToken CUSTOMER_PORTAL_LOGIN_TOKEN: this.customerPortalLoginToken
@ -492,6 +493,13 @@ export default {
widgetFields.TEXT_BLOCK_WIDGET.TEXT widgetFields.TEXT_BLOCK_WIDGET.TEXT
); );
} }
},
contactPhone() {
let phoneNumber = this.contactInfo.servicePhone;
if (this.contactInfo.extension) {
phoneNumber += `-${this.contactInfo.extension}`;
}
return phoneNumber;
} }
}, },
mounted() { mounted() {
@ -718,5 +726,10 @@ export default {
color: $black; color: $black;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
} }
.cart-dropdown:deep(.deductible-value) {
font-size: 1rem;
font-weight: $font-weight-bold;
}
} }
</style> </style>

View file

@ -148,8 +148,7 @@ export default {
this.navigateForward(glassPartsForStore, null); this.navigateForward(glassPartsForStore, null);
}, },
requestCallbackBailout() { requestCallbackBailout() {
this.mainStore.setBailout(bailoutMessage.RequestCallback()); this.$router.navigateBailout(bailoutMessage.RequestCallback());
this.$router.navigate(navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT, this.$route);
} }
}, },
}; };

View file

@ -2,32 +2,30 @@
<baseInputButton <baseInputButton
v-bind="$props" v-bind="$props"
v-model="selectedValue" v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"> buttonWrapperClasses="list-group base-input-button list-button payment-method d-flex flex-column w-100 no-hover">
<div class="button-content list-button-content d-flex flex-row py-3 px-4"> <div class="button-content list-button-content d-flex flex-row">
<div>
<span
v-for="token in tokens"
:key="token">
<span v-if="isInlineImageToken(token)">
<img
v-if="hasImage"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}
</span>
</span>
</div>
<img <img
v-if="shouldDisplaySideImage" v-if="shouldDisplaySideImage"
:id="buttonImageId" :id="buttonImageId"
class="ms-auto order-3" class="ms-auto"
:src="buttonImage" :src="buttonImage"
:alt="altText" /> :alt="altText" />
<div class="order-2">
<p class="m-0">
<span
v-for="token in tokens"
:key="token">
<span v-if="isInlineImageToken(token)">
<img
v-if="hasImage"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}
</span>
</span>
</p>
</div>
</div> </div>
</baseInputButton> </baseInputButton>
</template> </template>
@ -74,52 +72,41 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.list-button { .list-button.payment-method.list-group {
outline: none; outline: none;
input[type="radio"], input[type="radio"],
input[type="checkbox"] { 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 { &:checked + .list-button-content {
background: $background-color-selected;
border-color: $heritage-blue-primary;
font-weight: 600;
color: $black; 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; margin-bottom: .75rem;
} border: none;
&:checked + .list-button-content p,
&:checked + .list-button-content span { &.has-error {
font-weight: 500; input[type="radio"],
} input[type="checkbox"] {
&:checked + .list-button-content span:nth-child(2) { &:focus + .list-button-content {
font-weight: 400; box-shadow: none;
color: $gray-600; }
} }
} }
} }
.list-button-content { .list-button-content {
color: $gray-600; color: $darker-gray;
font-weight: 400;
position: relative; position: relative;
background: $white; background: $white;
transition: all 150ms linear; transition: all 150ms linear;
border-radius: $border-radius-lg; border-radius: 3.75rem;
border: 1px solid $gray-500; border: 1px solid #CACBCC;
width: 100%; width: 100%;
outline: none; outline: none;
padding: .625rem 2rem;
span { height: 2.875rem;
&.small {
font-size: $font-size-xsm;
color: $gray-550;
}
}
} }
</style> </style>

View file

@ -19,6 +19,7 @@
import buttonQuestion from '@/digital-components/button-question/button-question.vue'; import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import widgetFields from '@/constants/cms-widget-fields.js'; import widgetFields from '@/constants/cms-widget-fields.js';
import paymentMethodListButton from './payment-method-list-button/payment-method-list-button.vue'; import paymentMethodListButton from './payment-method-list-button/payment-method-list-button.vue';
import { markRaw } from 'vue';
export default { export default {
name: 'payment-method-question', name: 'payment-method-question',
@ -33,7 +34,7 @@ export default {
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() { data() {
return { return {
paymentMethodListButton paymentMethodListButton: markRaw(paymentMethodListButton)
}; };
}, },
computed: { computed: {

View file

@ -48,15 +48,19 @@ function setupMocks({ customMountOptions = {}, queryString }, mainInitialState =
}) })
} }
}; };
const cmsMixin = {
methods: {
getCmsContent: jest.fn()
}
}
mountOptions.global.plugins = [testingPinia]; mountOptions.global.plugins = [testingPinia];
mountOptions.global.mixins = [mockMixin]; mountOptions.mixins = [mockMixin, cmsMixin];
const wrapper = shallowMount(paymentMethod, mountOptions); const wrapper = shallowMount(paymentMethod, mountOptions);
return wrapper; return wrapper;
} }
beforeEach(() => { beforeEach(() => {
const store = useMainStore(); const store = useMainStore();
const defaultState = getDefaultState(); const defaultState = getDefaultState();

View file

@ -10,23 +10,32 @@
ref="siteHeader" ref="siteHeader"
cmsWidgetName="SiteHeaderWidget" /> cmsWidgetName="SiteHeaderWidget" />
</div> </div>
<div class="iss-heritage-container-width"> <div class="iss-heritage-container-width payment-method-container row">
<div class="payment-method-container iss-heritage-content-container-width"> <div class="order-info-container col-md-7">
<alert
class=""
alertClass="alert-warning"
cmsWidgetName="AlertIncompleteWidget" />
<siteSubHeader <siteSubHeader
class="mb-5 mt-4 px-3" class="subheader"
cmsWidgetName="SiteSubHeaderWidget" cmsWidgetName="SiteSubHeaderWidget" />
secondaryTextClasses="text-center"
subHeaderClasses="mt-4" />
<div class="main-content-container"> <div class="main-content-container">
<hr class="my-0" /> <hr class="section-divider" />
<reviewDropdown ref="reviewDropdown" /> <reviewDropdown
<hr class="my-0" /> ref="reviewDropdown"
<cartDropdown @edit-clicked="handleEditClicked"
:showAsPaid="false" />
:readOnly="false" <div class="cart-header">
recyclingModalCmsWidgetName="RecycleModal" <span class="cart-label">{{ cartHeaderText }}</span>
servicePackageTitleWidgetName="ServicePackageTitle" /> <span v-if="showCartTotal" class="cart-value">{{ cartTotal }}</span>
<hr class="mt-0 mb-5" /> </div>
<div class="cart-dropdown-container">
<cartDropdown
:showAsPaid="false"
:readOnly="false"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle" />
</div>
<alert <alert
v-if="displayPayInAdvanceAlert" v-if="displayPayInAdvanceAlert"
name="payInAdvanceErrorAlert" name="payInAdvanceErrorAlert"
@ -34,27 +43,79 @@
cmsWidgetName="PayInAdvanceErrorAlertWidget" cmsWidgetName="PayInAdvanceErrorAlertWidget"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" /> :isDismissible="false" />
<hr
v-if="!isPayInAdvanceDisabled"
class="pia-divider" />
<paymentMethodQuestion <paymentMethodQuestion
v-if="!isPayInAdvanceDisabled" v-if="!isPayInAdvanceDisabled"
v-model="paymentMethod" v-model="paymentMethod"
:cmsWidgetName="paymentMethodWidgetName" :cmsWidgetName="paymentMethodWidgetName"
:validationRules="rules.optionRequired" /> :validationRules="rules.optionRequired" />
<alert <div
v-if="isPayInAdvanceDisabled" v-if="!hideSMSOptIn"
:isDismissible="false" class="sms-opt-in-section">
alertClass="alert-info" <hr class="sms-divider" />
cmsWidgetName="NoPayInAdvanceDisclaimerWidget" <div class="sms-opt-in-header">{{ smsOptInHeaderText }}</div>
:shouldScrollToOnMount="false" /> <buttonQuestion
class="sms-opt-in-question"
v-model="smsOptIn"
groupName="sms-opt-in"
buttonTypeString="listButtonHorizontal"
:questionText="smsOptinQuestionText"
:answers="smsOptinQuestionAnswers"
isRequired
:validationRules="rules.optionRequired" />
<textboxQuestion
v-if="smsOptIn === 'Yes'"
v-model="smsPhoneNumber"
inputId="phoneNumber"
:cmsWidgetName="widget.smsPhoneNumber"
isRequired
:mask="phoneMask"
placeholderText="###-###-####"
disableAutoFill
:validationRules="rules.phoneNumber" />
</div>
<siteFooter <siteFooter
ref="siteFooter" ref="siteFooter"
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
:isStackedVertically="true"
@backClicked="backButtonAction" @backClicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
<div
v-if="!hideSMSOptIn"
class="disclaimer">
<textBlock :cmsWidgetName="widget.smsDisclaimer" />
</div>
</div>
</div>
<div class="offer-container col-lg-4 offset-lg-1 col-md-5 d-none d-md-flex">
<div
v-if="offerWipers"
class="wiper-offer-panel">
<div class="wiper-logo">
<img
:src="wiperOfferPanel.image"
alt="Wiper Offer Logo" />
</div>
<div class="panel-body">
<div class="wiper-header">{{ wiperOfferPanel.header }}</div>
<div class="wiper-body">{{ wiperOfferPanel.body }}</div>
</div>
<div class="panel-footer">
<buttonMain
class="edit-wiper-offer-button"
:buttonText="wiperOfferPanel.edit"
@click="handleEditClicked('wipers')" />
</div>
</div> </div>
</div> </div>
</div> </div>
<contactDetailsDrawer
ref="contactDetailsDrawer"
class="contact-details-drawer"
@updateContactDetails="refreshContactDetails"
@closeWithoutSaving="closeContactDetailsWithoutSaving" />
</div> </div>
</Form> </Form>
</template> </template>
@ -69,6 +130,11 @@ import reviewDropdown from '@/layouts/payment-method/review-dropdown/review-drop
import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue'; import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue';
import paymentMethodQuestion from '@/layouts/payment-method/payment-method-question/payment-method-question.vue'; import paymentMethodQuestion from '@/layouts/payment-method/payment-method-question/payment-method-question.vue';
import alert from '@/ux-components/alert/alert.vue'; import alert from '@/ux-components/alert/alert.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import buttonQuestion from '@/digital-components/button-question/button-question.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
import contactDetailsDrawer from '@/iss-components/contact-details-drawer/contact-details-drawer.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
// Supporting Items // Supporting Items
import settleAllPromises from '@/helpers/layout-helper'; import settleAllPromises from '@/helpers/layout-helper';
@ -86,6 +152,10 @@ import { experimentSettings } from '@/constants/experiments';
import submitType from '@/constants/submit-type'; import submitType from '@/constants/submit-type';
import routerParams from '@/router/router-constants/router-params'; import routerParams from '@/router/router-constants/router-params';
import { supportsApplePay } from '@/helpers/browser-helper'; import { supportsApplePay } from '@/helpers/browser-helper';
import { getCartTotal } from '@/helpers/cart-helper';
import { formatAmountInDollars } from '@/helpers/text-helper';
import widgetFields from '@/constants/cms-widget-fields';
import MaskaFormattedMasks from '@/constants/maska-masks';
export default { export default {
name: 'payment-method', name: 'payment-method',
@ -97,7 +167,12 @@ export default {
reviewDropdown, reviewDropdown,
cartDropdown, cartDropdown,
paymentMethodQuestion, paymentMethodQuestion,
alert alert,
textBlock,
buttonQuestion,
textboxQuestion,
contactDetailsDrawer,
buttonMain
}, },
mixins: [baseFormMixin], mixins: [baseFormMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -126,20 +201,29 @@ export default {
paymentMethod: null, paymentMethod: null,
widget: { widget: {
paymentMethod: 'PaymentMethodWidget', paymentMethod: 'PaymentMethodWidget',
paymentMethodApplePay: 'PaymentMethodWidgetApplePay' paymentMethodApplePay: 'PaymentMethodWidgetApplePay',
amountDue: 'AmountDueCartHeaderTextWidget',
payAtAppointment: 'PayAtAppointmentTextWidget',
smsOptInHeader: 'SMSOptInHeaderWidget',
smsOptInQuestion: 'SMSOptInQuestionWidget',
smsPhoneNumber: 'SMSPhoneNumberWidget',
smsDisclaimer: 'SMSDisclaimerWidget',
wiperOffer: 'WiperOfferPanelWidget'
}, },
rules: { rules: {
optionRequired: globalRules.OPTION_REQUIRED optionRequired: globalRules.OPTION_REQUIRED,
} phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}`
},
smsOptIn: '',
smsPhoneNumber: ''
}; };
}, },
computed: { computed: {
customCallToActionButtonCopy() { customCallToActionButtonCopy() {
switch (this.paymentMethod) { switch (this.paymentMethod) {
case paymentMethods.PayNow: case paymentMethods.PayNow:
return 'Continue to checkout';
case paymentMethods.AFTERPAY: case paymentMethods.AFTERPAY:
return 'Continue to Afterpay'; return 'Continue to checkout';
default: default:
return null; return null;
} }
@ -156,6 +240,60 @@ export default {
}, },
paymentMethodWidgetName() { paymentMethodWidgetName() {
return supportsApplePay() ? this.widget.paymentMethodApplePay : this.widget.paymentMethod; return supportsApplePay() ? this.widget.paymentMethodApplePay : this.widget.paymentMethod;
},
showCartTotal() {
return this.mainStore.isVerified && (this.mainStore.isITAC || this.mainStore.isNoComp
|| this.mainStore.order.lineItems?.vaps?.length > 0);
},
cartHeaderText() {
if (this.isPayInAdvanceDisabled) {
return this.getCmsContent(
this.widget.payAtAppointment,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
} else {
return this.getCmsContent(
this.widget.amountDue,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
}
},
cartTotal() {
return formatAmountInDollars(getCartTotal(this.mainStore.order));
},
hideSMSOptIn() {
return false;
},
smsOptInHeaderText() {
return this.getCmsContent(
this.widget.smsOptInHeader,
widgetFields.TEXT_BLOCK_WIDGET.TEXT
);
},
smsOptinQuestionText() {
return this.getCmsContent(this.widget.smsOptInQuestion, widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT);
},
smsOptinQuestionAnswers() {
return this.getCmsContent(this.widget.smsOptInQuestion, widgetFields.INPUT_QUESTION_WIDGET.ANSWERS) || [];
},
phoneMask() {
return MaskaFormattedMasks.PHONE_NUMBER;
},
offerWipers() {
return !this.mainStore.lineItems.vaps?.some((part) => part.partType.toLowerCase().includes('wiper'));
},
wiperOfferPanel() {
const wiperImage = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.IMAGE);
const wiperHeader = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
const wiperBody = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
const wiperEdit = this.getCmsContent(this.widget.wiperOffer, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT);
return {
image: wiperImage,
header: wiperHeader,
body: wiperBody,
edit: wiperEdit
};
} }
}, },
watch: { watch: {
@ -245,7 +383,12 @@ export default {
&& contactInfoReqs); && contactInfoReqs);
}, },
initializeComponent() { initializeComponent() {
this.paymentMethod = this.getPaymentMethodFromStore(); const paymentMethodFromStore = this.getPaymentMethodFromStore();
// SMS Opt In defaults to 'no', but we don't actually want to use it if the user hasn't saved data form this page yet
if (paymentMethodFromStore) {
this.smsOptIn = this.getSMSOptInFromStore();
}
this.smsPhoneNumber = this.getSMSPhoneFromStore();
}, },
getPaymentMethodFromStore() { getPaymentMethodFromStore() {
if (this.isPayInAdvanceDisabled) { if (this.isPayInAdvanceDisabled) {
@ -258,27 +401,25 @@ export default {
this.$refs.siteFooter.updateButtonText(newValue); this.$refs.siteFooter.updateButtonText(newValue);
}, },
async forwardButtonAction() { async forwardButtonAction() {
useMainStore().savePaymentMethodChoice(this.paymentMethod); this.mainStore.savePaymentMethodChoice(this.paymentMethod);
if (this.paymentMethod === paymentMethods.PAY_AT_TIME_OF_SERVICE) { if (!this.hideSMSOptIn) {
try { const requestTextUpdates = this.smsOptIn === 'Yes';
await submitWorkOrder({ submitType: submitType.SAFELITE }); this.mainStore.updateContactInfo({ requestTextUpdates });
this.$router.navigate( if (requestTextUpdates) {
this.navigationScenarios.CLICKED_FORWARD, this.mainStore.updatePhoneNumbers({ alternative: this.smsPhoneNumber, service: this.smsPhoneNumber });
this.$route
);
} catch (error) {
useMainStore().setBailout(bailoutMessage.saveSessionError(error.data));
this.$router.navigate(
this.navigationScenarios.SAVE_SESSION_FAILED,
this.$route,
{ issPage: issPageValues.PAYMENT_METHOD }
);
console.error(`error: response from submit work order:${error.message}`);
} }
}
if (this.paymentMethod === paymentMethods.PAY_AT_TIME_OF_SERVICE) {
await submitWorkOrder({ submitType: submitType.SAFELITE });
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
} else { } else {
await saveSession({ await saveSession({
createWorkOrderNumberForPIA: true, createWorkOrderNumberForPIA: true,
shouldAwaitSaveSessionQueue: true shouldAwaitSaveSessionQueue: true,
bailoutOnError: true
}); });
this.$router.navigate( this.$router.navigate(
@ -293,10 +434,56 @@ export default {
const scenario = this.mainStore.isMobileAppointment const scenario = this.mainStore.isMobileAppointment
? this.navigationScenarios.CLICKED_BACK_MOBILE ? this.navigationScenarios.CLICKED_BACK_MOBILE
: this.navigationScenarios.CLICKED_BACK_INSHOP; : this.navigationScenarios.CLICKED_BACK_INSHOP;
this.$router.navigate( this.$router.navigateWithSpinner(
scenario, scenario,
this.$route this.$route
); );
},
getSMSOptInFromStore() {
return this.mainStore.contactInfo.requestTextUpdates ? 'Yes' : 'No';
},
getSMSPhoneFromStore() {
return this.mainStore.contactInfo.alternativePhone ?? this.mainStore.contactInfo.servicePhone;
},
handleEditClicked(section) {
switch (section) {
case 'location':
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_SERVICE_LOCATION,
this.$route
);
break;
case 'schedule':
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_SCHEDULE,
this.$route
);
break;
case 'vehicle':
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_VEHICLE,
this.$route
);
break;
case 'customer':
this.openContactDetailsModal();
break;
case 'wipers':
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_WIPERS,
this.$route
);
break;
}
},
openContactDetailsModal() {
this.$refs.contactDetailsDrawer.openModal();
},
refreshContactDetails() {
this.smsPhoneNumber = this.mainStore.contactInfo.servicePhone;
},
closeContactDetailsWithoutSaving() {
this.smsOptIn = 'No';
} }
} }
}; };
@ -304,20 +491,145 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
$page-side-padding: 1.5rem; $page-side-padding: 1.5rem;
.iss-heritage-container-width { #app .iss-heritage-container-width.payment-method-container {
.payment-method-container { .alert-warning {
position: relative; margin-top: 1.25rem;
min-height: 1px; margin-bottom: 0rem;
padding-left: .9375rem; }
padding-right: .9375rem;
.subheader {
margin-top: 1.25rem;
:deep(strong) {
color: $black;
font-weight: 600;
}
:deep(.subheader-secondary) {
margin-top: .625rem;
p {
margin-bottom: 0;
color: #4d4e53;
}
}
}
.section-divider {
margin-top: 1.5rem;
margin-bottom: 1.875rem;
}
.cart-header {
display: flex;
align-items: center;
gap: .25rem;
margin-bottom: .625rem;
.cart-label {
color: $black;
font-weight: $font-weight-bold;
}
.cart-value {
color: $green;
font-weight: $font-weight-bold;
font-size: 1.625rem;
}
}
.cart-dropdown-container {
margin-bottom: 1.25rem;
}
.pia-divider {
margin-top: .5rem;
margin-bottom: 1.25rem;
}
.sms-divider {
margin-top: 1.75rem;
margin-bottom: 1.875rem;
}
.sms-opt-in-section {
margin-bottom: 1.875rem;
}
.sms-opt-in-header {
color: $black;
font-weight: $font-weight-bold;
margin-bottom: 1.125rem;
}
.sms-opt-in-question {
margin-bottom: 1.25rem;
:deep(.question-text) {
margin-bottom: .625rem;
}
}
.disclaimer {
margin-bottom: 1.875rem;
font-weight: $font-weight-light;
font-size: .8125rem;
line-height: 1.54;
color: #4d4e53;
:deep(a) {
color: $heritage-blue-primary;
text-decoration: none;
font-weight: $font-weight-bold;
&:hover,
&:focus {
color: $heritage-blue-secondary;
text-decoration: underline;
}
}
}
.contact-details-drawer {
:deep(strong) {
color: $black;
font-weight: 600;
}
}
.wiper-offer-panel {
margin-top: 1.25rem;
margin-bottom: 1.375rem;
box-shadow: 0 0 .625rem 1px #cacbcc;
border: 1px solid #ddd;
border-radius: .25rem;
max-height: fit-content;
.wiper-logo {
margin: 1.25rem 2.25rem 0 2.25rem;
img {
max-width: 100%;
height: auto;
}
}
.panel-body {
padding: 0 1rem 1.25rem 1rem;
.wiper-header {
margin: 1.25rem 0 .625rem 0;
color: $black;
font-weight: $font-weight-bold;
}
}
.panel-footer {
padding: 0 1rem 1.25rem 1rem;
.edit-wiper-offer-button {
width: 100%;
}
}
} }
} }
.main-content-container {
padding: 0;
}
.page-container-grouped-styles {
overflow: auto;
}
</style> </style>

View file

@ -1,34 +1,55 @@
<template> <template>
<div class="py-3"> <div class="block-container">
<div class="d-flex"> <div class="d-flex">
<textBlock <textBlock
:cmsWidgetName="headerCmsWidgetName" :cmsWidgetName="headerCmsWidgetName"
:customText="customHeaderText" :customText="customHeaderText"
typeStyle="body small dark" typeStyle="body dark"
fontWeight="bold" fontWeight="bold"
:marginTopSizeOverride="0" /> :marginTopSizeOverride="0" />
</div> </div>
<div <div class="review-block-body-container">
v-for="item in content" <div
:key="item" v-for="item in content"
class="small review-block-content" :key="item"
data-test="contentLine" class="review-block-content"
v-html="item"></div> data-test="contentLine"
v-html="item"></div>
<div
class="link-container"
:class="{ 'expanded': linkVisible }">
<textLink
linkType="text"
:text="editLinkText"
useLoadingModal
href="javascript:void(0)"
class="link"
@clickEvent="linkClicked">
</textLink>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import textBlock from '@/digital-components/text-block/text-block.vue'; import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
export default { export default {
name: 'review-block', name: 'review-block',
components: { components: {
textBlock textBlock,
textLink
}, },
props: { props: {
headerCmsWidgetName: String, headerCmsWidgetName: String,
customHeaderText: String, customHeaderText: String,
content: Array // Specifically an array of strings. content: Array, // Specifically an array of strings.
editLinkText: {
type: String,
default: 'Edit'
},
linkVisible: Boolean
}, },
emits: ['edit-clicked'], emits: ['edit-clicked'],
data() { data() {
@ -51,7 +72,17 @@ export default {
:deep(.review-block-content > ul) { :deep(.review-block-content > ul) {
margin-bottom: 0rem; margin-bottom: 0rem;
} }
.review-block-content { .review-block-body-container {
width: 90%; margin-top: .625rem;
}
.link-container {
max-height: 0;
transition: $transition-swing;
overflow: hidden;
&.expanded {
margin-top: .625rem;
max-height: 2rem;
}
} }
</style> </style>

View file

@ -1,45 +1,39 @@
<template> <template>
<div :class="[isExpanded ? 'pb-3' : 'pb-4']"> <div>
<div <div
class="row review-toggle flex align-items-center pt-4" class="review-toggle d-flex align-items-center justify-content-between"
:class="[isExpanded ? 'expanded' : '']" :class="[isExpanded ? 'expanded' : '']"
@click="toggleIsExpanded"> @click="toggleIsExpanded">
<a <a
aria-label="expand appointment details" aria-label="expand appointment details"
href="javascript:void(0)" href="javascript:void(0)"
class="col d-flex justify-content-between py-0"> class="d-flex py-0">
<span class="label">Appointment details</span> <span class="label">Appointment details</span>
</a> </a>
</div> </div>
<div class="review-table px-4"> <div class="review-table">
<vehicleReview
cmsWidgetName="VehicleReviewWidget"
:vehicle="vehicleInfo" />
<hr class="my-0" />
<damageReview
cmsWidgetName="DamageReviewWidget"
damageLocationsWidgetName="DamageLocationsWidget"
:damage="damageInfo" />
<hr class="my-0" />
<servicePackageReview
ref="servicePackageReview"
servicePackageOptionsCmsName="ServicePackageTitle"
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
vapsItemsCmsName="VapsItemDescriptions"
:damage="damageInfo"
:lineItems="lineItems" />
<hr class="my-0" />
<serviceLocationReview <serviceLocationReview
cmsWidgetName="ServiceLocationTitleWidget" :linkVisible="isExpanded"
:serviceLocation="serviceLocationInfo" /> cmsWidgetName="ServiceLocationWidget"
<hr class="my-0" /> :serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
<scheduleReview <scheduleReview
:linkVisible="isExpanded"
cmsWidgetName="ScheduleWidget" cmsWidgetName="ScheduleWidget"
:appointmentType="appointmentType" /> @edit-clicked="editSchedule" />
<hr class="my-0" /> <hr class="section-divider" />
<customerReview <div class="review-table-expander">
cmsWidgetName="CustomerReviewWidget" <vehicleReview
:customer="customerInfo" /> :linkVisible="true"
cmsWidgetName="VehicleReviewWidget"
@edit-clicked="editVehicle" />
<hr class="section-divider" />
<customerReview
:linkVisible="true"
cmsWidgetName="CustomerReviewWidget"
@edit-clicked="editCustomer" />
<hr class="section-divider" />
</div>
</div> </div>
</div> </div>
</template> </template>
@ -47,26 +41,18 @@
<script> <script>
// import textBlock from '@/digital-components/text-block/text-block.vue'; // import textBlock from '@/digital-components/text-block/text-block.vue';
import customerReview from '@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue'; import customerReview from '@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue';
import damageReview from '@/layouts/payment-method/review-dropdown/review-sections/damage-review/damage-review.vue';
import scheduleReview from '@/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review.vue'; import scheduleReview from '@/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review.vue';
import import serviceLocationReview from '@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue';
serviceLocationReview from '@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue';
import
servicePackageReview
from '@/layouts/payment-method/review-dropdown/review-sections/service-package-review/service-package-review.vue';
import vehicleReview from '@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue'; import vehicleReview from '@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import settleAllPromises from '@/helpers/layout-helper';
export default { export default {
name: 'review-dropdown', name: 'review-dropdown',
components: { components: {
customerReview, customerReview,
damageReview,
scheduleReview, scheduleReview,
serviceLocationReview, serviceLocationReview,
servicePackageReview,
vehicleReview vehicleReview
}, },
props: {}, props: {},
@ -79,12 +65,6 @@ export default {
vehicleInfo() { vehicleInfo() {
return useMainStore().vehicle; return useMainStore().vehicle;
}, },
damageInfo() {
return useMainStore().damage;
},
lineItems() {
return useMainStore().lineItems;
},
serviceLocationInfo() { serviceLocationInfo() {
return useMainStore().order.serviceLocation; return useMainStore().order.serviceLocation;
}, },
@ -95,9 +75,22 @@ export default {
return useMainStore().contactInfo; return useMainStore().contactInfo;
} }
}, },
emits: ['edit-clicked'],
methods: { methods: {
toggleIsExpanded() { toggleIsExpanded() {
this.isExpanded = !this.isExpanded; this.isExpanded = !this.isExpanded;
},
editServiceLocation() {
this.$emit('edit-clicked', 'location');
},
editSchedule() {
this.$emit('edit-clicked', 'schedule');
},
editVehicle() {
this.$emit('edit-clicked', 'vehicle');
},
editCustomer() {
this.$emit('edit-clicked', 'customer');
} }
} }
}; };
@ -111,34 +104,37 @@ export default {
line-height: 1.6; line-height: 1.6;
} }
.review-table { .review-table-expander {
max-height: 0; max-height: 0;
transition: all 350ms ease-in; transition: $transition-swing;
overflow: hidden; overflow: hidden;
visibility: hidden; }
.section-divider {
margin-top: 1.5rem;
margin-bottom: 1.875rem;
} }
.review-toggle { .review-toggle {
margin-bottom: .625rem;
flex-direction: row;
&:after { &:after {
content: ""; content: "";
transition: all 0.5s ease; transition: transform none;
background-image: url($svg-payment-method-review-toggle); background-image: url(~@/assets/img/icons/chevron-circle_review_dropdown.png);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right center; background-position: right center;
width: 16px; background-size: 1.25rem 1.25rem;
height: 9px; width: 1.25rem;
height: 1.25rem;
display: inline-flex; display: inline-flex;
position: relative;
right: 0.75rem;
margin: 0.5rem 0 0.5rem 1rem;
cursor: pointer; cursor: pointer;
} }
&.expanded:after { &.expanded:after {
transform: rotate(180deg); transform: rotate(180deg);
} }
&.expanded + .review-table { &.expanded + .review-table .review-table-expander {
max-height: 800px; max-height: 24rem;
transition: all 150ms ease-in;
overflow: hidden; overflow: hidden;
visibility: visible; visibility: visible;
} }

View file

@ -1,129 +0,0 @@
// Components
import customerReview from '@/layouts/payment-method/review-dropdown/review-sections/customer-review/customer-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
const testConstants = {
cms: {
header: {
text: 'Header'
},
sms: {
text: 'Sms'
}
},
customerInfo: {
firstName: 'First',
lastName: 'Last',
servicePhone: '111-111-1111',
emailAddress: 'builddigitaltest@safelite.com',
isSmsOptIn: false
},
displayContent: {
fullName: 'First Last',
phoneNumber: '111-111-1111',
emailAddress: 'builddigitaltest@safelite.com',
smsOptIn: 'Sms'
}
};
function generateDefaultProps() {
return {
cmsWidgetName: 'CustomerWidget',
customer: {
firstName: testConstants.customerInfo.firstName,
lastName: testConstants.customerInfo.lastName,
servicePhone: testConstants.customerInfo.servicePhone,
emailAddress: testConstants.customerInfo.emailAddress,
isSmsOptIn: testConstants.customerInfo.isSmsOptIn
}
};
}
let cmsContent;
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(customerReview, mountOptions);
return { wrapper };
}
beforeEach(() => {
cmsContent = {
CustomerWidget: {
HeaderText: testConstants.cms.header.text,
SubheaderText: testConstants.cms.sms.text
}
};
});
describe('Customer Review Block', () => {
test('Should display header text from cms', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
});
test('Should display sms text from cms', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.text);
});
test('Should render correct display content', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
testConstants.displayContent.fullName,
testConstants.displayContent.emailAddress,
testConstants.displayContent.phoneNumber,
testConstants.displayContent.smsOptIn
]);
});
});

View file

@ -1,7 +1,8 @@
<template> <template>
<reviewBlock <reviewBlock
:customHeaderText="header" :customHeaderText="header"
:content="displayContent" /> :content="displayContent"
:editLinkText="editLinkText" />
</template> </template>
<script> <script>
@ -13,30 +14,20 @@ export default {
reviewBlock reviewBlock
}, },
props: { props: {
cmsWidgetName: String, cmsWidgetName: String
customer: Object
}, },
data() { data() {
return {}; return {};
}, },
computed: { computed: {
displayContent() { displayContent() {
return [this.fullName, this.email, this.phoneNumber, this.smsOptIn]; return [this.getCmsContent(this.cmsWidgetName, 'BodyText')];
}, },
header() { header() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText'); return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
}, },
fullName() { editLinkText() {
return `${this.customer?.firstName} ${this.customer?.lastName}`; return this.getCmsContent(this.cmsWidgetName, 'FooterText');
},
email() {
return this.customer?.emailAddress;
},
phoneNumber() {
return this.customer?.servicePhone;
},
smsOptIn() {
return this.getCmsContent(this.cmsWidgetName, 'SubheaderText');
} }
} }
}; };

View file

@ -1,80 +0,0 @@
// Components
import damageReview from '@/layouts/payment-method/review-dropdown/review-sections/damage-review/damage-review.vue';
// Supporting Files
import { createTestingPinia } from '@pinia/testing';
import { useMainStore } from '@/store';
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { getDamageDisplayContent } from '@/helpers/damage-review-content-generator.js';
import baseMixin from '@/mixins/base-mixin';
jest.mock('@/helpers/damage-review-content-generator.js', () => ({
getDamageDisplayContent: jest.fn(),
getLocationAnswer: jest.fn()
}));
let cmsContent;
const mockMixin = {
methods: {
...baseMixin.methods,
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function getShallowMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
const testingPinia = createTestingPinia({
initialState: {
main: mainInitialState
}
});
useMainStore(testingPinia);
methodToRun();
mountOptions.global.plugins = [testingPinia];
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(damageReview, mountOptions);
return { wrapper };
}
beforeEach(() => {
getDamageDisplayContent.mockClear();
});
describe('Damage Review Block', () => {
test('computed displayContent calls getDamageDisplayContent with expected', () => {
// Arrange
const glassToReplace = ['front-window', 'side-window'];
const isRepair = false;
const initialStore = {
order: {
damage: {
glassToReplace,
isRepair
}
}
};
const expected = ['a', 'v', 'n'];
getDamageDisplayContent.mockImplementationOnce(() => expected);
const { wrapper } = getShallowMountedComponent(initialStore, {});
// Act
const result = wrapper.vm.displayContent;
// Assert
expect(result).toStrictEqual(expected);
expect(getDamageDisplayContent).toBeCalledTimes(1);
expect(getDamageDisplayContent).toBeCalledWith([], [], [], glassToReplace, isRepair);
});
});

View file

@ -1,47 +0,0 @@
<template>
<reviewBlock
:headerCmsWidgetName="cmsWidgetName"
:content="displayContent" />
</template>
<script>
import damageLocationsSelected from '@/constants/damage-locations-selected.js';
import { useMainStore } from '@/store';
import { getDamageDisplayContent, getLocationAnswer } from '@/helpers/damage-review-content-generator.js';
import reviewBlock from '@/layouts/payment-method/review-dropdown/review-block/review-block.vue';
export default {
name: 'damage-review',
components: {
reviewBlock
},
props: {
cmsWidgetName: String,
damageLocationsWidgetName: String,
damage: Object
},
computed: {
displayContent() {
const { glassToReplace, isRepair } = useMainStore().order.damage;
return getDamageDisplayContent(
this.locationAnswers,
this.driverSideDamageAnswers,
this.passengerSideDamageAnswers,
glassToReplace,
isRepair
);
},
driverSideDamageAnswers() {
const answerContent = getLocationAnswer(damageLocationsSelected.DRIVER, this.locationAnswers);
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
},
passengerSideDamageAnswers() {
const answerContent = getLocationAnswer(damageLocationsSelected.PASSENGER, this.locationAnswers);
return this.getInputQuestionWidgetAnswersNullSafe(answerContent?.SubWidgetName);
},
locationAnswers() {
return this.getInputQuestionWidgetAnswersNullSafe(this.damageLocationsWidgetName);
}
}
};
</script>

View file

@ -1,7 +1,8 @@
<template> <template>
<reviewBlock <reviewBlock
:customHeaderText="headerText" :customHeaderText="headerText"
:content="displayContent" /> :content="displayContent"
:editLinkText="editLinkText" />
</template> </template>
<script> <script>
@ -13,8 +14,7 @@ export default {
reviewBlock reviewBlock
}, },
props: { props: {
cmsWidgetName: String, cmsWidgetName: String
appointmentType: String
}, },
data() { data() {
return {}; return {};
@ -28,6 +28,9 @@ export default {
}, },
headerText() { headerText() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText'); return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
},
editLinkText() {
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
} }
} }
}; };

View file

@ -1,135 +0,0 @@
// Components
import
serviceLocationReview from '@/layouts/payment-method/review-dropdown/review-sections/service-location-review/service-location-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
const cmsContent = {
ServiceLocationTitleWidget: {
Text: 'Title Text'
}
};
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function generateDefaultProps() {
return {
cmsWidgetName: 'ServiceLocationTitleWidget',
serviceLocation: {
address: 'Mobile Address 1',
address2: 'Mobile Address 2',
city: 'Mobile City',
state: 'MO',
zipCode: '11111',
zipCodeCtu: '',
appointmentType: AppointmentTypeStrings.MOBILE,
isVehicleProtected: false,
provider: {
providerNumber: '',
address: {
streetAddress: 'Service Location Address',
city: 'Service Location City',
state: 'SL',
zipCode: '22222',
zipCodeCtu: ''
}
}
}
};
}
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(serviceLocationReview, mountOptions);
return { wrapper };
}
describe('Service Location Review Block', () => {
test('Should render mobile address if mobile appointment', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
'Mobile Address 1, Mobile Address 2, Mobile City, MO 11111'
]);
});
test('Should render service location address if inshop appointment.', async () => {
// Arrange
const props = generateDefaultProps();
props.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
'Service Location Address, Service Location City, SL 22222'
]);
});
test('Should render service location address if drop-off appointment', async () => {
// Arrange
const props = generateDefaultProps();
props.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF;
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual([
'Service Location Address, Service Location City, SL 22222'
]);
});
test('Should not add comma or any text if address2 is null', async () => {
// Arrange
const props = generateDefaultProps();
props.serviceLocation.address2 = null;
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toEqual(['Mobile Address 1, Mobile City, MO 11111']);
});
});

View file

@ -1,11 +1,13 @@
<template> <template>
<reviewBlock <reviewBlock
:headerCmsWidgetName="cmsWidgetName" :customHeaderText="headerText"
:content="displayContent" /> :content="displayContent"
:editLinkText="editLinkText" />
</template> </template>
<script> <script>
import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import { toTitleCase } from '@/helpers/text-helper';
import reviewBlock from '@/layouts/payment-method/review-dropdown/review-block/review-block.vue'; import reviewBlock from '@/layouts/payment-method/review-dropdown/review-block/review-block.vue';
export default { export default {
@ -21,11 +23,14 @@ export default {
return {}; return {};
}, },
computed: { computed: {
headerText() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
},
displayContent() { displayContent() {
return [ return [
`${this.addressInfo.address}${this.addressInfo.address2 ? ', ' : ''}${ `${this.addressInfo.address}${this.addressInfo.address2 ? ', ' : ''}${
this.addressInfo.address2 this.addressInfo.address2
}, ${this.addressInfo.city}, ${this.addressInfo.state} ${this.addressInfo.zipCode}` }, ${toTitleCase(this.addressInfo.city)}, ${this.addressInfo.state} ${this.addressInfo.zipCode}`
]; ];
}, },
addressInfo() { addressInfo() {
@ -46,6 +51,9 @@ export default {
state: this.serviceLocation?.provider?.address?.state, state: this.serviceLocation?.provider?.address?.state,
zipCode: this.serviceLocation?.provider?.address?.zipCode zipCode: this.serviceLocation?.provider?.address?.zipCode
}; };
},
editLinkText() {
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
} }
} }
}; };

View file

@ -1,965 +0,0 @@
// Components
import
servicePackageReview from '@/layouts/payment-method/review-dropdown/review-sections/service-package-review/service-package-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import packageNames from '@/constants/package-names';
import partTypeStrings from '@/constants/part-type-strings';
import damageLocationsSelected from '@/constants/damage-locations-selected';
import { useMainStore } from '@/store';
const testConstants = {
cmsPropValues: {
servicePackageOptionsCmsName: 'ServicePackageTitle',
defaultPackageItemsCmsName: 'DefaultPackageItemDescriptions',
vapsItemsCmsName: 'VapsItemDescriptions'
},
widgetNames: {
tierOneTitle: 'EconomyServiceTitle',
tierTwoTitle: 'StandardServiceTitle',
tierThreeTitle: 'PremiumServiceTitle'
},
defaultItemCopy: {
itemOne: 'Item Description 1',
itemTwo: 'Item Description 2',
itemThree: 'Item Description 3',
itemFour: 'Item Description 4',
defaultItemCopyArray: ['Item Description 1', 'Item Description 2', 'Item Description 3']
},
vapsCopy: {
frontWiperCopy: 'Front Wiper copy',
rearWiperCopy: 'Rear Wiper copy',
rainDefenseCopy: 'Rain repel copy'
},
parts: {
frontWiperPart: {
partNumber: 'SBB16',
description: 'SAFELITE BEAM BLADE 16',
partType: 'FRONT WIPER',
price: 32.64
},
rearWiperPart: {
partNumber: 'SBBR12A',
description: 'SAFELITE REAR BLADE 12A',
partType: 'REAR WIPER',
price: 24.48
},
rainDefensePart: {
partNumber: 'RAIN REPEL',
description: null,
partType: 'RAIN REPEL',
price: 35.5
},
recalPart: {
partNumber: 'RECAL STATIC',
Description: 'Recalibration',
partType: 'recalibration',
Quantity: '1',
price: 150.0
}
},
damages: {
frontWindshield: {
glassLocation: damageLocationsSelected.WINDSHIELD,
glassName: damageLocationsSelected.SINGLE
},
rearWindshield: {
glassLocation: damageLocationsSelected.REAR,
glassName: damageLocationsSelected.STATIONARY
},
sideGlass: {
glassLocation: damageLocationsSelected.PASSENGER,
glassName: damageLocationsSelected.QUARTER
}
},
imageId: '00000000-0000-0000-0000-000000000000'
};
const figmaScenarios = [
{
name: '05_01_CSR_Quote_Cash',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_Standard_Repair',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: true,
glassToReplace: []
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_Recal',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
},
glassParts: [],
supportingItems: [testConstants.parts.recalPart]
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
// 05_01_CSR_Quote_RearGlass omitted as a duplicate of below.
{
name: '05_01_CSR_Quote_RearGlass+NonWindshield',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.rearWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Frontwiper',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Standard+RainDefense',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Premium',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_RearGlass+Windshield',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [
testConstants.damages.frontWindshield,
testConstants.damages.rearWindshield
]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Frontwiper',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Economy+Rearwiper',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Economy+RainDefense',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Economy+Frontwiper+RainDefense',
vapsCombo: [
testConstants.parts.frontWiperPart,
testConstants.parts.rainDefensePart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Economy+Rearwiper+RainDefense',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rearWiperPart,
testConstants.parts.frontWiperPart,
testConstants.parts.rainDefensePart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
},
{
name: '05_01_CSR_Quote_RearGlassNoFrontFit',
params: {
wiperResponse: [testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.rearWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Raindefense',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
},
// 05_01_CSR_Quote_Windshield+SideGlass has identical outcomes to 05_01_CSR_Quote_Cash, but included in case that changes in the future.
{
name: '05_01_CSR_Quote_Windshield+SideGlass',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [
testConstants.damages.frontWindshield,
testConstants.damages.sideGlass
]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Standard',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Standard+RearWiper',
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy
]
}
}
]
},
// Has no standard package
{
name: '05_01_CSR_Quote_SideGlass',
params: {
wiperResponse: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.sideGlass]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Economy+Frontwiper',
vapsCombo: [testConstants.parts.frontWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy
]
}
},
{
name: 'Economy+RainDefense',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Economy+Rearwiper',
vapsCombo: [testConstants.parts.rearWiperPart],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rearWiperCopy
]
}
},
{
name: 'Premium',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
},
{
name: 'Premium+Rearwiper',
vapsCombo: [
testConstants.parts.rainDefensePart,
testConstants.parts.frontWiperPart,
testConstants.parts.rearWiperPart
],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.frontWiperCopy,
testConstants.vapsCopy.rearWiperCopy,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
},
// Has no standard package
{
name: '05_01_CSR_Quote_NoWiperFit',
params: {
wiperResponse: [],
rainDefenseResponse: testConstants.parts.rainDefensePart,
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
},
glassParts: [],
supportingItems: []
},
iterations: [
{
name: 'Economy',
vapsCombo: [],
expected: {
packageNameWidget: testConstants.widgetNames.tierOneTitle,
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray
}
},
{
name: 'Premium',
vapsCombo: [testConstants.parts.rainDefensePart],
expected: {
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
displayContent: [
...testConstants.defaultItemCopy.defaultItemCopyArray,
testConstants.vapsCopy.rainDefenseCopy
]
}
}
]
}
];
function generateDefaultProps() {
return {
servicePackageOptionsCmsName: testConstants.cmsPropValues.servicePackageOptionsCmsName,
defaultPackageItemsCmsName: testConstants.cmsPropValues.defaultPackageItemsCmsName,
vapsItemsCmsName: testConstants.cmsPropValues.vapsItemsCmsName,
lineItems: {
glassParts: [],
supportingItems: [],
vaps: [testConstants.parts.frontWiperPart]
},
damage: {
isRepair: false,
glassToReplace: [testConstants.damages.frontWindshield]
}
};
}
let cmsContent;
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => cmsContent?.[widgetName]?.[cmsFieldName] ?? '')
}
};
function initializeWithDefault() {
useMainStore().order.availableVaps = [testConstants.parts.frontWiperPart, testConstants.parts.rainDefensePart];
}
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(servicePackageReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}
beforeEach(() => {
cmsContent = {
ServicePackageTitle: {
Answers: [
{
Name: packageNames.TIER_ONE,
Text: '',
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: testConstants.widgetNames.tierOneTitle
},
{
Name: packageNames.TIER_TWO,
Text: '',
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: testConstants.widgetNames.tierTwoTitle
},
{
Name: packageNames.TIER_THREE,
Text: '',
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: testConstants.widgetNames.tierThreeTitle
}
]
},
DefaultPackageItemDescriptions: {
Answers: [
{
Name: 'Item1',
Text: testConstants.defaultItemCopy.itemOne,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: 'Item2',
Text: testConstants.defaultItemCopy.itemTwo,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: 'Item3',
Text: testConstants.defaultItemCopy.itemThree,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
},
VapsItemDescriptions: {
Answers: [
{
Name: partTypeStrings.FRONT_WIPER,
Text: testConstants.vapsCopy.frontWiperCopy,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: partTypeStrings.REAR_WIPER,
Text: testConstants.vapsCopy.rearWiperCopy,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
},
{
Name: partTypeStrings.RAIN_DEFENSE,
Text: testConstants.vapsCopy.rainDefenseCopy,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
}
]
}
};
});
describe('Service Package Review Block', () => {
describe('General functionality', () => {
test('Should properly "Round Down" package tier', async () => {
// Slightly longer explanation:
// Should only return the highest tier where *every* offered VAP is part of the order.
// However, there may be vaps not offered in the qualifying tier. Hence rounding *down*.
//
// I.e. Economy=[], Standard=[front wipers], Premium=[front wipers, rain defense].
// Current vaps=[rain defense]. Though rain defense is in Premium, we don't satisfy it or standard.
// So our tier should still be Economy.
// Should still display extra vaps.
// Arrange
const props = generateDefaultProps();
props.lineItems.vaps = [testConstants.parts.rainDefensePart];
const { wrapper } = getShallowMountedComponent({
...props
});
initializeWithDefault();
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.packageNameWidget).toEqual(testConstants.widgetNames.tierOneTitle);
const containsRainDefenseCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.rainDefenseCopy);
expect(containsRainDefenseCopy).toBe(true);
});
test('Should display all default items from cms', async () => {
// Arrange
cmsContent.DefaultPackageItemDescriptions.Answers.push({
Name: 'Item4',
Text: testConstants.defaultItemCopy.itemFour,
SubText: '',
ImageId: testConstants.imageId,
Image: '',
SubWidgetName: ''
});
const props = generateDefaultProps();
props.lineItems.vaps = [];
const { wrapper } = getShallowMountedComponent({
...props
});
initializeWithDefault();
// Act
await wrapper.vm.$nextTick();
// Assert
const expectedResult = [
testConstants.defaultItemCopy.itemOne,
testConstants.defaultItemCopy.itemTwo,
testConstants.defaultItemCopy.itemThree,
testConstants.defaultItemCopy.itemFour
];
expect(wrapper.vm.displayContent).toEqual(expectedResult);
});
test('Should display vaps if and only if they are added', async () => {
// Arrange
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
// Act
await wrapper.vm.$nextTick();
initializeWithDefault();
// Assert
const includesFrontWiperCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.frontWiperCopy);
const includesRainDefenseCopy = wrapper.vm.displayContent.includes(testConstants.vapsCopy.rainDefenseCopy);
expect(includesFrontWiperCopy).toBe(true);
expect(includesRainDefenseCopy).toBe(false);
});
test('Should not error if cms content is missing (though may display poorly).', async () => {
// Arrange
cmsContent = {};
const props = generateDefaultProps();
const { wrapper } = getShallowMountedComponent({
...props
});
initializeWithDefault();
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.packageNameWidget).toEqual('');
expect(wrapper.vm.displayContent).toEqual([]);
});
});
describe('Match Figma Scenarios', () => {
figmaScenarios.forEach((scenario) => {
scenario.iterations.forEach((iteration) => {
it(`Should match figma scenario "${scenario.name}", iteration "${iteration.name}"`, async () => {
// Arrange
const props = generateDefaultProps();
props.damage = scenario.params.damage;
props.glassParts = scenario.params.glassParts;
props.lineItems.supportingItems = scenario.params.supportingItems;
props.lineItems.vaps = iteration.vapsCombo;
const { wrapper } = getShallowMountedComponent({
...props
});
useMainStore().order.availableVaps = [scenario.params.rainDefenseResponse, ...scenario.params.wiperResponse];
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.packageNameWidget).toEqual(iteration.expected.packageNameWidget);
expect(wrapper.vm.displayContent).toEqual(iteration.expected.displayContent);
});
});
});
});
});

View file

@ -1,100 +0,0 @@
<template>
<reviewBlock
:headerCmsWidgetName="packageNameWidget"
:content="displayContent" />
</template>
<script>
import settleAllPromises from '@/helpers/layout-helper';
// import baseMixin from '@/mixins/base-mixin.js';
import { useMainStore } from '@/store';
import reviewBlock from '@/layouts/payment-method/review-dropdown/review-block/review-block.vue';
import {
getHighestFullySatisfiedTier,
containsLineItemWithPartType
} from '@/helpers/service-package-helper.js';
export default {
name: 'service-package-review',
components: {
reviewBlock
},
props: {
servicePackageOptionsCmsName: String,
defaultPackageItemsCmsName: String,
vapsItemsCmsName: String,
lineItems: Object,
damage: Object
},
emits: ['edit-clicked'],
data() {
return {
availableVaps: []
};
},
computed: {
displayContent() {
return [...this.defaultPackageText, ...this.vapsItemText];
},
packageNameWidget() {
const servicePackageNames = this.getCmsContent(
this.servicePackageOptionsCmsName,
'Answers'
);
if (!servicePackageNames) {
return '';
}
const currentPackage = servicePackageNames.find((entry) => entry.Name === this.packageLevel);
return currentPackage.SubWidgetName;
},
defaultPackageText() {
const defaultItems = this.getCmsContent(this.defaultPackageItemsCmsName, 'Answers');
if (!defaultItems) {
return [];
}
return defaultItems.map((answer) => answer.Text);
},
vapsItemText() {
const vapsDescriptions = this.getCmsContent(this.vapsItemsCmsName, 'Answers');
if (!vapsDescriptions) {
return [];
}
const vapsDescriptionsOnOrder = vapsDescriptions.filter((answer) =>
containsLineItemWithPartType(answer.Name, this.vaps));
return vapsDescriptionsOnOrder.map((answer) => answer.Text);
},
packageLevel() {
return getHighestFullySatisfiedTier(
this.glassToReplace,
this.availableLineItems,
this.isRepair,
this.vaps
);
},
glassToReplace() {
return this.damage.glassToReplace;
},
isRepair() {
return this.damage.isRepair;
},
vaps() {
return this.lineItems?.vaps;
},
availableLineItems() {
return [
...(this.lineItems?.glassParts ?? []),
...(this.lineItems?.supportingItems ?? []),
...(useMainStore().order.availableVaps ?? [])
];
}
}
};
</script>

View file

@ -1,47 +0,0 @@
// Components
import vehicleReview from '@/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue';
// Supporting Files
import { shallowMount } from '@vue/test-utils';
import { getMountOptions } from '@/helpers/unit-test-helper.js';
jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: () => Promise.resolve('content')
}));
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
navigate: jest.fn()
}
});
methodToRun();
mountOptions.data = () => (
initialData
);
const wrapper = shallowMount(vehicleReview, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
return { wrapper };
}
describe('Vehicle Review Block', () => {
test('Correctly assembles vehicle info into a display string', async () => {
// Arrange
const { wrapper } = getShallowMountedComponent({
vehicle: {
year: '2019',
make: 'Honda',
model: 'Odyssey'
}
});
// Act
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayContent).toStrictEqual(['2019 Honda Odyssey']);
});
});

View file

@ -1,10 +1,13 @@
<template> <template>
<reviewBlock <reviewBlock
:headerCmsWidgetName="cmsWidgetName" :customHeaderText="header"
:content="displayContent" /> :content="displayContent"
:editLinkText="editLinkText" />
</template> </template>
<script> <script>
import { getStringWithCustomValues, processIfStatements } from '@/helpers/cms-content-helper';
import { getGlassList } from '@/helpers/damage-helper';
import reviewBlock from '@/layouts/payment-method/review-dropdown/review-block/review-block.vue'; import reviewBlock from '@/layouts/payment-method/review-dropdown/review-block/review-block.vue';
export default { export default {
@ -13,15 +16,40 @@ export default {
reviewBlock reviewBlock
}, },
props: { props: {
cmsWidgetName: String, cmsWidgetName: String
vehicle: Object
}, },
data() { data() {
return {}; return {};
}, },
computed: { computed: {
header() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
},
displayContent() { displayContent() {
return [`${this.vehicle.year} ${this.vehicle.make} ${this.vehicle.model}`]; return [this.getCmsContentWithCustomValues(this.cmsWidgetName, 'BodyText')];
},
editLinkText() {
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
},
customValueMap() {
return {
glassList: this.glassList,
};
},
glassList() {
const glassPieces = this.mainStore.lineItems.glassParts?.map((part) => part.partType.toLowerCase()) ?? [];
return getGlassList(glassPieces);
},
},
methods: {
getCmsContentWithCustomValues(widgetName, widgetField) {
const rawText = this.getCmsContent(widgetName, widgetField);
const processedIfStatements = processIfStatements(
rawText,
'custom',
(v) => this.customValueMap[v]
);
return getStringWithCustomValues(processedIfStatements, this.customValueMap);
} }
} }
}; };

View file

@ -124,13 +124,7 @@ export default {
}, },
async saveAndSubmitWorkOrder() { async saveAndSubmitWorkOrder() {
// Final work order submit after returning from pay in advance. // Final work order submit after returning from pay in advance.
try { await submitWorkOrder({ submitType: submitType.SAFELITE });
await submitWorkOrder({ submitType: submitType.SAFELITE });
} catch (error) {
console.error(`error: response from submit work order:${error.message}`);
this.navigateOnPayInAdvanceError();
return;
}
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.PAY_IN_ADVANCE_SUCCESS, this.navigationScenarios.PAY_IN_ADVANCE_SUCCESS,

View file

@ -252,12 +252,12 @@ describe('policy-vehicles.vue', () => {
); );
test( test(
'Error in lookupVehicleByVin call => bailout true and navigate forward with CLICKED_FORWARD_WITH_BAILOUT scenario.', 'Error in lookupVehicleByVin call => error thrown in forwardButtonAction.',
async () => { async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
const lookupReturnValue = { error: true, status: 500, data: 'error' }; const lookupReturnValue = { error: true, status: 500, data: 'error' };
wrapper.vm.lookupVehicleByVin = jest.fn().mockReturnValue(lookupReturnValue); wrapper.vm.lookupVehicleByVin = jest.fn().mockReturnValue(Promise.reject(lookupReturnValue));
const vin = getRandomString(17, 17); const vin = getRandomString(17, 17);
await wrapper.setData({ await wrapper.setData({
@ -270,21 +270,14 @@ describe('policy-vehicles.vue', () => {
}); });
// Act // Act
wrapper.vm.mainStore.applicationUser.pageData[issPageValues.BAILOUT_PAGE] = {
'bailout-page': {
bailoutCode: bailoutCode.VehicleVinLookupError
}
};
await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(wrapper.vm.mainStore.setBailout).toHaveBeenCalledWith(bailoutMessage.vehicleVinLookupError(vin, lookupReturnValue.data)); expect.assertions(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith( try {
navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT, await wrapper.vm.forwardButtonAction();
undefined, } catch (e) {
{}, expect(e).toBe(lookupReturnValue);
{} }
);
} }
); );
@ -292,7 +285,7 @@ describe('policy-vehicles.vue', () => {
async () => { async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.lookupVehicleByVin = jest.fn().mockReturnValue({ error: true, status: 404 }); wrapper.vm.lookupVehicleByVin = jest.fn().mockRejectedValue({ isAxiosError: true, status: 404 });
const vin = getRandomString(17, 17); const vin = getRandomString(17, 17);
await wrapper.setData({ await wrapper.setData({

View file

@ -59,7 +59,6 @@ import vehicleSelectionOptions from '@/constants/vehicle-selection-options.js';
import endorsementOptions from '@/constants/endorsement-options.js'; import endorsementOptions from '@/constants/endorsement-options.js';
import globalRules from '@/constants/global-rules.js'; import globalRules from '@/constants/global-rules.js';
import { useMainStore } from '@/store/index.js'; import { useMainStore } from '@/store/index.js';
import bailoutMessage from '@/constants/bailoutMessage';
import { import {
deductibleForSelectedVehicle, deductibleForSelectedVehicle,
endorsementsForSelectedVehicle, endorsementsForSelectedVehicle,
@ -144,10 +143,6 @@ export default {
}, },
repairWaivedForSelectedVehicle() { repairWaivedForSelectedVehicle() {
return repairWaivedForSelectedVehicle(this.selectedPolicyVehicle); return repairWaivedForSelectedVehicle(this.selectedPolicyVehicle);
},
selectedVehicle() {
const vehicle = this.mainStore.lookupVehicleByVin(this.selectedVehicleVin);
return vehicle;
} }
}, },
watch: { watch: {
@ -156,15 +151,12 @@ export default {
if (value === vehicleSelectionOptions.VEHICLE_NOT_LISTED) { if (value === vehicleSelectionOptions.VEHICLE_NOT_LISTED) {
// clear previously selected vehicle and image // clear previously selected vehicle and image
this.mainStore.resetVehicleState(); this.mainStore.resetVehicleState();
} else { return;
// get vehicle details from selected VIN }
const vehicle = await this.lookupVehicleByVin(value);
// handle error in case vehicle info doesn't come back for selected VIN // get vehicle details from selected VIN
if (vehicle?.error === true) { try {
this.mainStore.resetVehicleState(); const vehicle = await this.lookupVehicleByVin(value);
return;
}
if (!vehicle?.data.canSafeliteService) { if (!vehicle?.data.canSafeliteService) {
this.displayNoServiceAlert = true; this.displayNoServiceAlert = true;
return; return;
@ -177,6 +169,8 @@ export default {
vin: value vin: value
}); });
} }
} catch (e) {
this.mainStore.resetVehicleState();
} }
} }
}, },
@ -196,33 +190,16 @@ export default {
); );
}, },
async forwardButtonAction() { async forwardButtonAction() {
if (this.selectedVehicleVin !== vehicleSelectionOptions.VEHICLE_NOT_LISTED) { if (this.selectedVehicleVin === vehicleSelectionOptions.VEHICLE_NOT_LISTED) {
useMainStore().updateCoverageType(coverageType.NONE);
useMainStore().updateCoverageStatus(coverageStatuses.NO_COVERAGE);
this.navigateForward();
return;
}
const vehicle = this.policyVehicles.find((pv) => pv.vin === this.selectedVehicleVin);
try {
const vehicleLookupResponse = await this.lookupVehicleByVin(this.selectedVehicleVin); const vehicleLookupResponse = await this.lookupVehicleByVin(this.selectedVehicleVin);
const vehicle = this.policyVehicles.find((pv) => pv.vin === this.selectedVehicleVin);
if (vehicleLookupResponse.error) {
if (vehicleLookupResponse.status === 404) {
this.mainStore.resetVehicleState();
useMainStore().updateVehicle({
policyVehicleId: vehicle.id,
carId: '0',
category: '',
year: vehicle.vehicleYear || '',
make: vehicle.vehicleMake || '',
model: vehicle.vehicleModel || '',
style: vehicle.vehicleStyle || '',
vin: vehicle.vin
});
this.policyVinFound = false;
return this.navigateForward();
}
this.mainStore.setBailout(bailoutMessage.vehicleVinLookupError(
vehicle.vin,
vehicleLookupResponse.data
));
return this.navigateForward();
}
useMainStore().updateVehicle({ useMainStore().updateVehicle({
...vehicleLookupResponse.data, ...vehicleLookupResponse.data,
@ -235,21 +212,30 @@ export default {
repairWaived: this.repairWaivedForSelectedVehicle, repairWaived: this.repairWaivedForSelectedVehicle,
endorsements: this.endorsementsForSelectedVehicle endorsements: this.endorsementsForSelectedVehicle
}); });
} else { this.navigateForward();
useMainStore().updateCoverageType(coverageType.NONE); } catch (e) {
useMainStore().updateCoverageStatus(coverageStatuses.NO_COVERAGE); if (e.isAxiosError && e.status === 404) {
this.mainStore.resetVehicleState();
useMainStore().updateVehicle({
policyVehicleId: vehicle.id,
carId: '0',
category: '',
year: vehicle.vehicleYear || '',
make: vehicle.vehicleMake || '',
model: vehicle.vehicleModel || '',
style: vehicle.vehicleStyle || '',
vin: vehicle.vin
});
this.policyVinFound = false;
this.navigateForward();
return;
}
throw e;
} }
return this.navigateForward();
}, },
navigateForward() { navigateForward() {
if (this.mainStore.isBailout) { if (this.selectedVehicleVin === vehicleSelectionOptions.VEHICLE_NOT_LISTED) {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route,
{},
{}
);
} else if (this.selectedVehicleVin === vehicleSelectionOptions.VEHICLE_NOT_LISTED) {
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_NON_LISTED_VEHICLE, this.navigationScenarios.CLICKED_FORWARD_NON_LISTED_VEHICLE,
this.$route, this.$route,
@ -281,15 +267,7 @@ export default {
} }
}, },
async lookupVehicleByVin(vin) { async lookupVehicleByVin(vin) {
try { return useMainStore().lookupVehicleByVin(vin);
return await useMainStore().lookupVehicleByVin(vin);
} catch (responseError) {
return {
error: true,
status: responseError.status,
data: responseError.data
};
}
}, },
async addAnotherVehicle() { async addAnotherVehicle() {
this.selectedVehicleVin = vehicleSelectionOptions.VEHICLE_NOT_LISTED; this.selectedVehicleVin = vehicleSelectionOptions.VEHICLE_NOT_LISTED;

View file

@ -20,7 +20,8 @@ jest.mock('@/helpers/cms-content-helper', () => ({
function setupMocks(mockApiResponses) { function setupMocks(mockApiResponses) {
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
router: { router: {
navigate: jest.fn() navigate: jest.fn(),
navigateBailout: jest.fn()
}, },
route: 'provider-preference' route: 'provider-preference'
}); });
@ -43,7 +44,7 @@ function setupMocks(mockApiResponses) {
} }
describe('provider-preference.vue', () => { describe('provider-preference.vue', () => {
test('Should navigate to tpa disabled route page when TPAOption selected and TPA Flow disabled', () => { test('Should navigateBailout when TPAOption selected and TPA Flow disabled', () => {
// Arrange // Arrange
const { wrapper } = setupMocks(); const { wrapper } = setupMocks();
@ -52,7 +53,7 @@ describe('provider-preference.vue', () => {
wrapper.vm.findAnotherShopClicked(); wrapper.vm.findAnotherShopClicked();
// Test // Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED, 'provider-preference'); expect(wrapper.vm.$router.navigateBailout).toBeCalled();
}); });
test('Should navigate to safelite flow when navigateWithTPARecalAnswer is called with SafeliteOption', () => { test('Should navigate to safelite flow when navigateWithTPARecalAnswer is called with SafeliteOption', () => {

View file

@ -186,9 +186,7 @@ export default {
this.scheduleWithTPA(); this.scheduleWithTPA();
} }
} else { } else {
this.mainStore.setBailout(bailoutMessage.TPANotEnabled()); this.$router.navigateBailout(bailoutMessage.TPANotEnabled());
const scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED;
this.navigateForward(scenario);
} }
}, },
openStateSteeringModal() { openStateSteeringModal() {

View file

@ -17,6 +17,7 @@
<div class="service-location-content-container"> <div class="service-location-content-container">
<serviceLocation <serviceLocation
ref="serviceLocation" ref="serviceLocation"
:schedulingInshopAvailabilityRating="inShopAvailabilityRating"
@appointmentTypeChanged="appointmentTypeChangedFromServiceLocation" @appointmentTypeChanged="appointmentTypeChangedFromServiceLocation"
@cityUpdated="cityUpdatedFromServiceLocation" @cityUpdated="cityUpdatedFromServiceLocation"
@inShopZipUpdated="inShopZipUpdatedFromServiceLocation" @inShopZipUpdated="inShopZipUpdatedFromServiceLocation"
@ -88,9 +89,9 @@ import {
import settleAllPromises from '@/helpers/layout-helper'; import settleAllPromises from '@/helpers/layout-helper';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js'; import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import { import {
getPricedMobileFeePart, getProviderData,
getServiceabilityDetails, getZipCodeData,
getZipCodeData onProviderChanged
} from '@/helpers/service-location-helper'; } from '@/helpers/service-location-helper';
import { Form } from 'vee-validate'; import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js'; import BaseFormMixin from '@/mixins/base-form-mixin.js';
@ -170,20 +171,20 @@ const getAvailableDates = async (
await Promise.all(storeActionConfigs.map(async (storeAction) => { await Promise.all(storeActionConfigs.map(async (storeAction) => {
let timeSlotsResponse = null; let timeSlotsResponse = null;
if (storeAction.storeAction === GET_SHOP_TIME_SLOTS) { if (storeAction.storeAction === GET_SHOP_TIME_SLOTS) {
timeSlotsResponse = await useMainStore().getShopTimeSlots( if (storeAction.payload.providerNumber) {
storeAction.payload.startDate, timeSlotsResponse = await useMainStore().getShopTimeSlots(
storeAction.payload.endDate, storeAction.payload.startDate,
storeAction.payload.shopAppointmentType, storeAction.payload.endDate,
storeAction.payload.providerNumber storeAction.payload.shopAppointmentType,
); storeAction.payload.providerNumber
);
}
} else if (storeAction.payload?.zipCodeOverride) { } else if (storeAction.payload?.zipCodeOverride) {
timeSlotsResponse = await useMainStore().getMobileTimeSlots( timeSlotsResponse = await useMainStore().getMobileTimeSlots(
storeAction.payload.startDate, storeAction.payload.startDate,
storeAction.payload.endDate, storeAction.payload.endDate,
storeAction.payload.zipCodeOverride storeAction.payload.zipCodeOverride
).catch(() => { );
console.warn('Error fetching mobile time slots...');
});
} }
if (!timeSlotsResponse || !timeSlotsResponse.data) { if (!timeSlotsResponse || !timeSlotsResponse.data) {
@ -203,6 +204,9 @@ const getAvailableDates = async (
// sort days chronologically // sort days chronologically
timeSlotsResponsesData.days.sort(compareDayStrings); timeSlotsResponsesData.days.sort(compareDayStrings);
return timeSlotsResponsesData; return timeSlotsResponsesData;
})
.catch(() => {
return timeSlotsResponsesData;
}); });
}; };
@ -221,17 +225,10 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const storeServiceLocation = useMainStore().order.serviceLocation; const { isMobileAppointment, storeServiceLocation, storeSelectedProvider, serviceZipCode } = getProviderData();
const storeSelectedAppointmentType = storeServiceLocation?.appointmentType;
const isMobileAppointment = storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE
|| storeSelectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const storeSelectedProvider = storeServiceLocation?.provider;
const serviceZipCode = storeServiceLocation?.zipCode || useMainStore().order.customer.address.zipCode;
const zipCodeData = getZipCodeData(serviceZipCode); const zipCodeData = getZipCodeData(serviceZipCode);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode); const serviceabilityDetailsPromise = useMainStore().getServiceabilityDetails(serviceZipCode);
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode); const providersPromise = useMainStore().getSafeliteProviders(serviceZipCode, 150);
const getGlassFeesPromise = useMainStore().getGlassFees();
const providersPromise = useMainStore().getSafeliteProviders(serviceZipCode);
const premiumFeePromise = useMainStore().getMobilePremiumFee(); const premiumFeePromise = useMainStore().getMobilePremiumFee();
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => { const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
@ -247,14 +244,6 @@ export default {
resultKey: 'cmsContent', resultKey: 'cmsContent',
promise: cmsContentPromise promise: cmsContentPromise
}, },
{
resultKey: 'mobileFeePart',
promise: mobileFeePartPromise
},
{
resultKey: 'glassFees',
promise: getGlassFeesPromise
},
{ {
resultKey: 'serviceabilityDetails', resultKey: 'serviceabilityDetails',
promise: serviceabilityDetailsPromise promise: serviceabilityDetailsPromise
@ -267,6 +256,10 @@ export default {
resultKey: 'providers', resultKey: 'providers',
promise: providersPromise promise: providersPromise
}, },
{
resultKey: 'providerChanged',
promise: onProviderChanged()
},
{ {
resultKey: 'premiumFeeWithPrice', resultKey: 'premiumFeeWithPrice',
promise: premiumFeeWithPricePromise promise: premiumFeeWithPricePromise
@ -275,25 +268,39 @@ export default {
// use resultMap to populate layout content. // use resultMap to populate layout content.
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
const providersUnderOneHundredMiles = resultMap.providers?.shopProviders?.filter((provider) => provider.distanceMiles <= 100);
const serviceLocationData = { const serviceLocationData = {
defaultMobileZipCode: isMobileAppointment defaultMobileZipCode: isMobileAppointment
? storeServiceLocation?.zipCode ? storeServiceLocation?.zipCode
: '', : '',
glassFees: resultMap.glassFees, glassFees: resultMap.glassFees,
mobileFeePart: resultMap.mobileFeePart, mobileFeePart: resultMap.mobileFeePart,
providers: resultMap.providers, providers: {
...resultMap.providers,
shopProviders: providersUnderOneHundredMiles || []
},
serviceabilityDetails: resultMap.serviceabilityDetails, serviceabilityDetails: resultMap.serviceabilityDetails,
zipCodeData: resultMap.zipCodeData zipCodeData: resultMap.zipCodeData
}; };
useMainStore().updateIsSafeliteProvider(true); useMainStore().updateIsSafeliteProvider(true);
next(async (vm) => { next(async (vm) => {
const providerToUse = resultMap.providers?.shopProviders const foundProviderInFullList = resultMap.providers?.shopProviders
.find((provider) => provider.providerNumber === storeSelectedProvider?.providerNumber) || resultMap.providers?.shopProviders[0]; .find((provider) => provider.providerNumber === storeSelectedProvider?.providerNumber) || resultMap.providers?.shopProviders[0];
const foundProviderInHundredMiles = providersUnderOneHundredMiles
.find((provider) => provider.providerNumber === storeSelectedProvider?.providerNumber) || providersUnderOneHundredMiles[0];
if (!foundProviderInHundredMiles && !foundProviderInFullList) {
if (storeSelectedProvider?.providerNumber && resultMap.providers?.shopProviders) {
serviceLocationData.providers?.shopProviders.push(storeSelectedProvider);
}
} else if (!foundProviderInHundredMiles && foundProviderInFullList) {
serviceLocationData.providers?.shopProviders.push(foundProviderInFullList);
}
const initialServiceLocationObj = { const initialServiceLocationObj = {
mobileProviderNumber: isMobileAppointment mobileProviderNumber: isMobileAppointment
? storeSelectedProvider?.providerNumber ? storeSelectedProvider?.providerNumber
: null, : null,
provider: isMobileAppointment ? null : providerToUse, provider: isMobileAppointment ? null : foundProviderInFullList,
zipCode: useMainStore().order.serviceLocation.zipCode || useMainStore().order.customer.address.zipCode, zipCode: useMainStore().order.serviceLocation.zipCode || useMainStore().order.customer.address.zipCode,
zipCodeCtu: resultMap.zipCodeData?.zipCodeCtu zipCodeCtu: resultMap.zipCodeData?.zipCodeCtu
}; };
@ -310,10 +317,10 @@ export default {
}, },
data() { data() {
return { return {
inShopAvailabilityRating: 'high',
inShopDatesData: [], inShopDatesData: [],
isMobileView: false, isMobileView: false,
mobileDatesData: [], mobileDatesData: [],
mobileFeePart: null,
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
mobileProviderNumber: null, mobileProviderNumber: null,
selectableDatesData: [], selectableDatesData: [],
@ -364,6 +371,27 @@ export default {
return useMainStore().lineItems.supportingItems; return useMainStore().lineItems.supportingItems;
} }
}, },
watch: {
inShopDatesData(newData) {
if (newData?.initialShopTimeSlotsResponse?.days?.length > 0) {
const numberOfDaysNeededToBeHighAvailability = 2;
const availabilityEndDate = new Date();
availabilityEndDate.setDate(availabilityEndDate.getDate() + 6);
const datesInAvailabilityRange = newData.initialShopTimeSlotsResponse.days.filter((day) => {
const dayDate = new Date(`${day.date}T00:00:00`);
return dayDate <= availabilityEndDate;
});
const isGoodAvailability =
datesInAvailabilityRange.filter((x) => x.timeSlots.length > 0).length
>= numberOfDaysNeededToBeHighAvailability;
this.inShopAvailabilityRating = isGoodAvailability ? 'high' : 'low';
} else {
this.inShopAvailabilityRating = 'low';
}
}
},
mounted() { mounted() {
showIssLoadingModal(true); showIssLoadingModal(true);
this.mql = window.matchMedia('(min-width: 1200px)'); this.mql = window.matchMedia('(min-width: 1200px)');
@ -453,9 +481,6 @@ export default {
}, },
getAvailableDates, getAvailableDates,
async getDatePickerInitialData(defaultProviderNumber = null) { async getDatePickerInitialData(defaultProviderNumber = null) {
this.inShopDatesData = [];
this.mobileDatesData = [];
let todayDateString; let todayDateString;
const todayDateObject = new Date(); const todayDateObject = new Date();
const calendarViewDirection = 'future'; const calendarViewDirection = 'future';
@ -504,7 +529,20 @@ export default {
preSelectedDate, preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays, initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null daysFromStart: endDateObject.daysFromStart || null
})); }))
.catch(() => {
console.warn('Error fetching initial available dates...');
return {
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: { days: [] },
preSelectedDate,
initialDaysLoaded: endDateObject.newDaysLoaded || initialDays,
daysFromStart: endDateObject.daysFromStart || null
};
});
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) { || this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
@ -532,6 +570,7 @@ export default {
} }
} else { } else {
this.inShopDatesData = initialData; this.inShopDatesData = initialData;
this.mobileDatesData = [];
} }
return initialData; return initialData;
@ -608,7 +647,9 @@ export default {
} }
}, },
mobileZipUpdatedFromServiceLocation(mobileZipServiceLocation) { mobileZipUpdatedFromServiceLocation(mobileZipServiceLocation) {
if (mobileZipServiceLocation) { if (mobileZipServiceLocation
&& (this.mobileProviderNumber !== mobileZipServiceLocation.mobileProviderNumber
|| !this.hasNeededServiceLocationData)) {
this.mobileProviderNumber = mobileZipServiceLocation.mobileProviderNumber; this.mobileProviderNumber = mobileZipServiceLocation.mobileProviderNumber;
this.selectedMobileZipCode = mobileZipServiceLocation.zipCode; this.selectedMobileZipCode = mobileZipServiceLocation.zipCode;
this.selectedServiceLocation = mobileZipServiceLocation; this.selectedServiceLocation = mobileZipServiceLocation;
@ -618,12 +659,17 @@ export default {
} }
} }
}, },
providerChangedFromServiceLocation(newProvider) { async providerChangedFromServiceLocation(newProvider) {
this.selectedProvider = newProvider?.provider; this.selectedProvider = newProvider?.provider;
if (newProvider?.provider) {
if (newProvider?.refreshDatePicker) { this.mainStore.updateServiceLocation(newProvider);
showIssLoadingModal(true); showIssLoadingModal(true);
this.refreshDatePicker(); await onProviderChanged();
if (newProvider.refreshDatePicker) {
await this.refreshDatePicker();
} else {
showIssLoadingModal(false);
}
} }
}, },
async refreshDatePicker() { async refreshDatePicker() {
@ -654,8 +700,7 @@ export default {
appointmentTypeToUse = AppointmentTypeStrings.DROP_OFF; appointmentTypeToUse = AppointmentTypeStrings.DROP_OFF;
} }
// Save service location then save schedule then navigate forward this.mainStore.updateAppointmentType(appointmentTypeToUse);
await this.$refs.serviceLocation.forwardButtonAction(appointmentTypeToUse);
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot); this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD, this.navigationScenarios.CLICKED_FORWARD,

View file

@ -88,7 +88,6 @@ import { useMainStore } from '@/store';
import { import {
getMobileZipCodeData, getMobileZipCodeData,
getPricedMobileFeePart, getPricedMobileFeePart,
getServiceabilityDetails
} from '@/helpers/service-location-helper'; } from '@/helpers/service-location-helper';
import { deepClone } from '@/helpers/object-helper.js'; import { deepClone } from '@/helpers/object-helper.js';
import vehicleProtectedQuestion from '@/layouts/schedule-page/service-location/mobile-location-modal-question/vehicle-protected-question/vehicle-protected-question.vue'; import vehicleProtectedQuestion from '@/layouts/schedule-page/service-location/mobile-location-modal-question/vehicle-protected-question/vehicle-protected-question.vue';
@ -273,7 +272,7 @@ export default {
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode); const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
// retrieve serviceability details // retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode); const serviceabilityDetails = await useMainStore().getServiceabilityDetails(serviceZipCode);
// update content related to service zip code // update content related to service zip code
this.$emit('updated-mobile-fee-part', mobileFeePart); this.$emit('updated-mobile-fee-part', mobileFeePart);

View file

@ -86,7 +86,6 @@ const mockProviders = () => Promise.resolve([
jest.mock( jest.mock(
'@/helpers/service-location-helper', '@/helpers/service-location-helper',
() => ({ () => ({
getServiceabilityDetails: jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode)),
getZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)), getZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)),
getMobileZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode)) getMobileZipCodeData: jest.fn((mockServiceZipCode) => mockZipcodeData(mockServiceZipCode))
}) })
@ -147,6 +146,7 @@ function setupMocks({ appointmentType = AppointmentTypeStrings.IN_SHOP }) {
} }
})]; })];
useMainStore().getSafeliteProviders = jest.fn().mockImplementation(() => mockProviders()); useMainStore().getSafeliteProviders = jest.fn().mockImplementation(() => mockProviders());
useMainStore().getServiceabilityDetails = jest.fn((mockServiceZipCode) => mockGetServiceabilityDetails(mockServiceZipCode));
const wrapper = mount(serviceLocation, mountOptions); const wrapper = mount(serviceLocation, mountOptions);
wrapper.vm.$router.navigateWithSpinner = jest.fn(); wrapper.vm.$router.navigateWithSpinner = jest.fn();

View file

@ -129,8 +129,6 @@ import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js'; import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import { import {
getAvailabilityRating,
getServiceabilityDetails,
getZipCodeData, getZipCodeData,
getMobileZipCodeData getMobileZipCodeData
} from '@/helpers/service-location-helper'; } from '@/helpers/service-location-helper';
@ -160,6 +158,12 @@ export default {
}, },
mixins: [baseFormMixin], mixins: [baseFormMixin],
emits: ['appointment-type-changed', 'city-updated', 'in-shop-zip-updated', 'mobile-zip-updated', 'provider-changed'], emits: ['appointment-type-changed', 'city-updated', 'in-shop-zip-updated', 'mobile-zip-updated', 'provider-changed'],
props: {
schedulingInshopAvailabilityRating: {
type: String,
default: 'high'
}
},
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
return { mainStore }; return { mainStore };
@ -178,7 +182,6 @@ export default {
isRecalibrationServiceableMobile: null, isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(), selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(), selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
mobileProviderNumber: null, mobileProviderNumber: null,
zipContainsMilitaryBase: false, zipContainsMilitaryBase: false,
zipCodeCtu: null, zipCodeCtu: null,
@ -198,7 +201,7 @@ export default {
return this.isBigTruck && !this.isServiceableInshop && !this.isServiceableMobile; return this.isBigTruck && !this.isServiceableInshop && !this.isServiceableMobile;
}, },
displayLowAvailabilityInshop() { displayLowAvailabilityInshop() {
return this.availabilityRating === 'low' && this.isInshop; return this.availabilityRating === 'low' && this.isInshop && this.selectedProvider !== null;
}, },
displayMilitaryZipAlert() { displayMilitaryZipAlert() {
return this.zipContainsMilitaryBase && this.isServiceableMobile; return this.zipContainsMilitaryBase && this.isServiceableMobile;
@ -228,8 +231,8 @@ export default {
}, },
isInshop() { isInshop() {
return ( return (
this.selectedAppointmentType === 'Inshop' this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP
|| this.selectedAppointmentType === 'Dropoff' || this.selectedAppointmentType === AppointmentTypeStrings.DROP_OFF
); );
}, },
isMobile() { isMobile() {
@ -307,11 +310,12 @@ export default {
this.$emit('mobile-zip-updated', updateMobileZipServiceLocationObj); this.$emit('mobile-zip-updated', updateMobileZipServiceLocationObj);
} }
}, },
schedulingInshopAvailabilityRating(newRating) {
this.availabilityRating = newRating;
},
selectedAppointmentType(newValue, oldValue) { selectedAppointmentType(newValue, oldValue) {
if (this.isInshop && this.selectedProvider && this.availabilityRating === null) { if (this.isInshop && this.selectedProvider && this.availabilityRating === null) {
this.refreshAvailabilityRating(); const appointmentTypeServiceLocationObj = {
const appointmenTypeServiceLocationObj = {
appointmentType: newValue, appointmentType: newValue,
mobileProviderNumber: null, mobileProviderNumber: null,
provider: this.selectedProvider, provider: this.selectedProvider,
@ -320,9 +324,9 @@ export default {
zipCode: this.zipCode, zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu zipCodeCtu: this.zipCodeCtu
}; };
this.$emit('appointment-type-changed', appointmenTypeServiceLocationObj); this.$emit('appointment-type-changed', appointmentTypeServiceLocationObj);
} else { } else {
const appointmenTypeServiceLocationObj = { const appointmentTypeServiceLocationObj = {
appointmentType: newValue, appointmentType: newValue,
mobileProviderNumber: this.mobileProviderNumber, mobileProviderNumber: this.mobileProviderNumber,
provider: null, provider: null,
@ -331,24 +335,31 @@ export default {
zipCode: this.zipCode, zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu zipCodeCtu: this.zipCodeCtu
}; };
this.$emit('appointment-type-changed', appointmenTypeServiceLocationObj); this.$emit('appointment-type-changed', appointmentTypeServiceLocationObj);
} }
}, },
selectedProvider(newProvider, oldProvider) { selectedProvider(newProvider, oldProvider) {
if (newProvider?.providerNumber !== oldProvider?.providerNumber || this.availabilityRating === null) { if (newProvider?.providerNumber !== oldProvider?.providerNumber) {
const appointmentIsInshop = this.isInshop; const appointmentIsInshop = this.isInshop;
const returnedProvider = { const returnedProvider = {
provider: newProvider, refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null,
refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType,
provider: !this.isMobile ? newProvider : {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
}
}; };
if (appointmentIsInshop && newProvider?.providerNumber != null) { this.$emit('provider-changed', returnedProvider);
this.refreshAvailabilityRating();
}
if (newProvider?.providerNumber !== oldProvider?.providerNumber) {
this.$emit('provider-changed', returnedProvider);
}
} }
} }
}, },
@ -358,34 +369,6 @@ export default {
await this.setData(initialData); await this.setData(initialData);
this.initializingData = false; this.initializingData = false;
}, },
async forwardButtonAction(appointmentType) {
let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null);
if (this.isMobile) {
if (this.mainStore.isNoComp || this.mainStore.isITAC) {
this.mainStore.updateMobileFee(this.mobileFeePart);
}
provider = {
providerNumber: this.mobileProviderNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null
}
};
}
this.mainStore.saveServiceLocation({
state: this.state,
zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu,
appointmentType: appointmentType || this.selectedAppointmentType,
provider
});
},
openModalAction(modalName) { openModalAction(modalName) {
this.$refs[modalName].openModal(); this.$refs[modalName].openModal();
}, },
@ -425,7 +408,7 @@ export default {
this.setCtuForMobile(zipCodeData.zipCodeCtu); this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.zipCode); const serviceabilityDetailsPromise = this.mainStore.getServiceabilityDetails(this.zipCode);
await serviceabilityDetailsPromise.then((result) => { await serviceabilityDetailsPromise.then((result) => {
const details = result.data; const details = result.data;
if (details) { if (details) {
@ -444,24 +427,6 @@ export default {
this.$emit('in-shop-zip-updated', inShopZipServiceLocationObj); this.$emit('in-shop-zip-updated', inShopZipServiceLocationObj);
}); });
}, },
refreshAvailabilityRating() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
getAvailabilityRating(
formattedStartDate,
formattedEndDate,
AppointmentTypeStrings.IN_SHOP,
this.selectedProvider ? this.selectedProvider.providerNumber : null
).then((rating) => {
this.availabilityRating = rating;
}).catch(() => {
this.availabilityRating = null;
});
},
setCtuForMobile(val) { setCtuForMobile(val) {
this.zipCodeCtu = val; this.zipCodeCtu = val;
}, },
@ -481,10 +446,6 @@ export default {
this.setServiceabilityDetails(initialData.serviceabilityDetails); this.setServiceabilityDetails(initialData.serviceabilityDetails);
} }
if (initialData.mobileFeePart) {
this.mobileFeePart = initialData.mobileFeePart;
}
if (initialData.providers) { if (initialData.providers) {
this.setMobileProviderNumber(initialData.providers.mobileProviderNumber); this.setMobileProviderNumber(initialData.providers.mobileProviderNumber);
let foundMatch = false; let foundMatch = false;
@ -501,22 +462,12 @@ export default {
this.selectedProvider = null; this.selectedProvider = null;
} }
} }
if (initialData.glassFees) {
useMainStore().getCombinedQuote(initialData.glassFees)
.then((combinedQuote) => {
this.setGlassFeeItems(combinedQuote);
});
}
}, },
setContainsMilitaryBase(val) { setContainsMilitaryBase(val) {
if (this.zipContainsMilitaryBase !== val) { if (this.zipContainsMilitaryBase !== val) {
this.zipContainsMilitaryBase = val; this.zipContainsMilitaryBase = val;
} }
}, },
setGlassFeeItems(newGlassFeeItems) {
this.mainStore.updateGlassFees(newGlassFeeItems);
},
setMobileProviderNumber(providerNumber) { setMobileProviderNumber(providerNumber) {
this.mobileProviderNumber = providerNumber; this.mobileProviderNumber = providerNumber;
}, },
@ -547,7 +498,7 @@ export default {
this.setCtuForMobile(zipCodeData.zipCodeCtu); this.setCtuForMobile(zipCodeData.zipCodeCtu);
this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase); this.setContainsMilitaryBase(zipCodeData.containsMilitaryBase);
const serviceabilityDetailsPromise = getServiceabilityDetails(this.mobileZipCode); const serviceabilityDetailsPromise = this.mainStore.getServiceabilityDetails(this.mobileZipCode);
await serviceabilityDetailsPromise.then((result) => { await serviceabilityDetailsPromise.then((result) => {
const details = result.data; const details = result.data;
if (details) { if (details) {
@ -636,5 +587,8 @@ $page-side-padding: 1.5rem;
} }
} }
} }
:deep(.alert-shop-distance) {
margin-bottom: 0;
}
} }
</style> </style>

View file

@ -2,6 +2,7 @@
<textboxQuestion <textboxQuestion
ref="zipInputTextQuestion" ref="zipInputTextQuestion"
v-model="internalZipcode" v-model="internalZipcode"
:alwaysEmitFieldErrorOnEvent="true"
:cmsWidgetName="cmsWidgetName" :cmsWidgetName="cmsWidgetName"
inputId="serviceZipCode" inputId="serviceZipCode"
cornerStyle="rounded" cornerStyle="rounded"
@ -9,7 +10,8 @@
isRequired isRequired
:hasError="hasError" :hasError="hasError"
:validationRules="`zip-required|${cmsWidgetName}-zip-format`" :validationRules="`zip-required|${cmsWidgetName}-zip-format`"
@clickEvent="updateModelValue" /> @clickEvent="updateModelValue"
@fieldHasError="handleFieldError" />
</template> </template>
<script> <script>
@ -41,7 +43,7 @@ export default {
default: false default: false
} }
}, },
emits: ['update:modelValue'], emits: ['update:modelValue', 'field-has-error'],
data() { data() {
return { return {
internalZipcode: this.modelValue internalZipcode: this.modelValue
@ -58,6 +60,9 @@ export default {
defineRule(`${this.cmsWidgetName}-zip-format`, regex(/^\d{5}$/, this.serviceZipFormatErrorMessage)); defineRule(`${this.cmsWidgetName}-zip-format`, regex(/^\d{5}$/, this.serviceZipFormatErrorMessage));
}, },
methods: { methods: {
handleFieldError(hasError) {
this.$emit('field-has-error', hasError);
},
updateModelValue() { updateModelValue() {
this.$emit('update:modelValue', this.internalZipcode); this.$emit('update:modelValue', this.internalZipcode);
} }

View file

@ -68,8 +68,7 @@
groupName="chooseShop" groupName="chooseShop"
textPosition="text-start" textPosition="text-start"
isRequired isRequired
validationRules="option-required" validationRules="option-required" />
:additionalButtonData="additionalButtonData" />
<span class="service-zip-prompt"> <span class="service-zip-prompt">
{{ serviceZipPrompt }} {{ serviceZipPrompt }}
</span> </span>
@ -78,7 +77,8 @@
:ref="SERVICE_ZIP_QUESTION_REF_NAME" :ref="SERVICE_ZIP_QUESTION_REF_NAME"
v-model="internalZipcode" v-model="internalZipcode"
customInputId="serviceZipCode" customInputId="serviceZipCode"
:cmsWidgetName="textboxQuestionWidgetName" /> :cmsWidgetName="textboxQuestionWidgetName"
@fieldHasError="handleFieldError" />
<div <div
v-if="errorMessage" v-if="errorMessage"
ref="errorMessageDiv" ref="errorMessageDiv"
@ -108,7 +108,6 @@ import baseMixin from '@/mixins/base-mixin.js';
import showIssLoadingModal from '@/helpers/loading-modal-helper.js'; import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
import { toTitleCase } from '@/helpers/text-helper.js'; import { toTitleCase } from '@/helpers/text-helper.js';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import { getAvailabilityRating } from '@/helpers/service-location-helper';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import widgetFields from '@/constants/cms-widget-fields'; import widgetFields from '@/constants/cms-widget-fields';
import { dropdownVariants, modalPositions } from '@/constants/component-variants'; import { dropdownVariants, modalPositions } from '@/constants/component-variants';
@ -185,22 +184,6 @@ export default {
modalName() { modalName() {
return this.modalWidgetName; return this.modalWidgetName;
}, },
additionalButtonData() {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + 6);
const formattedStartDate = startDate.toISOString().split('T')[0];
const formattedEndDate = endDate.toISOString().split('T')[0];
return {
displayAvailabilityIndicators: false,
availabilityRatingCallback: getAvailabilityRating,
startDate: formattedStartDate,
endDate: formattedEndDate,
shopAppointmentType: 'InshopOrDropoff'
};
},
serviceZipPrompt() { serviceZipPrompt() {
return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2); return this.getCmsContent(this.modalWidgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
}, },
@ -271,6 +254,7 @@ export default {
currentSearchIndex += 1; currentSearchIndex += 1;
} }
if (result.length === 0) { if (result.length === 0) {
this.nearbyShops = [];
this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE; this.errorMessage = errorMessages.ZIP_CODE_NOT_SERVICED_FOR_VEHICLE;
} else { } else {
if (!result.find((shop) => shop.providerNumber === this.internalProviderNumber)) { if (!result.find((shop) => shop.providerNumber === this.internalProviderNumber)) {
@ -344,6 +328,11 @@ export default {
} }
return addressLine2; return addressLine2;
}, },
handleFieldError(hasChildFormFieldError) {
if (hasChildFormFieldError) {
this.errorMessage = '';
}
},
forceServiceZipRerender() { forceServiceZipRerender() {
this.renderServiceZip = false; this.renderServiceZip = false;
this.$nextTick(() => { this.$nextTick(() => {

View file

@ -103,6 +103,7 @@ export default {
const wipersPromise = await store.getWipers(); const wipersPromise = await store.getWipers();
const rainDefensePromise = await store.getRainDefense(); const rainDefensePromise = await store.getRainDefense();
const supportingItemsPromise = await store.getSupportingItems();
const promiseResultMap = [ const promiseResultMap = [
{ {
resultKey: 'cmsContent', resultKey: 'cmsContent',
@ -115,10 +116,18 @@ export default {
{ {
resultKey: 'rainDefense', resultKey: 'rainDefense',
promise: rainDefensePromise promise: rainDefensePromise
},
{
resultKey: 'supportingItems',
promise: supportingItemsPromise
} }
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
if (resultMap.supportingItems) {
useMainStore().updateSupportingItems(resultMap.supportingItems);
}
const { feeItems } = store.order.lineItems; const { feeItems } = store.order.lineItems;
const availableLineItems = []; const availableLineItems = [];

View file

@ -53,7 +53,8 @@ const loaderStub = {
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) { function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
router: { router: {
navigate: jest.fn() navigate: jest.fn(),
navigateBailout: jest.fn()
}, },
route: { route: {
query: { issPage: 'tpa-search' } query: { issPage: 'tpa-search' }
@ -355,7 +356,7 @@ describe('tpa-search.vue', () => {
// Assert // Assert
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0].title).toBe('test shop'); expect(result[0].title).toBe('Test Shop');
expect(result[0].addressLines.length).toBe(2); expect(result[0].addressLines.length).toBe(2);
}); });
@ -395,7 +396,7 @@ describe('tpa-search.vue', () => {
// Assert // Assert
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0].buttonLabel).toBe('auto glass shop'); expect(result[0].buttonLabel).toBe('Auto Glass Shop');
expect(result[0].buttonLabelSubCopy).toBe('5.8 mi'); expect(result[0].buttonLabelSubCopy).toBe('5.8 mi');
expect(result[0].value).toBe('12345'); expect(result[0].value).toBe('12345');
}); });
@ -616,7 +617,7 @@ describe('tpa-search.vue', () => {
const result = wrapper.vm.getShopButtonDataFromProvider(provider); const result = wrapper.vm.getShopButtonDataFromProvider(provider);
// Assert // Assert
expect(result.buttonLabel).toBe('test shop'); expect(result.buttonLabel).toBe('Test Shop');
expect(result.buttonLabelSubCopy).toBe('5.8 mi'); expect(result.buttonLabelSubCopy).toBe('5.8 mi');
expect(result.value).toBe('12345'); expect(result.value).toBe('12345');
expect(result.buttonBodyCopy).toContain('<br>'); expect(result.buttonBodyCopy).toContain('<br>');
@ -787,7 +788,7 @@ describe('tpa-search.vue', () => {
}); });
describe('needHelpLinkClick', () => { describe('needHelpLinkClick', () => {
test('sets bailout and navigates', () => { test('navigatesBailout', () => {
// Arrange // Arrange
const { wrapper } = getMountedComponent(); const { wrapper } = getMountedComponent();
@ -795,11 +796,7 @@ describe('tpa-search.vue', () => {
wrapper.vm.needHelpLinkClick(); wrapper.vm.needHelpLinkClick();
// Assert // Assert
expect(wrapper.vm.mainStore.setBailout).toHaveBeenCalled(); expect(wrapper.vm.$router.navigateBailout).toBeCalled();
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_NEED_HELP,
expect.anything()
);
}); });
}); });

View file

@ -165,7 +165,9 @@ export default {
}, },
mixins: [BaseFormMixin], mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
const zipCode = useMainStore().order.customer.address.zipCode; const serviceZipCode = useMainStore().order.serviceLocation.zipCode;
const customerZipCode = useMainStore().order.customer.address.zipCode;
const zipCode = serviceZipCode || customerZipCode;
const pageData = useMainStore().pageData(to.query.issPage); const pageData = useMainStore().pageData(to.query.issPage);
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const providersPromise = useMainStore().getTpaAndSafeliteProviders(zipCode, pageData?.tpaSearchValue ?? ''); const providersPromise = useMainStore().getTpaAndSafeliteProviders(zipCode, pageData?.tpaSearchValue ?? '');
@ -265,7 +267,7 @@ export default {
}, },
providerAddresses() { providerAddresses() {
return this.radiusFilteredAndCappedProviders?.map((provider) => ({ return this.radiusFilteredAndCappedProviders?.map((provider) => ({
title: provider.companyName.toLowerCase(), title: toTitleCase(provider.companyName),
fullAddress: this.getFullProviderAddress(provider), fullAddress: this.getFullProviderAddress(provider),
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)] addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
})) ?? []; })) ?? [];
@ -295,7 +297,7 @@ export default {
const provider = this.providers?.find((p) => p.providerNumber === newNumber); const provider = this.providers?.find((p) => p.providerNumber === newNumber);
if (provider && this.dataLoaded) { if (provider && this.dataLoaded) {
useMainStore().updateServiceLocation({ useMainStore().updateServiceLocation({
zipCode: this.zipCode, zipCode: this.mapZipCode,
provider: { provider: {
providerNumber: provider?.providerNumber, providerNumber: provider?.providerNumber,
address: { address: {
@ -358,11 +360,7 @@ export default {
return getTpaProvidersResult?.data ?? []; return getTpaProvidersResult?.data ?? [];
}, },
needHelpLinkClick() { needHelpLinkClick() {
this.mainStore.setBailout(bailoutMessage.RequestCallback()); this.$router.navigateBailout(bailoutMessage.RequestCallback());
this.$router.navigate(
this.navigationScenarios.CLICKED_NEED_HELP,
this.$route
);
}, },
async searchClick() { async searchClick() {
if (isNaN(this.tpaSearchValue)) { if (isNaN(this.tpaSearchValue)) {
@ -395,7 +393,7 @@ export default {
: null; : null;
return { return {
buttonLabel: provider?.companyName.toLowerCase() ?? '', buttonLabel: toTitleCase(provider?.companyName) ?? '',
buttonLabelSubCopy: distance === null ? '' : `${distance} mi`, buttonLabelSubCopy: distance === null ? '' : `${distance} mi`,
buttonBodyCopy: `${this.getFullProviderAddress(provider)}<br>${ buttonBodyCopy: `${this.getFullProviderAddress(provider)}<br>${
cellNumber ?? '' cellNumber ?? ''

View file

@ -99,7 +99,7 @@ import buttonMain from '@/ux-components/button-main/button-main.vue';
import textBlock from '@/digital-components/text-block/text-block.vue'; import textBlock from '@/digital-components/text-block/text-block.vue';
import reviewBlock from '@/layouts/tpa-submit/review-block/review-block.vue'; import reviewBlock from '@/layouts/tpa-submit/review-block/review-block.vue';
import deductibleBox from '@/layouts/tpa-submit/deductible-box/deductible-box.vue'; import deductibleBox from '@/layouts/tpa-submit/deductible-box/deductible-box.vue';
import contactDetailsDrawer from '@/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue'; import contactDetailsDrawer from '@/iss-components/contact-details-drawer/contact-details-drawer.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import alert from '@/ux-components/alert/alert.vue'; import alert from '@/ux-components/alert/alert.vue';
import modal from '@/digital-components/modal/modal.vue'; import modal from '@/digital-components/modal/modal.vue';
@ -315,20 +315,8 @@ export default {
}; };
}, },
async forwardButtonAction() { async forwardButtonAction() {
try { await submitWorkOrder({ submitType: submitType.TPA })
await submitWorkOrder({ submitType: submitType.TPA }).then(() => { this.navigate(this.navigationScenarios.CLICKED_FORWARD);
this.navigate(this.navigationScenarios.CLICKED_FORWARD);
}).catch((submitError) => {
this.mainStore.setBailout(bailoutMessage.saveSessionError(submitError.data));
this.navigate(
this.navigationScenarios.SAVE_SESSION_FAILED,
this.$route,
{ issPage: this.issPageValues.TPA_SUBMIT }
);
});
} catch (error) {
console.error(`error: response from submit work order:${error.message}`);
}
}, },
navigate(scenario) { navigate(scenario) {
this.$router.navigate(scenario, this.$route); this.$router.navigate(scenario, this.$route);

View file

@ -139,7 +139,7 @@ describe('vehicle-damage.vue', () => {
expect(vehicleQuestionsMixin.methods.getPartsOrQuestions).toHaveBeenCalledTimes(1); expect(vehicleQuestionsMixin.methods.getPartsOrQuestions).toHaveBeenCalledTimes(1);
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalledTimes(1); expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalledTimes(1);
}); });
test('Error in getPartsOrQuestions call => bailout true and navigate forward with CLICKED_FORWARD_WITH_BAILOUT scenario', async () => { test('Error in getPartsOrQuestions call => error thrown in forwardButtonAction', async () => {
mountOptions.global.plugins = [createTestingPinia({ mountOptions.global.plugins = [createTestingPinia({
initialState: { initialState: {
main: { main: {
@ -154,23 +154,21 @@ describe('vehicle-damage.vue', () => {
} }
} }
})]; })];
mountOptions.data = () => ({ mountOptions.data = () => ({});
hasBailedOut: true
});
const wrapper = mount(VehicleDamageComponent, mountOptions); const wrapper = mount(VehicleDamageComponent, mountOptions);
const siteFooterWrapper = wrapper.getComponent({ ref: 'siteFooter' });
const partsQuestionsErrorResponse = { const partsQuestionsErrorResponse = {
error: 'Error getting parts' error: 'Error getting parts'
}; };
vehicleQuestionsMixin.methods.getPartsOrQuestions.mockImplementation(() => ( vehicleQuestionsMixin.methods.getPartsOrQuestions.mockImplementation(() => (
partsQuestionsErrorResponse Promise.reject(partsQuestionsErrorResponse)
)); ));
siteFooterWrapper.vm.$emit('forwardClicked');
await flushPromises(); expect.assertions(1);
expect(mockRouter.navigate).toHaveBeenCalledTimes(1); try {
expect(mockRouter.navigate) await wrapper.vm.forwardButtonAction();
.toHaveBeenCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT, mockRoute); } catch (e) {
expect(e).toBe(partsQuestionsErrorResponse);
}
}); });
}); });

View file

@ -189,8 +189,7 @@ export default {
this.getPassengerSideReplaceOptionsFromStore() this.getPassengerSideReplaceOptionsFromStore()
}, },
selectedWindshieldOptions: this.getWindshieldOptionsFromStore(), selectedWindshieldOptions: this.getWindshieldOptionsFromStore(),
selectedRearReplaceOptions: this.getRearReplaceOptionsFromStore(), selectedRearReplaceOptions: this.getRearReplaceOptionsFromStore()
hasBailedOut: false
}; };
}, },
computed: { computed: {
@ -429,23 +428,12 @@ export default {
// If vin already exists or not replacing windshield, get parts/questions and navigate forward // If vin already exists or not replacing windshield, get parts/questions and navigate forward
const partsOrQuestionsResponse = await this.getPartsOrQuestions(); const partsOrQuestionsResponse = await this.getPartsOrQuestions();
if (partsOrQuestionsResponse.error) {
this.mainStore.setBailout(bailoutMessage.PartsServiceError(partsOrQuestionsResponse.error.data));
window.console.error('Error on retrieving PartsOrQuestions');
this.hasBailedOut = true;
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
// Comes from vehicleQuestionsMixin.navigateForward() // Comes from vehicleQuestionsMixin.navigateForward()
if (!this.hasBailedOut) { await this.navigateForward(
await this.navigateForward( partsOrQuestionsResponse.data.partsOrQuestions,
partsOrQuestionsResponse.data.partsOrQuestions, this
this );
);
}
} else { } else {
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,

View file

@ -113,21 +113,10 @@ export default {
break; break;
case vinLookupMethodSelections.NOVIN: case vinLookupMethodSelections.NOVIN:
const partsOrQuestionsResponse = await this.getPartsOrQuestions(); const partsOrQuestionsResponse = await this.getPartsOrQuestions();
if (partsOrQuestionsResponse.error) { await this.navigateForward(
this.mainStore.setBailout(bailoutMessage.PartsServiceError(partsOrQuestionsResponse.error.data)); partsOrQuestionsResponse.data.partsOrQuestions,
window.console.error('Error on retrieving PartsOrQuestions'); this
this.hasBailedOut = true; );
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
else {
await this.navigateForward(
partsOrQuestionsResponse.data.partsOrQuestions,
this
);
}
break; break;
default: default:
break; break;

View file

@ -8,7 +8,7 @@
v-for="(tint, index) in tintSelectionOptions" v-for="(tint, index) in tintSelectionOptions"
:key="index"> :key="index">
<div class="tint-option"> <div class="tint-option">
<span>{{ `${partType} - ${tint}` }}</span> <div class="tint-option-text">{{ `${partType} - ${tint}` }}</div>
<span :class="`tint-image ${convertTintToCSSClass(tint)}`"></span> <span :class="`tint-image ${convertTintToCSSClass(tint)}`"></span>
</div> </div>
<buttonQuestion <buttonQuestion
@ -59,7 +59,7 @@ export default {
partValidationRules() { partValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`; const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED)); defineRule(validationRuleName, required(`${this.partType} not selected`));
return validationRuleName; return validationRuleName;
}, },
@ -155,7 +155,7 @@ export default {
return tint.toLowerCase().replaceAll(' ', '-').replaceAll(',', ''); return tint.toLowerCase().replaceAll(' ', '-').replaceAll(',', '');
}, },
toTitleCaseWithExceptions(text) { toTitleCaseWithExceptions(text) {
const exceptions = ['side', 'glass', 'dimming']; const exceptions = ['side', 'glass', 'dimming', 'BPassenger', 'soundproofing'];
let temp = toTitleCase(text); let temp = toTitleCase(text);
exceptions.forEach((exception) => { exceptions.forEach((exception) => {
const regEx = new RegExp(exception, 'ig'); const regEx = new RegExp(exception, 'ig');
@ -176,7 +176,8 @@ export default {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
height: 2rem; position: relative;
min-height: 2rem;
margin-top: .5rem; margin-top: .5rem;
margin-bottom: .25rem; margin-bottom: .25rem;
font-weight: 600; font-weight: 600;
@ -185,19 +186,48 @@ export default {
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
justify-content: flex-start; justify-content: flex-start;
} }
.tint-option-text {
padding-right: 2.5rem;
}
} }
.radioQuestion { .radioQuestion {
:deep(.list-button) { :deep(.list-button) {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
&:not(:last-child) {
:deep(.form-test-error) {
display: none;
}
}
&:last-child {
:deep(.form-test-error) {
position: absolute;
margin: -10px 0 10px 10px;
font-weight: 600;
> * {
padding: 0;
}
}
}
} }
.tint-image { .tint-image {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
margin-left: 1rem;
margin-top: -.25rem;
background-repeat: no-repeat; background-repeat: no-repeat;
background-image: url(~@/assets/img/tints/GlassTint.png); background-image: url(~@/assets/img/tints/GlassTint.png);
position: absolute;
top: -.25rem;
right: 1rem;
@include media-breakpoint-up(md) {
position: relative;
top: -.25rem;
right: 1.5rem;
float: left;
}
} }
.blue-tint-green-shade { .blue-tint-green-shade {
background-position: -53px -53px; background-position: -53px -53px;

View file

@ -64,6 +64,7 @@ import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin';
import BaseFormMixin from '@/mixins/base-form-mixin.js'; import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import widgetFields from '@/constants/cms-widget-fields'; import widgetFields from '@/constants/cms-widget-fields';
import bailoutMessage from '@/constants/bailoutMessage';
export default { export default {
name: 'vehicle-parts', name: 'vehicle-parts',
@ -213,8 +214,7 @@ export default {
}); });
}, },
requestCallbackBailout() { requestCallbackBailout() {
this.mainStore.setBailout(bailoutMessage.RequestCallback()); this.$router.navigateBailout(bailoutMessage.RequestCallback());
this.$router.navigate(navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT, this.$route);
} }
} }
}; };

View file

@ -163,16 +163,18 @@ export default {
} }
}, },
selectedStyle(value) { selectedStyle(value) {
this.resetAlert(); if (value) {
this.mainStore.updateVehicleStyle(value); this.resetAlert();
this.mainStore.setVehicle( this.mainStore.updateVehicleStyle(value);
this.selectedYear, this.mainStore.setVehicle(
this.selectedMake, this.selectedYear,
this.selectedModel, this.selectedMake,
this.selectedStyle this.selectedModel,
).then((result) => { this.selectedStyle
this.displayNoServiceAlert = !result.data.canSafeliteService; ).then((result) => {
}); this.displayNoServiceAlert = !result.data.canSafeliteService;
});
}
} }
}, },
mounted() { mounted() {
@ -206,77 +208,25 @@ export default {
}, },
navigateForward() { navigateForward() {
this.mainStore.setVehicle().then( this.mainStore.setVehicle().then(() => {
() => {
if (this.mainStore.isBailout) {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
return;
}
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD, this.navigationScenarios.CLICKED_FORWARD,
this.$route this.$route
); );
},
(error) => {
this.mainStore.setBailout(bailoutMessage.vehicleYMMSLookupError(this.mainStore.vehicle.year, this.mainStore.vehicle.make, this.mainStore.vehicle.model, this.mainStore.vehicle.style, error));
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
} }
); );
}, },
async updateYearValues() { async updateYearValues() {
return this.mainStore.getVehicleYears().then( return this.mainStore.getVehicleYears();
(response) => response,
(error) => {
this.mainStore.setBailout(bailoutMessage.vehicleYMMSLookupError(null, null, null, null, error));
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
);
}, },
async updateMakeValues() { async updateMakeValues() {
return this.mainStore.getVehicleMakes().then( return this.mainStore.getVehicleMakes();
(response) => response,
(error) => {
this.mainStore.setBailout(bailoutMessage.vehicleYMMSLookupError(this.mainStore.vehicle.year, null, null, null, error));
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
);
}, },
async updateModelValues() { async updateModelValues() {
return this.mainStore.getVehicleModels().then( return this.mainStore.getVehicleModels();
(response) => response,
(error) => {
this.mainStore.setBailout(bailoutMessage.vehicleYMMSLookupError(this.mainStore.vehicle.year, this.mainStore.vehicle.make, null, null, error));
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
);
}, },
async updateStyleValues() { async updateStyleValues() {
return this.mainStore.getVehicleStyles().then( return this.mainStore.getVehicleStyles();
(response) => response,
(error) => {
this.mainStore.setBailout(bailoutMessage.vehicleYMMSLookupError(this.mainStore.vehicle.year, this.mainStore.vehicle.make, this.mainStore.vehicle.model, null, error));
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
);
}, },
resetAlert() { resetAlert() {
this.displayNoServiceAlert = false; this.displayNoServiceAlert = false;

View file

@ -148,7 +148,8 @@ const mockRoute = {
}; };
const mockRouter = { const mockRouter = {
navigate: jest.fn(), navigate: jest.fn(),
navigateWithSpinner: jest.fn() navigateWithSpinner: jest.fn(),
navigateBailout: jest.fn()
}; };
const maska = jest.fn(); const maska = jest.fn();
jest.mock('@/helpers/damage-helper', () => ({ jest.mock('@/helpers/damage-helper', () => ({
@ -553,45 +554,6 @@ describe('vin-lookup.vue', () => {
); );
}); });
}); });
test(
'Error in getPartsOrQuestions call => bailout true and navigate forward with CLICKED_FORWARD_WITH_BAILOUT scenario',
async () => {
const user = userEvent.setup();
mountOptions.global.stubs.vinQuestion = false;
mountOptions.data = () => ({
vinWithNonMatchingCarId: false,
isCarIdDifferentFromTheStore: false,
vin: mockValidVin,
hasBailedOut: true
});
getPartsOrQuestions.mockResponse = partsOrQuestionsErrorMockResponse;
jest.spyOn(VinLookupComponent.methods, lookupVehicleByVin.methodName)
.mockResolvedValue(lookupVehicleByVin.mockResponse);
jest.spyOn(vehicleQuestionsMixin.methods, getPartsOrQuestions.methodName)
.mockResolvedValue(getPartsOrQuestions.mockResponse);
const { container } = render(VinLookupComponent, mountOptions);
const vinInput = container.querySelector(vinInputSelector);
await user.type(vinInput, mockValidVin);
const continueButton = container.querySelector(continueButtonQuerySelector);
await user.click(continueButton);
await flushPromises();
await waitFor(() => {
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate)
.toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
mockRoute
);
});
}
);
}); });
}); });
}); });

View file

@ -112,8 +112,7 @@ export default {
vinWithNonMatchingCarId: vin?.length > 0 && !this.hasValidCarId(), vinWithNonMatchingCarId: vin?.length > 0 && !this.hasValidCarId(),
vin, vin,
forwardButtonCarStyle: '', forwardButtonCarStyle: '',
vinPopulatedOnPageLoad: vin?.length > 0 && this.hasValidCarId(), vinPopulatedOnPageLoad: vin?.length > 0 && this.hasValidCarId()
hasBailedOut: false
}; };
}, },
computed: { computed: {
@ -176,35 +175,37 @@ export default {
showIssLoadingModal(true); showIssLoadingModal(true);
if (this.needToLookupVehicle) { if (this.needToLookupVehicle) {
const vehicleLookupResponse = await this.lookupVehicleByVin(this.vin); try {
const vehicleLookupResponse = await this.lookupVehicleByVin(this.vin);
if (vehicleLookupResponse.error) { if (!vehicleLookupResponse.data.canSafeliteService) {
this.activeVehicleLookupAlertType = vehicleLookupAlertTypes.NOT_FOUND; this.activeVehicleLookupAlertType = vehicleLookupAlertTypes.NO_SERVICE;
this.mainStore.setBailout(bailoutMessage.vehicleNotFound(this.vin)); this.resetVehicleFromLookup();
this.resetVehicleFromLookup(); this.$refs.siteFooter.disableForwardButton();
// Temp solution to turn on 'disabled' style on the Continue button showIssLoadingModal(false);
// because the form itself actually passes its client-side validation. return;
// SSR-189 Scenario #4.
this.$refs.siteFooter.enableForwardAction();
showIssLoadingModal(false);
return;
}
if (!vehicleLookupResponse.data.canSafeliteService) {
this.activeVehicleLookupAlertType = vehicleLookupAlertTypes.NO_SERVICE;
this.resetVehicleFromLookup();
this.$refs.siteFooter.disableForwardButton();
showIssLoadingModal(false);
return;
}
// Add vin bcs the response from the service doesn't contain vin
this.vehicleFromLookup = Object.assign(
vehicleLookupResponse.data,
{
vin: this.vin
} }
);
// Add vin bcs the response from the service doesn't contain vin
this.vehicleFromLookup = Object.assign(
vehicleLookupResponse.data,
{
vin: this.vin
}
);
} catch (e) {
if (e.status === 404) {
this.activeVehicleLookupAlertType = vehicleLookupAlertTypes.NOT_FOUND;
this.resetVehicleFromLookup();
// Temp solution to turn on 'disabled' style on the Continue button
// because the form itself actually passes its client-side validation.
// SSR-189 Scenario #4.
this.$refs.siteFooter.enableForwardAction();
showIssLoadingModal(false);
return;
}
throw e;
}
} }
if (this.needToLookupVehicle && this.isCarIdDifferentFromTheStore) { if (this.needToLookupVehicle && this.isCarIdDifferentFromTheStore) {
@ -259,34 +260,15 @@ export default {
} }
const partsOrQuestionsResponse = await this.getPartsOrQuestions(); const partsOrQuestionsResponse = await this.getPartsOrQuestions();
if (partsOrQuestionsResponse.error) {
this.mainStore.setBailout(bailoutMessage.PartsServiceError(partsOrQuestionsResponse.error.data));
window.console.error('Error on retrieving PartsOrQuestions');
this.hasBailedOut = true;
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
}
// Comes from vehicleQuestionsMixin.navigateForward() // Comes from vehicleQuestionsMixin.navigateForward()
if (!this.hasBailedOut) { await this.navigateForward(
await this.navigateForward( partsOrQuestionsResponse.data.partsOrQuestions,
partsOrQuestionsResponse.data.partsOrQuestions, this
this );
);
}
}, },
async lookupVehicleByVin(vin) { async lookupVehicleByVin(vin) {
try { return this.mainStore.lookupVehicleByVin(vin);
return await this.mainStore.lookupVehicleByVin(vin);
} catch (responseError) {
return {
error: {
status: responseError.status
}
};
}
}, },
resetActiveAlert() { resetActiveAlert() {
this.activeVehicleLookupAlertType = null; this.activeVehicleLookupAlertType = null;

View file

@ -72,7 +72,8 @@ function setupMocks({
const mockDataMountOptions = { const mockDataMountOptions = {
...mountOptionsMockData, ...mountOptionsMockData,
router: { router: {
navigate: jest.fn() navigate: jest.fn(),
navigateBailout: jest.fn()
} }
}; };
@ -166,7 +167,7 @@ describe('navigation', () => {
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.mainStore.getDuplicateReferrals = jest.fn().mockImplementation(() => Promise.resolve({})); wrapper.vm.mainStore.getDuplicateReferrals = jest.fn().mockImplementation(() => Promise.resolve({}));
wrapper.vm.mainStore.applicationUser.duplicateOrders = [{ test: 'a' }]; wrapper.vm.mainStore.applicationUser.duplicateOrders = [{ test: 'a' }];
wrapper.vm.mainStore.validateZip = jest.fn().mockImplementation(() => Promise.reject()); wrapper.vm.mainStore.validateZip = jest.fn().mockImplementation(() => Promise.reject({ isAxiosError: false }));
// Act // Act
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
@ -316,10 +317,14 @@ describe('navigation', () => {
})); }));
// Act // Act
await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect.assertions(1);
try {
await wrapper.vm.forwardButtonAction();
} catch (e) {
expect(e).toMatch(error);
}
}); });
}); });
}); });

View file

@ -177,6 +177,7 @@ import { getPropertyCaseInsensitive } from '@/helpers/object-helper';
import { saveSession } from '@/helpers/order-helper'; import { saveSession } from '@/helpers/order-helper';
import { getISSCookie } from '@/helpers/cookie-helper.js'; import { getISSCookie } from '@/helpers/cookie-helper.js';
import bailoutMessage from '@/constants/bailoutMessage'; import bailoutMessage from '@/constants/bailoutMessage';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
// define validation rules // define validation rules
defineRule( defineRule(
@ -303,54 +304,45 @@ export default {
}, },
methods: { methods: {
async forwardButtonAction() { async forwardButtonAction() {
try { this.mainStore.updatePolicyData(this.welcomePageModel);
this.mainStore.updatePolicyData(this.welcomePageModel);
const promises = [];
promises.push(this.configureZip().then(async () => await this.mainStore.getBillToInfo()));
// Clear duplicate orders if navigating away from the welcome page after visiting duplicate check page. // We want to await this separately. It's quick and if this has an invalid zip we don't want to be waiting on the slower API calls
if (this.mainStore.order.visitedDuplicateCheckPage) { await this.configureZip();
this.mainStore.clearDuplicateOrders(); if (this.displayInvalidZipAlert) {
} showIssLoadingModal(false);
return;
// Skip duplicate check if loaded from cookie or already visited duplicate check page.
if (!this.mainStore.order.loadedFromCookie && !this.mainStore.order.visitedDuplicateCheckPage) {
promises.push(this.mainStore.getDuplicateReferrals());
}
promises.push(this.mainStore.getCoveragePolicyInfo());
await Promise.allSettled(promises);
} catch (e) {
console.error(e);
// TODO: Bailout?
} finally {
if (!this.displayInvalidZipAlert) {
await saveSession({ shouldAwaitSaveSessionQueue: true })
.catch((error) => {
this.mainStore.setBailout(bailoutMessage.saveSessionError(error.data));
})
.finally(() => this.navigateForward());
}
} }
// Clear duplicate orders if navigating away from the welcome page after visiting duplicate check page.
if (this.mainStore.order.visitedDuplicateCheckPage) {
this.mainStore.clearDuplicateOrders();
}
const promises = [
this.mainStore.getBillToInfo()
];
// Skip duplicate check if loaded from cookie or already visited duplicate check page.
if (!this.mainStore.order.loadedFromCookie && !this.mainStore.order.visitedDuplicateCheckPage) {
promises.push(this.mainStore.getDuplicateReferrals());
}
promises.push(this.mainStore.getCoveragePolicyInfo());
await Promise.all(promises);
await saveSession({ shouldAwaitSaveSessionQueue: true, bailoutOnError: true })
this.navigateForward();
}, },
async configureZip() { async configureZip() {
try { try {
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
await this.mainStore.validateZip({ zip: this.welcomePageModel.policyZipCode }); await this.mainStore.validateZip({ zip: this.welcomePageModel.policyZipCode });
return Promise.resolve();
} catch (e) { } catch (e) {
if (e.isAxiosError) {
throw e;
}
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
return Promise.reject(e);
} }
}, },
navigateForward() { navigateForward() {
if (this.mainStore.isBailout) { if ((this.mainStore.applicationUser.duplicateOrders?.length > 0 ?? false)
this.$router.navigate(
this.navigationScenarios.SAVE_SESSION_FAILED,
this.$route,
{},
{ [routerParams.SKIP_SAVE_SESSION]: true }
);
} else if ((this.mainStore.applicationUser.duplicateOrders?.length > 0 ?? false)
&& !this.answeredContinueModal) { && !this.answeredContinueModal) {
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_DUPLICATES, this.navigationScenarios.CLICKED_FORWARD_WITH_DUPLICATES,
@ -445,12 +437,7 @@ export default {
.then((response) => { .then((response) => {
this.answeredContinueModal = true; this.answeredContinueModal = true;
if (response && !response.provider?.isSafeliteProvider) { if (response && !response.provider?.isSafeliteProvider) {
this.mainStore.setBailout(bailoutMessage.SafeliteNotTheProvider()); this.$router.navigateBailout(bailoutMessage.SafeliteNotTheProvider());
this.hasBailedOut = true;
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
this.$route
);
} else { } else {
this.updateWelcomePageModel(response); this.updateWelcomePageModel(response);
} }

View file

@ -48,7 +48,7 @@ function getPageName(vm) {
// Vue Error Handling // Vue Error Handling
vueApp.config.errorHandler = (err, vm, info) => { vueApp.config.errorHandler = (err, vm, info) => {
const pageName = getPageName(vm); const pageName = getPageName(vm);
global.$logger.logError(`Page Name - ${pageName} - ${info}: ${err.message}\n${err.stack}`); global.$logger.logError(`[${pageName}] ${info}`, err);
if (applicationConfig.BAILOUT_ON_APPLICATION_ERROR) { if (applicationConfig.BAILOUT_ON_APPLICATION_ERROR) {
router.navigateBailout(bailoutMessage.applicationError(`[${pageName}] ${info}: ${err.message}\n${err.stack}`)); router.navigateBailout(bailoutMessage.applicationError(`[${pageName}] ${info}: ${err.message}\n${err.stack}`));
} }
@ -56,7 +56,7 @@ vueApp.config.errorHandler = (err, vm, info) => {
// Vue Router Error Handling // Vue Router Error Handling
router.onError((err) => { router.onError((err) => {
global.$logger.logError(err.message, err.cause); global.$logger.logError('Router Error:', err);
if (applicationConfig.BAILOUT_ON_ROUTER_ERROR) { if (applicationConfig.BAILOUT_ON_ROUTER_ERROR) {
router.navigateBailout(bailoutMessage.routerError(`${err.message}\n${err.stack}`)); router.navigateBailout(bailoutMessage.routerError(`${err.message}\n${err.stack}`));
} }

View file

@ -353,16 +353,7 @@ export default {
}, },
async getPartsOrQuestions() { async getPartsOrQuestions() {
try { return useMainStore().getPartsOrQuestions();
return await useMainStore().getPartsOrQuestions();
} catch (responseError) {
return {
error: {
status: responseError.status,
data: responseError.data
}
};
}
}, },
// Can't use `this` because navigateForward is also called from vin-pages-mixin // Can't use `this` because navigateForward is also called from vin-pages-mixin

View file

@ -25,100 +25,95 @@ const routes = [
path: '/', path: '/',
name: 'root', name: 'root',
async beforeEnter(to, from, next) { async beforeEnter(to, from, next) {
try { const issPageToUse = !to.query.issPage ? issPageValues.WELCOME_PAGE : to.query.issPage;
const issPageToUse = !to.query.issPage ? issPageValues.WELCOME_PAGE : to.query.issPage; const fromQueryPage = from.query?.issPage;
const fromQueryPage = from.query?.issPage;
if ((issPageToUse === issPageValues.ACCESS_DENIED if ((issPageToUse === issPageValues.ACCESS_DENIED
|| (issPageToUse !== issPageValues.ENTRY_PAGE && !useMainStore().issConfig.parentAccountNumber)) || (issPageToUse !== issPageValues.ENTRY_PAGE && !useMainStore().issConfig.parentAccountNumber))
&& process.env.VUE_APP_CURRENT_ENVIRONMENT !== 'Localhost' && process.env.VUE_APP_CURRENT_ENVIRONMENT !== 'Localhost'
) { ) {
return await GoToAccessIsDenied(next); return await GoToAccessIsDenied(next);
}
// Do not run these for the main entry page - as it is not part of the user flow.
if (issPageToUse !== issPageValues.ENTRY_PAGE) {
if (analyticsMixin.methods.noSession()) {
await analyticsMixin.methods.initSession();
} else {
updateSessionIdCookie();
} }
// Do not run these for the main entry page - as it is not part of the user flow. await runExperiments(issPageToUse); // fmg has this further down
if (issPageToUse !== issPageValues.ENTRY_PAGE) { }
if (analyticsMixin.methods.noSession()) {
await analyticsMixin.methods.initSession();
} else {
updateSessionIdCookie();
}
await runExperiments(issPageToUse); // fmg has this further down // Intercept all navigation if a submitted order exists in storage
if (useMainStore().hasSubmittedOrder()) {
if (to.query.issPage !== issPageValues.ENTRY_PAGE) {
return await GoToConfirmationPage(next, useMainStore().getSubmittedOrder());
}
}
// If the saved session has timed out, clear the session, execute 404 logic.
if (getISSCookie() !== null && !isSavedSessionStillActive()) {
// await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
await GoToStartOn404(next);
}
// Process ISS cookie.
// Skip if Entry Page or Refreshing Welcome page
if (issPageToUse !== issPageValues.ENTRY_PAGE
&& !(issPageToUse === issPageValues.WELCOME_PAGE && fromQueryPage === undefined)) {
updateOrCreateISSCookie();
}
if (router.hasRoute(issPageToUse)) {
// Since our route is already in scope, we can grab the component and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === issPageToUse)[0].components;
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
} }
// Intercept all navigation if a submitted order exists in storage if (!arePagePrerequisitesValid(component)) {
if (useMainStore().hasSubmittedOrder()) {
if (to.query.issPage !== issPageValues.ENTRY_PAGE) {
return await GoToConfirmationPage(next, useMainStore().getSubmittedOrder());
}
}
// If the saved session has timed out, clear the session, execute 404 logic.
if (getISSCookie() !== null && !isSavedSessionStillActive()) {
// await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
await GoToStartOn404(next); await GoToStartOn404(next);
} }
// Process ISS cookie. return next({name: issPageToUse, query: to.query, params: to.params});
// Skip if Entry Page or Refreshing Welcome page
if (issPageToUse !== issPageValues.ENTRY_PAGE
&& !(issPageToUse === issPageValues.WELCOME_PAGE && fromQueryPage === undefined)) {
updateOrCreateISSCookie();
}
if (router.hasRoute(issPageToUse)) {
// Since our route is already in scope, we can grab the component and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === issPageToUse)[0].components;
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
}
if (!arePagePrerequisitesValid(component)) {
await GoToStartOn404(next);
}
return next({ name: issPageToUse, query: to.query, params: to.params });
}
const routeData = await GetRouteInfoFromPageName(issPageToUse);
if (routeData[0].name.toLowerCase() === 'error') {
throw new Error('Page not found!');
}
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component
});
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router
.getRoutes()
.filter((x) => x.name === routeData[0].name)[0]
.components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
const tempMsgCopy = 'Pre Requisites failed, need to handle. Default is Welcome Page.';
const tempMsgHeadline = `${issPageToUse}: pre-req failed...`;
await GoToStartOn404(next, tempMsgCopy, tempMsgHeadline);
}
// Assign current query string parameters, as well as our issPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { issPage: routeData[0].name }),
params: to.params
});
} catch (error) {
window.console.warn(error);
await GoToStartOn404(next);
} }
const routeData = await GetRouteInfoFromPageName(issPageToUse);
if (routeData[0].name.toLowerCase() === 'error') {
throw new Error('Page not found!');
}
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component
});
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router
.getRoutes()
.filter((x) => x.name === routeData[0].name)[0]
.components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
const tempMsgCopy = 'Pre Requisites failed, need to handle. Default is Welcome Page.';
const tempMsgHeadline = `${issPageToUse}: pre-req failed...`;
await GoToStartOn404(next, tempMsgCopy, tempMsgHeadline);
}
// Assign current query string parameters, as well as our issPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, {issPage: routeData[0].name}),
params: to.params
});
return null; return null;
} }
} }
@ -177,7 +172,7 @@ router.afterEach(async (to, from) => {
const skipSaveSession = !!router.options.history.state[routerParams.SKIP_SAVE_SESSION]; const skipSaveSession = !!router.options.history.state[routerParams.SKIP_SAVE_SESSION];
if (from.name !== undefined && !skipSaveSession && !store.hasSubmittedOrder()) { if (from.name !== undefined && !skipSaveSession && !store.hasSubmittedOrder()) {
await saveSession({}); await saveSession({ bailoutOnError: from.name === issPageValues.ENTRY_PAGE});
} }
if (to.query.issPage !== issPageValues.ENTRY_PAGE) { if (to.query.issPage !== issPageValues.ENTRY_PAGE) {
@ -350,17 +345,12 @@ router.navigateBailout = (bailoutData = null) => {
// Get navigation map depending on the scenario and the current 'page' you're on. // Get navigation map depending on the scenario and the current 'page' you're on.
function getNavigationMap(scenario, currentRoute) { function getNavigationMap(scenario, currentRoute) {
const issPageValue = currentRoute.query.issPage; const issPageValue = currentRoute.query.issPage;
try { const matchedQueryValue = routingTable(useMainStore())
const matchedQueryValue = routingTable(useMainStore()) .filter((item) => item.issPageValue === issPageValue && item.maps.filter((map) => map.scenario === scenario).length > 0);
.filter((item) => item.issPageValue === issPageValue && item.maps.filter((map) => map.scenario === scenario).length > 0);
const maps = matchedQueryValue ? matchedQueryValue.map((m) => m.maps.filter((map) => map.scenario === scenario))[0] : undefined; const maps = matchedQueryValue ? matchedQueryValue.map((m) => m.maps.filter((map) => map.scenario === scenario))[0] : undefined;
return maps ? maps.filter((x) => x.filter === true || x.filter === undefined)[0] : undefined; return maps ? maps.filter((x) => x.filter === true || x.filter === undefined)[0] : undefined;
} catch (e) {
window.console.error(e);
return undefined;
}
} }
async function GoToAccessIsDenied(next) { async function GoToAccessIsDenied(next) {

View file

@ -12,7 +12,6 @@ const navigationScenarios = Object.freeze({
CLICKED_FORWARD_POLICY_UNVERIFIED: 'CLICKED_FORWARD_POLICY_UNVERIFIED', CLICKED_FORWARD_POLICY_UNVERIFIED: 'CLICKED_FORWARD_POLICY_UNVERIFIED',
CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES: 'CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES', CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES: 'CLICKED_FORWARD_POLICY_VERIFIED_NO_VEHICLES',
CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES: 'CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES', CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES: 'CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES',
SAVE_SESSION_FAILED: 'SAVE_SESSION_FAILED',
// Duplicate Check // Duplicate Check
CLICKED_FORWARD_LOADED_DUPLICATE_WITH_POLICY_VEHICLE: 'CLICKED_FORWARD_LOADED_DUPLICATE_WITH_POLICY_VEHICLE', CLICKED_FORWARD_LOADED_DUPLICATE_WITH_POLICY_VEHICLE: 'CLICKED_FORWARD_LOADED_DUPLICATE_WITH_POLICY_VEHICLE',
@ -70,11 +69,8 @@ const navigationScenarios = Object.freeze({
// Coverage Statement // Coverage Statement
CLICKED_BACK_WITH_REPAIR: 'CLICKED_BACK_WITH_REPAIR', CLICKED_BACK_WITH_REPAIR: 'CLICKED_BACK_WITH_REPAIR',
CLICKED_FORWARD_WITH_INVALID_STATE: 'CLICKED_FORWARD_WITH_INVALID_STATE',
PRICING_LOOKUP_ERROR: 'PRICING_LOOKUP_ERROR',
// TPA Search // TPA Search
CLICKED_NEED_HELP: 'CLICKED_NEED_HELP',
CLICKED_FORWARD_WITH_SAFELITE_SHOP: 'CLICKED_FORWARD_WITH_SAFELITE_SHOP', CLICKED_FORWARD_WITH_SAFELITE_SHOP: 'CLICKED_FORWARD_WITH_SAFELITE_SHOP',
CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP: 'CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP', CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP: 'CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP',
@ -88,7 +84,6 @@ const navigationScenarios = Object.freeze({
// Provider Preference // Provider Preference
CLICKED_FORWARD_WITH_SAFELITE: 'CLICKED_FORWARD_WITH_SAFELITE', CLICKED_FORWARD_WITH_SAFELITE: 'CLICKED_FORWARD_WITH_SAFELITE',
CLICKED_FORWARD_WITH_TPA_ENABLED: 'CLICKED_FORWARD_WITH_TPA_ENABLED', CLICKED_FORWARD_WITH_TPA_ENABLED: 'CLICKED_FORWARD_WITH_TPA_ENABLED',
CLICKED_FORWARD_WITH_TPA_DISABLED: 'CLICKED_FORWARD_WITH_TPA_DISABLED',
CLICKED_FORWARD_WITH_POLICY_AND_VEHICLES: 'CLICKED_FORWARD_WITH_POLICY_AND_VEHICLES', CLICKED_FORWARD_WITH_POLICY_AND_VEHICLES: 'CLICKED_FORWARD_WITH_POLICY_AND_VEHICLES',
// Service Package // Service Package
@ -110,10 +105,11 @@ const navigationScenarios = Object.freeze({
PAY_IN_ADVANCE_ERROR: 'PAY_IN_ADVANCE_ERROR', PAY_IN_ADVANCE_ERROR: 'PAY_IN_ADVANCE_ERROR',
PAY_IN_ADVANCE_CREDIT_CARD_ERROR: 'PAY_IN_ADVANCE_CREDIT_CARD_ERROR', PAY_IN_ADVANCE_CREDIT_CARD_ERROR: 'PAY_IN_ADVANCE_CREDIT_CARD_ERROR',
PAY_IN_ADVANCE_SUCCESS: 'PAY_IN_ADVANCE_SUCCESS', PAY_IN_ADVANCE_SUCCESS: 'PAY_IN_ADVANCE_SUCCESS',
EDIT_SERVICE_LOCATION: 'EDIT_SERVICE_LOCATION',
EDIT_SCHEDULE: 'EDIT_SCHEDULE',
EDIT_WIPERS: 'EDIT_WIPERS',
// Bailout // Bailout
CLICKED_FORWARD_WITH_BAILOUT: 'CLICKED_FORWARD_WITH_BAILOUT',
CLICKED_NEED_HELP_WITH_BAILOUT: 'CLICKED_NEED_HELP_WITH_BAILOUT',
BAILOUT: 'BAILOUT' BAILOUT: 'BAILOUT'
}); });

View file

@ -14,10 +14,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD, scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS destinationIssPageValue: issPageValues.POLICY_HOLDER_DETAILS
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -55,10 +51,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -141,14 +133,6 @@ const routingTable = () => [
{ {
scenario: issPageValues.VEHICLE_LOOKUP, scenario: issPageValues.VEHICLE_LOOKUP,
destinationIssPageValue: issPageValues.VEHICLE_LOOKUP destinationIssPageValue: issPageValues.VEHICLE_LOOKUP
},
{
scenario: issPageValues.BAILOUT_PAGE, // TODO is this a bug
destinationIssPageValue: issPageValues.BAILOUT_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -215,10 +199,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -256,10 +236,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -293,10 +269,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -330,6 +302,10 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -451,14 +427,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES, scenario: navigationScenarios.CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES,
destinationIssPageValue: issPageValues.POLICY_VEHICLES destinationIssPageValue: issPageValues.POLICY_VEHICLES
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
},
{
scenario: navigationScenarios.SAVE_SESSION_FAILED,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -535,10 +503,6 @@ const routingTable = () => [
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAR_ID_NOT_FOUND, scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAR_ID_NOT_FOUND,
destinationIssPageValue: issPageValues.VIN_LOOKUP destinationIssPageValue: issPageValues.VIN_LOOKUP
}, },
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
},
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_ENDORSEMENTS, scenario: navigationScenarios.CLICKED_FORWARD_WITH_ENDORSEMENTS,
destinationIssPageValue: issPageValues.POLICY_ENDORSEMENTS destinationIssPageValue: issPageValues.POLICY_ENDORSEMENTS
@ -587,18 +551,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_INVALID_STATE,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
},
{
scenario: navigationScenarios.PRICING_LOOKUP_ERROR,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -616,10 +568,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED, scenario: navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED,
destinationIssPageValue: issPageValues.TPA_SEARCH destinationIssPageValue: issPageValues.TPA_SEARCH
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -690,8 +638,20 @@ const routingTable = () => [
destinationIssPageValue: issPageValues.PAYMENT_PAGE destinationIssPageValue: issPageValues.PAYMENT_PAGE
}, },
{ {
scenario: navigationScenarios.SAVE_SESSION_FAILED, scenario: navigationScenarios.EDIT_SERVICE_LOCATION,
destinationIssPageValue: issPageValues.BAILOUT_PAGE destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.EDIT_SCHEDULE,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
},
{
scenario: navigationScenarios.EDIT_VEHICLE,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.EDIT_WIPERS,
destinationIssPageValue: issPageValues.SERVICE_PACKAGES
} }
] ]
}, },
@ -764,10 +724,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD, scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.TPA_CONFIRMATION destinationIssPageValue: issPageValues.TPA_CONFIRMATION
},
{
scenario: navigationScenarios.SAVE_SESSION_FAILED,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },
@ -794,10 +750,6 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP, scenario: navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,
destinationIssPageValue: issPageValues.TPA_SUBMIT destinationIssPageValue: issPageValues.TPA_SUBMIT
},
{
scenario: navigationScenarios.CLICKED_NEED_HELP,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },

View file

@ -4,6 +4,8 @@ import issPageValues from '@/router/router-constants/issPage-values';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import App from '@/App.vue'; import App from '@/App.vue';
import bailoutMessage from '@/constants/bailoutMessage';
import { useMainStore } from '@/store';
describe('Router', () => { describe('Router', () => {
beforeAll(() => { beforeAll(() => {
@ -36,4 +38,16 @@ describe('Router', () => {
// Assert // Assert
expect(router.push.mock.calls[0][0].state).toBe(parameters); expect(router.push.mock.calls[0][0].state).toBe(parameters);
}); });
it('Should set bailout and navigate to bailout page when calling navigateBailout', () => {
// Arrange
router.push = jest.fn();
// Act
router.navigateBailout(bailoutMessage.unknown({}));
// Assert
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.BAILOUT_PAGE);
expect(useMainStore().isBailout).toBeTruthy();
});
}); });

View file

@ -1,7 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import applicationConfig from '@/constants/application-config'; import applicationConfig from '@/constants/application-config';
import bailoutCode from '@/constants/bailoutCode'; import bailoutCode from '@/constants/bailoutCode';
import bailoutMessage from '@/constants/bailoutMessage';
import coverageStatuses from '@/constants/coverage-statuses'; import coverageStatuses from '@/constants/coverage-statuses';
import coverageType from '@/constants/coverage-type'; import coverageType from '@/constants/coverage-type';
import damageLocationsSelected from '@/constants/damage-locations-selected'; import damageLocationsSelected from '@/constants/damage-locations-selected';
@ -22,14 +21,11 @@ import {
noCoverageForSelectedVehicle, noCoverageForSelectedVehicle,
repairWaivedForSelectedVehicle repairWaivedForSelectedVehicle
} from '@/helpers/policy-vehicle-helper'; } from '@/helpers/policy-vehicle-helper';
import { import { buildURLSearchParams, getPartNumbersListForQueryString } from '@/helpers/querystring-helper';
buildURLSearchParams,
getPartNumbersListForQueryString,
getTaxLineItemQueryString
} from '@/helpers/querystring-helper';
import { getTopLevelGlassPartsWithRecal } from '@/helpers/recal-helper'; import { getTopLevelGlassPartsWithRecal } from '@/helpers/recal-helper';
import { getDateForSavedSessionTimeout } from '@/helpers/session-helper'; import { getDateForSavedSessionTimeout } from '@/helpers/session-helper';
import issPageValues from '@/router/router-constants/issPage-values'; import issPageValues from '@/router/router-constants/issPage-values';
import CoverageStatuses from '@/constants/coverage-statuses';
const storeId = 'main'; const storeId = 'main';
@ -533,20 +529,15 @@ export const useMainStore = defineStore({
payload: {} payload: {}
}); });
}, },
getIsVinbyAddressPermissible() { async getIsVinbyAddressPermissible() {
try { try {
const response = globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.IsVinbyAddressPermissible.method, method: endpoints.IsVinbyAddressPermissible.method,
endpoint: `${endpoints.IsVinbyAddressPermissible.url}?state=${this.order.customer.address.state}`, endpoint: `${endpoints.IsVinbyAddressPermissible.url}?state=${this.order.customer.address.state}`,
payload: {} payload: {}
}); });
return response; } catch (e) {
} catch (responseError) { return false;
return {
error: {
status: responseError.status
}
};
} }
}, },
async getCoveragePolicyInfo() { async getCoveragePolicyInfo() {
@ -555,7 +546,7 @@ export const useMainStore = defineStore({
if (!issConfig.isCoverageEnabled || applicationUser.coverageLookupAttempts > 10) { if (!issConfig.isCoverageEnabled || applicationUser.coverageLookupAttempts > 10) {
this.updateCoverageType(coverageType.NONE); this.updateCoverageType(coverageType.NONE);
return Promise.resolve(); return;
} }
this.applicationUser.coverageAttempts += 1; this.applicationUser.coverageAttempts += 1;
@ -571,7 +562,8 @@ export const useMainStore = defineStore({
dateOfLoss: policy.dateOfLoss, dateOfLoss: policy.dateOfLoss,
zipCode: policy.policyZipCode, zipCode: policy.policyZipCode,
referralCorrelationId: order.referralCorrelationId referralCorrelationId: order.referralCorrelationId
} },
bailoutOnError: false
}); });
const responsePolicy = response?.data?.policies?.[0]; const responsePolicy = response?.data?.policies?.[0];
@ -600,10 +592,8 @@ export const useMainStore = defineStore({
} else { } else {
this.updateCoverageType(coverageType.NONE); this.updateCoverageType(coverageType.NONE);
} }
return Promise.resolve();
} catch (e) { } catch (e) {
this.updateCoverageType(coverageType.NONE); this.updateCoverageType(coverageType.NONE);
return Promise.reject(e);
} }
}, },
clearDuplicateOrders() { clearDuplicateOrders() {
@ -621,82 +611,81 @@ export const useMainStore = defineStore({
updateIsItacOptimized(isItacOptimized) { updateIsItacOptimized(isItacOptimized) {
this.order.insuranceCoverage.isItacOptimized = isItacOptimized || false; this.order.insuranceCoverage.isItacOptimized = isItacOptimized || false;
}, },
registerClaim() { async registerClaim() {
const nonNumberCharRegex = /[^0-9]/g; const nonNumberCharRegex = /[^0-9]/g;
const { order, isITAC } = this; const { isITAC } = this;
return new Promise((resolve, reject) => {
globalMethods.callHttpClient({ try {
const response = await globalMethods.callHttpClient({
method: endpoints.RegisterClaim.method, method: endpoints.RegisterClaim.method,
endpoint: endpoints.RegisterClaim.url, endpoint: endpoints.RegisterClaim.url,
payload: payload:
{ {
referralCorrelationId: this.order.referralCorrelationId, referralCorrelationId: this.order.referralCorrelationId,
accountNumber: this.order.parentAccountNumber?.toString() ?? '', accountNumber: this.order.parentAccountNumber?.toString() ?? '',
policyData: this.order.policy.policyData, policyData: this.order.policy.policyData,
isItac: isITAC, isItac: isITAC,
insured: { insured: {
firstName: this.order.customer.firstName, firstName: this.order.customer.firstName,
lastName: this.order.customer.lastName, lastName: this.order.customer.lastName,
address: { address: {
addressLine1: this.order.customer.address.streetAddress, addressLine1: this.order.customer.address.streetAddress,
addressLine2: this.order.customer.address.streetAddress2, addressLine2: this.order.customer.address.streetAddress2,
city: this.order.customer.address.city, city: this.order.customer.address.city,
state: this.order.customer.address.state, state: this.order.customer.address.state,
zipCode: this.order.customer.address.zipCode, zipCode: this.order.customer.address.zipCode,
country: 'US' // TODO set from store country: 'US' // TODO set from store
},
homePhone: {
number: this.contactInfo.homePhone?.replaceAll(nonNumberCharRegex, '') ?? ''
}
}, },
homePhone: { driver: {
number: this.contactInfo.homePhone?.replaceAll(nonNumberCharRegex, '') ?? '' firstName: this.order.customer.firstName,
lastName: this.order.customer.lastName
},
caller: {
homePhone: {}
},
policyInfo: {
policyNumber: this.order.policy.policyNumber,
safelitePolicy: {
policies: []
},
actualDeductible: this.currentDeductible.toString() ?? ''
},
lossInfo: {
dateOfLoss: this.order.policy.dateOfLoss,
location: {
city: this.order.policy.damageCity,
state: this.order.policy.damageState,
country: 'US' // TODO set from store
},
vehicle: {
id: this.order.vehicle.policyVehicleId?.toString() ?? '',
year: this.order.vehicle.year?.toString() ?? '',
make: this.order.vehicle.make,
model: this.order.vehicle.model,
vin: this.order.vehicle.vin
},
cause: this.order.policy.damageCause,
damageDescription: this.order.policy.damageCause
} }
}, },
driver: { bailoutOnError: false
firstName: this.order.customer.firstName,
lastName: this.order.customer.lastName
},
caller: {
homePhone: {}
},
policyInfo: {
policyNumber: this.order.policy.policyNumber,
safelitePolicy: {
policies: []
},
actualDeductible: this.currentDeductible.toString() ?? ''
},
lossInfo: {
dateOfLoss: this.order.policy.dateOfLoss,
location: {
city: this.order.policy.damageCity,
state: this.order.policy.damageState,
country: 'US' // TODO set from store
},
vehicle: {
id: this.order.vehicle.policyVehicleId?.toString() ?? '',
year: this.order.vehicle.year?.toString() ?? '',
make: this.order.vehicle.make,
model: this.order.vehicle.model,
vin: this.order.vehicle.vin
},
cause: this.order.policy.damageCause,
damageDescription: this.order.policy.damageCause
}
}
}).then((response) => {
this.order.insuranceCoverage.claimNumber = response.data.claimNumber;
if (response.data.isSuccess) {
this.updateCoverageStatus(coverageStatuses.VERIFIED);
} else {
this.updateCoverageStatus(coverageStatuses.NO_COVERAGE);
}
return resolve(response);
}, (error) => {
this.updateCoverageStatus(coverageStatuses.NO_COVERAGE);
this.order.insuranceCoverage.claimNumber = null;
return reject(error);
}); });
}); this.order.insuranceCoverage.claimNumber = response.data.claimNumber;
if (response.data.isSuccess) {
this.updateCoverageStatus(coverageStatuses.VERIFIED);
} else {
this.updateCoverageStatus(coverageStatuses.NO_COVERAGE);
}
} catch (e) {
this.updateCoverageStatus(coverageStatuses.NO_COVERAGE);
this.order.insuranceCoverage.claimNumber = null;
}
}, },
getFinalDeductible() { async getFinalDeductible() {
const endorsementAnswersForPayload = []; const endorsementAnswersForPayload = [];
const endorsementAnswers = this.order.policy.endorsementQuestionAnswers; const endorsementAnswers = this.order.policy.endorsementQuestionAnswers;
if (endorsementAnswers) { if (endorsementAnswers) {
@ -712,9 +701,9 @@ export const useMainStore = defineStore({
glassInformation?.forEach((glassPiece) => { glassInformation?.forEach((glassPiece) => {
manualGlassNamesArray.push(glassPiece?.glassLocation?.toUpperCase()); manualGlassNamesArray.push(glassPiece?.glassLocation?.toUpperCase());
}); });
return new Promise((resolve, reject) => { try {
globalMethods.callHttpClient({ const r = await globalMethods.callHttpClient({
method: endpoints.FinalDeductible.method, method: endpoints.FinalDeductible.method,
endpoint: endpoints.FinalDeductible.url, endpoint: endpoints.FinalDeductible.url,
payload: { payload: {
@ -736,16 +725,18 @@ export const useMainStore = defineStore({
vehicleVin: this.order.vehicle.vin, vehicleVin: this.order.vehicle.vin,
policyData: this.order.policy.policyData, policyData: this.order.policy.policyData,
isItac: this.isITAC isItac: this.isITAC
} },
}).then((r) => { bailoutOnError: false
this.order.policy.policyData = r.data.policyData; });
this.updateDeductible(r.data); this.order.policy.policyData = r.data.policyData;
return resolve(r); this.updateDeductible(r.data);
}).catch((error) => reject(error)); return r;
}); } catch (e) {
this.updateCoverageStatus(CoverageStatuses.PENDING);
}
}, },
getDuplicateReferrals() { async getDuplicateReferrals() {
const params = new URLSearchParams({ const params = new URLSearchParams({
parentAccountNumber: this.order.parentAccountNumber, parentAccountNumber: this.order.parentAccountNumber,
customerPhoneNumber: this.order.contactInfo.servicePhone, customerPhoneNumber: this.order.contactInfo.servicePhone,
@ -754,18 +745,16 @@ export const useMainStore = defineStore({
dateOfLoss: this.order.policy.dateOfLoss dateOfLoss: this.order.policy.dateOfLoss
}); });
return new Promise((resolve, reject) => { try {
globalMethods.callHttpClient({ const duplicates = await globalMethods.callHttpClient({
method: endpoints.DuplicateSearch.method, method: endpoints.DuplicateSearch.method,
endpoint: `${endpoints.DuplicateSearch.url}?${params.toString()}` endpoint: `${endpoints.DuplicateSearch.url}?${params.toString()}`,
}).then((r) => { bailoutOnError: false
this.applicationUser.duplicateOrders = r.data ?? [];
return resolve(r.data);
}).catch((error) => {
this.applicationUser.duplicateOrders = [];
return reject(error);
}); });
}); this.applicationUser.duplicateOrders = duplicates.data ?? [];
} catch (e) {
this.applicationUser.duplicateOrders = [];
}
}, },
async lookupVinByPlate(licensePlate, licenseState) { async lookupVinByPlate(licensePlate, licenseState) {
try { try {
@ -884,6 +873,10 @@ export const useMainStore = defineStore({
}, },
getMobileTimeSlots(startDate, endDate, zipCodeOverride = null) { getMobileTimeSlots(startDate, endDate, zipCodeOverride = null) {
const { order } = this; const { order } = this;
if (!this.billToAccountNumber) {
return null;
}
const { vehicle } = this.order; const { vehicle } = this.order;
let lineItems = [ let lineItems = [
...(order.lineItems.supportingItems ?? []), ...(order.lineItems.supportingItems ?? []),
@ -939,6 +932,9 @@ export const useMainStore = defineStore({
}, },
getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) { getShopTimeSlots(startDate, endDate, shopAppointmentType, providerNumber) {
const { order } = this; const { order } = this;
if (!this.billToAccountNumber || !providerNumber) {
return null;
}
const { vehicle } = this.order; const { vehicle } = this.order;
let lineItems = [ let lineItems = [
@ -990,7 +986,8 @@ export const useMainStore = defineStore({
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetShopTimeSlots.method, method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.url, endpoint: endpoints.GetShopTimeSlots.url,
payload payload,
bailoutOnError: false
}); });
}, },
async getWipers() { async getWipers() {
@ -1062,7 +1059,7 @@ export const useMainStore = defineStore({
safeliteOnly, safeliteOnly,
carId, carId,
windshieldPartWithRecal?.partNumber, windshieldPartWithRecal?.partNumber,
isBigTruck !!isBigTruck
) )
}); });
}, },
@ -1135,6 +1132,10 @@ export const useMainStore = defineStore({
const providerToUse = provider?.providerNumber ?? defaultProviderNumber; const providerToUse = provider?.providerNumber ?? defaultProviderNumber;
const partNumberListQueryString = getPartNumbersListForQueryString(glassParts, 'PartNumbers'); const partNumberListQueryString = getPartNumbersListForQueryString(glassParts, 'PartNumbers');
if (!this.billToAccountNumber || !providerToUse) {
return null
}
const params = new URLSearchParams({ const params = new URLSearchParams({
serviceType: damageType, serviceType: damageType,
facilityType, facilityType,
@ -1218,9 +1219,6 @@ export const useMainStore = defineStore({
IsOEMRequest: this.hasOemEndorsement IsOEMRequest: this.hasOemEndorsement
} }
} }
}).catch((error) => {
console.error(error);
throw error;
}); });
const { lineItems, serverData, isItac, primaryBillToNumber, partsWerePriced, isItacOptimized } = response.data; const { lineItems, serverData, isItac, primaryBillToNumber, partsWerePriced, isItacOptimized } = response.data;
@ -1255,62 +1253,74 @@ export const useMainStore = defineStore({
}, },
async getCombinedQuote(availableLineItems) { async getCombinedQuote(availableLineItems) {
if (availableLineItems && availableLineItems.length > 0) { if (!availableLineItems || availableLineItems.length === 0) {
const { vehicle, serviceLocation, policy, eon, referralNumber, referralDate, referralSequenceNumber, lineItems } = this.order; return [];
const availableLineItemsFiltered = availableLineItems.filter((lineItem) => !!(lineItem.partNumber));
const lineItemsWithOnlyPartNumbers = availableLineItemsFiltered.map((lineItem) => ({ partNumber: lineItem.partNumber }));
const payload = {
ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber,
CarId: vehicle.carId,
Make: vehicle.make,
Model: vehicle.model,
Year: vehicle.year,
EON: eon,
ZipCode: policy.policyZipCode,
State: this.order.customer.address.state,
ReferralNumber: referralNumber,
ReferralDate: referralDate,
ReferralSequenceNumber: referralSequenceNumber,
ServiceZipCode: serviceLocation.zipCode,
IsReplacement: !this.isRepair,
ServerData: lineItems.serverData,
LineItems: lineItemsWithOnlyPartNumbers,
FeatureToggles: getFeatureTogglesPayloadObject(this.experimentSettings)
};
const response = await globalMethods
.callHttpClient({
method: endpoints.GetCombinedQuote.method,
endpoint: endpoints.GetCombinedQuote.url,
payload
})
.catch((error) => {
console.error(error);
return [];
});
const { lineItems: pricedLinedItems, serverData } = response.data;
if (serverData) {
this.order.lineItems.serverData = serverData;
}
if (pricedLinedItems) {
return addPricesToLineItems(availableLineItems, pricedLinedItems);
}
return availableLineItems;
} }
return []; const {
vehicle,
serviceLocation,
policy,
eon,
referralNumber,
referralDate,
referralSequenceNumber,
lineItems
} = this.order;
const availableLineItemsFiltered = availableLineItems.filter((lineItem) => !!(lineItem.partNumber));
const lineItemsWithOnlyPartNumbers = availableLineItemsFiltered.map((lineItem) => ({ partNumber: lineItem.partNumber }));
const payload = {
ParentAccountNumber: this.order.parentAccountNumber,
BillToAccountNumber: this.billToAccountNumber,
ProviderNumber: this.providerNumber,
CarId: vehicle.carId,
Make: vehicle.make,
Model: vehicle.model,
Year: vehicle.year,
EON: eon,
ZipCode: policy.policyZipCode,
State: this.order.customer.address.state,
ReferralNumber: referralNumber,
ReferralDate: referralDate,
ReferralSequenceNumber: referralSequenceNumber,
ServiceZipCode: serviceLocation.zipCode,
IsReplacement: !this.isRepair,
ServerData: lineItems.serverData,
LineItems: lineItemsWithOnlyPartNumbers,
FeatureToggles: getFeatureTogglesPayloadObject(this.experimentSettings)
};
const response = await globalMethods
.callHttpClient({
method: endpoints.GetCombinedQuote.method,
endpoint: endpoints.GetCombinedQuote.url,
payload
})
.catch((error) => {
console.error(error);
return [];
});
const { lineItems: pricedLinedItems, serverData } = response.data;
if (serverData) {
this.order.lineItems.serverData = serverData;
}
if (pricedLinedItems) {
return addPricesToLineItems(availableLineItems, pricedLinedItems);
}
return availableLineItems;
}, },
getMobileFeePart() { getMobileFeePart() {
const { vehicle, insuranceCoverage } = this.order; const { vehicle, insuranceCoverage } = this.order;
if (!this.billToAccountNumber || !this.providerNumber) {
return null;
}
const params = new URLSearchParams({ const params = new URLSearchParams({
serviceType: this.damage.isRepair ? 'Repair' : 'Install', serviceType: this.damage.isRepair ? 'Repair' : 'Install',
@ -1330,7 +1340,7 @@ export const useMainStore = defineStore({
}); });
}, },
getServiceabilityDetails({ serviceZipCode }) { getServiceabilityDetails(serviceZipCode) {
const { vehicle, damage, parentAccountNumber, referralSequenceNumber, lineItems } = this.order; const { vehicle, damage, parentAccountNumber, referralSequenceNumber, lineItems } = this.order;
const { carId } = vehicle; const { carId } = vehicle;
const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace); const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace);
@ -1360,7 +1370,8 @@ export const useMainStore = defineStore({
endpoint: endpoints.LookupVehicleByVin.url, endpoint: endpoints.LookupVehicleByVin.url,
payload: { payload: {
vin vin
} },
bailoutOnError: false
}); });
}, },
@ -1392,7 +1403,7 @@ export const useMainStore = defineStore({
this.applicationUser.crmCustomerId = response.crmCustomerId.toString(); this.applicationUser.crmCustomerId = response.crmCustomerId.toString();
}, },
saveSession({ submitAfterSave, createWorkOrderNumberForPIA }) { saveSession({ submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError }) {
const { vehicle, damage, policy, customer, contactInfo, payment, const { vehicle, damage, policy, customer, contactInfo, payment,
lineItems, serviceLocation, schedule, insuranceCoverage } = this.order; lineItems, serviceLocation, schedule, insuranceCoverage } = this.order;
@ -1544,7 +1555,7 @@ export const useMainStore = defineStore({
method: endpoints.SaveSession.method, method: endpoints.SaveSession.method,
endpoint: endpoints.SaveSession.url, endpoint: endpoints.SaveSession.url,
payload, payload,
bailoutOnError: false bailoutOnError
}).then((response) => { }).then((response) => {
if (loadedFromDupeCheck) { if (loadedFromDupeCheck) {
this.order.loadedSessionClearedPreviousData = true; this.order.loadedSessionClearedPreviousData = true;
@ -1831,24 +1842,26 @@ export const useMainStore = defineStore({
this.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu ?? this.order.serviceLocation.zipCodeCtu; this.order.serviceLocation.zipCodeCtu = serviceLocationInfo.zipCodeCtu ?? this.order.serviceLocation.zipCodeCtu;
this.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType ?? this.order.serviceLocation.appointmentType; this.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType ?? this.order.serviceLocation.appointmentType;
this.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected ?? this.order.serviceLocation.isVehicleProtected; this.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected ?? this.order.serviceLocation.isVehicleProtected;
this.updateServiceLocationProvider(serviceLocationInfo.provider)
if (serviceLocationInfo.provider) {
this.order.serviceLocation.provider = {
providerNumber: serviceLocationInfo.provider?.providerNumber,
address: {
streetAddress: serviceLocationInfo.provider?.address?.streetAddress,
city: serviceLocationInfo.provider?.address?.city,
state: serviceLocationInfo.provider?.address?.state,
zipCode: serviceLocationInfo.provider?.address?.zipCode,
zipCodeCtu: serviceLocationInfo.provider?.address?.zipCodeCtu
},
companyName: serviceLocationInfo.provider?.companyName,
phoneNumber: serviceLocationInfo.provider?.phoneNumber
};
}
this.order.serviceLocation.tpaSearchRadius = serviceLocationInfo.tpaSearchRadius; this.order.serviceLocation.tpaSearchRadius = serviceLocationInfo.tpaSearchRadius;
}, },
updateAppointmentType(appointmentType) {
this.order.serviceLocation.appointmentType = appointmentType;
},
updateServiceLocationProvider(providerInfo) {
this.order.serviceLocation.provider = {
providerNumber: providerInfo?.providerNumber,
address: {
streetAddress: providerInfo?.address?.streetAddress,
city: providerInfo?.address?.city,
state: providerInfo?.address?.state,
zipCode: providerInfo?.address?.zipCode,
zipCodeCtu: providerInfo?.address?.zipCodeCtu
},
companyName: providerInfo?.companyName,
phoneNumber: providerInfo?.phoneNumber
};
},
resetState() { resetState() {
Object.assign(this, getDefaultState()); Object.assign(this, getDefaultState());
}, },
@ -2448,6 +2461,7 @@ export const useMainStore = defineStore({
populateInitialState(forceReset) { populateInitialState(forceReset) {
if (!sessionStorage.getItem(storeId) || forceReset) { if (!sessionStorage.getItem(storeId) || forceReset) {
this.$state = getDefaultState(); this.$state = getDefaultState();
this.resetSubmittedOrder();
} }
}, },
@ -2684,36 +2698,33 @@ export const useMainStore = defineStore({
} }
}, },
async getBillToInfo() { async getBillToInfo(componentProviderNumber = null) {
const { order, issConfig } = this; const providerNumber = componentProviderNumber || this.providerNumber;
try { if (!providerNumber) {
const params = new URLSearchParams({ return;
parentAccountNumber: order.parentAccountNumber.toString(),
providerNumber: this.providerNumber,
typeOfClaim: 'GLASS ONLY',
lineOfBusiness: 'PERSONAL',
isItac: this.isITAC
});
const response = await globalMethods.callHttpClient({
method: endpoints.GetBillToInfo.method,
endpoint: `${endpoints.GetBillToInfo.url}?${params.toString()}`
});
const billToInfo = response.data;
if (billToInfo != null) {
issConfig.billToAccountNumber = billToInfo.toString();
return Promise.resolve();
}
return Promise.reject(new Error('Invalid billToInfo'));
} catch (e) {
return Promise.reject(e);
} }
const { order, issConfig } = this;
const params = new URLSearchParams({
parentAccountNumber: order.parentAccountNumber.toString(),
providerNumber,
typeOfClaim: 'GLASS ONLY',
lineOfBusiness: 'PERSONAL',
isItac: this.isITAC
});
const response = await globalMethods.callHttpClient({
method: endpoints.GetBillToInfo.method,
endpoint: `${endpoints.GetBillToInfo.url}?${params.toString()}`,
bailoutOnError: false
});
const billToInfo = response.data;
issConfig.billToAccountNumber = billToInfo.toString();
}, },
async validateClientTag(clientTag) { async validateClientTag(clientTag) {
return await globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.ValidateClientTag.method, method: endpoints.ValidateClientTag.method,
endpoint: `${endpoints.ValidateClientTag.url}/${clientTag}` endpoint: `${endpoints.ValidateClientTag.url}/${clientTag}`
}); });
@ -2733,10 +2744,6 @@ export const useMainStore = defineStore({
}); });
}, },
saveServiceLocation(serviceLocationInfo) {
this.updateServiceLocation(serviceLocationInfo);
},
saveRegistrationAddressLookup({ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) { saveRegistrationAddressLookup({ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) {
// Reset dependent state when changing // Reset dependent state when changing
if if

View file

@ -459,9 +459,9 @@ describe('Store', () => {
expect(store.order.insuranceCoverage.claimNumber).not.toBeNull(); expect(store.order.insuranceCoverage.claimNumber).not.toBeNull();
}); });
it('Call to client returns exception, resulting in object with error property being returned', async () => { it('Call to client returns exception, and no error is returned from registerClaim method', async () => {
// Arrange // Arrange
expect.assertions(4); expect.assertions(3);
const error = 'register claim error'; const error = 'register claim error';
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error));
store.order.insuranceCoverage.coverageStatus = coverageStatuses.PENDING; store.order.insuranceCoverage.coverageStatus = coverageStatuses.PENDING;
@ -1494,8 +1494,8 @@ describe('Store', () => {
// Assert // Assert
expect(globalMethods.callHttpClient).toHaveBeenCalled(); expect(globalMethods.callHttpClient).toHaveBeenCalled();
}); });
it('api call throws exception => insuranceCoverage.coverageType is NONE', async () => { it('api call throws exception => no error is thrown from getCoveragePolicyInfo and insuranceCoverage.coverageType is NONE', async () => {
expect.assertions(3); expect.assertions(2);
const error = 'get coverage policy info error'; const error = 'get coverage policy info error';
store.issConfig.isCoverageEnabled = true; store.issConfig.isCoverageEnabled = true;
store.applicationUser.coverageLookupAttempts = 0; store.applicationUser.coverageLookupAttempts = 0;
@ -1567,9 +1567,9 @@ describe('Store', () => {
expect(globalMethods.callHttpClient).toHaveBeenCalled(); expect(globalMethods.callHttpClient).toHaveBeenCalled();
expect(store.applicationUser.duplicateOrders).toEqual(expected); expect(store.applicationUser.duplicateOrders).toEqual(expected);
}); });
it('Call to client returns exception => object with error property returned and duplicateReferrals set to []', async () => { it('Call to client returns exception => object with error property returned and duplicateReferrals set to [] and no error is returned from getDuplicateReferrals', async () => {
// Arrange // Arrange
expect.assertions(3); expect.assertions(2);
const error = 'get duplicate referrals error'; const error = 'get duplicate referrals error';
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error));
@ -1807,7 +1807,8 @@ describe('Store', () => {
vehicleVin, vehicleVin,
policyData, policyData,
isItac: store.isITAC isItac: store.isITAC
}) }),
bailoutOnError: false
})); }));
}); });
it('only "Yes" endorsement answers are added to payload', () => { it('only "Yes" endorsement answers are added to payload', () => {
@ -1912,14 +1913,15 @@ describe('Store', () => {
vehicleVin, vehicleVin,
policyData, policyData,
isItac: store.isITAC isItac: store.isITAC
}) }),
bailoutOnError: false
})); }));
}); });
}); });
describe('unsuccessful api call', () => { describe('unsuccessful api call', () => {
it('api call throws exception', async () => { it('api call throws exception. no error from getFinalDeductible', async () => {
// Arrange // Arrange
expect.assertions(2); expect.assertions(1);
const error = 'final deductible error'; const error = 'final deductible error';
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error));
@ -2078,8 +2080,8 @@ describe('Store', () => {
expect(result.data.shopProviders[1]).toBe(provider2); expect(result.data.shopProviders[1]).toBe(provider2);
}); });
}); });
it('api call throws exception => coverageType none', async () => { it('api call throws exception => coverageType none and no error from getCoveragePolicyInfo', async () => {
expect.assertions(3); expect.assertions(2);
const error = 'get coverage policy info error'; const error = 'get coverage policy info error';
store.issConfig.isCoverageEnabled = true; store.issConfig.isCoverageEnabled = true;

View file

@ -7,11 +7,6 @@ html {
&.list-button, &.list-button,
&.list-button.list-group, &.list-button.list-group,
&.list-card &.option-label { &.list-card &.option-label {
border: 1px solid $red;
position: relative;
z-index: 4;
border-radius: 60px;
.button-content { .button-content {
border: 1px solid $red; border: 1px solid $red;
} }

View file

@ -7,6 +7,7 @@ $svg-loading-modal-image: "data:image/svg+xml;charset=UTF-8,%3csvg fill='none' x
$svg-modal-header-button-close: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A"; $svg-modal-header-button-close: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
$svg-payment-method-review-toggle: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.9' xml:space='preserve'%3e%3cpath d='M8 8.9c-.2 0-.5-.1-.6-.3L.3 1.5C.1 1.4 0 1.1 0 .9 0 .7.1.4.3.3.4.1.7 0 .9 0c.2 0 .5.1.6.3L8 6.7 14.5.2c.1-.1.4-.2.6-.2.2 0 .5.1.6.3s.3.4.3.6c0 .2-.1.5-.3.6L8.6 8.6c-.1.2-.4.3-.6.3z' fill='%231474a2'/%3e%3c/svg%3e"; $svg-payment-method-review-toggle: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.9' xml:space='preserve'%3e%3cpath d='M8 8.9c-.2 0-.5-.1-.6-.3L.3 1.5C.1 1.4 0 1.1 0 .9 0 .7.1.4.3.3.4.1.7 0 .9 0c.2 0 .5.1.6.3L8 6.7 14.5.2c.1-.1.4-.2.6-.2.2 0 .5.1.6.3s.3.4.3.6c0 .2-.1.5-.3.6L8.6 8.6c-.1.2-.4.3-.6.3z' fill='%231474a2'/%3e%3c/svg%3e";
$svg-search-icon: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.7817 14.7328L11.8252 10.7762C12.8833 9.45005 13.3936 7.76911 13.2513 6.07849C13.1091 4.38788 12.325 2.81587 11.0601 1.6852C9.79515 0.554524 8.14538 -0.0490261 6.44946 -0.00154744C4.75353 0.0459312 3.14012 0.740836 1.94045 1.94051C0.740775 3.14018 0.0458701 4.75359 -0.00160848 6.44952C-0.0490871 8.14545 0.554463 9.79521 1.68514 11.0601C2.81581 12.325 4.38782 13.1091 6.07843 13.2514C7.76905 13.3937 9.44999 12.8834 10.7762 11.8252L14.7349 15.7839C14.8044 15.8527 14.8869 15.907 14.9774 15.9439C15.068 15.9808 15.165 15.9995 15.2628 15.9989C15.3606 15.9983 15.4573 15.9784 15.5475 15.9405C15.6376 15.9025 15.7194 15.8471 15.7881 15.7776C15.8568 15.708 15.9112 15.6256 15.9481 15.535C15.985 15.4444 16.0036 15.3474 16.0031 15.2496C16.0025 15.1518 15.9826 15.0551 15.9446 14.965C15.9067 14.8748 15.8513 14.7931 15.7817 14.7243V14.7328ZM6.63737 11.7913C5.61803 11.7913 4.62157 11.4891 3.77402 10.9228C2.92646 10.3564 2.26587 9.5515 1.87578 8.60975C1.4857 7.668 1.38363 6.63172 1.5825 5.63196C1.78136 4.6322 2.27222 3.71386 2.99301 2.99307C3.7138 2.27229 4.63214 1.78142 5.6319 1.58256C6.63166 1.38369 7.66793 1.48576 8.60969 1.87585C9.55144 2.26593 10.3564 2.92652 10.9227 3.77408C11.489 4.62163 11.7913 5.61809 11.7913 6.63743C11.7896 8.00382 11.2461 9.31376 10.2799 10.2799C9.3137 11.2461 8.00376 11.7897 6.63737 11.7913Z' fill='%231574A1'/%3E%3C/svg%3E%0A"; $svg-search-icon: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.7817 14.7328L11.8252 10.7762C12.8833 9.45005 13.3936 7.76911 13.2513 6.07849C13.1091 4.38788 12.325 2.81587 11.0601 1.6852C9.79515 0.554524 8.14538 -0.0490261 6.44946 -0.00154744C4.75353 0.0459312 3.14012 0.740836 1.94045 1.94051C0.740775 3.14018 0.0458701 4.75359 -0.00160848 6.44952C-0.0490871 8.14545 0.554463 9.79521 1.68514 11.0601C2.81581 12.325 4.38782 13.1091 6.07843 13.2514C7.76905 13.3937 9.44999 12.8834 10.7762 11.8252L14.7349 15.7839C14.8044 15.8527 14.8869 15.907 14.9774 15.9439C15.068 15.9808 15.165 15.9995 15.2628 15.9989C15.3606 15.9983 15.4573 15.9784 15.5475 15.9405C15.6376 15.9025 15.7194 15.8471 15.7881 15.7776C15.8568 15.708 15.9112 15.6256 15.9481 15.535C15.985 15.4444 16.0036 15.3474 16.0031 15.2496C16.0025 15.1518 15.9826 15.0551 15.9446 14.965C15.9067 14.8748 15.8513 14.7931 15.7817 14.7243V14.7328ZM6.63737 11.7913C5.61803 11.7913 4.62157 11.4891 3.77402 10.9228C2.92646 10.3564 2.26587 9.5515 1.87578 8.60975C1.4857 7.668 1.38363 6.63172 1.5825 5.63196C1.78136 4.6322 2.27222 3.71386 2.99301 2.99307C3.7138 2.27229 4.63214 1.78142 5.6319 1.58256C6.63166 1.38369 7.66793 1.48576 8.60969 1.87585C9.55144 2.26593 10.3564 2.92652 10.9227 3.77408C11.489 4.62163 11.7913 5.61809 11.7913 6.63743C11.7896 8.00382 11.2461 9.31376 10.2799 10.2799C9.3137 11.2461 8.00376 11.7897 6.63737 11.7913Z' fill='%231574A1'/%3E%3C/svg%3E%0A";
$svg-error-search-icon: "data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.7817 14.7328L11.8252 10.7762C12.8833 9.45005 13.3936 7.76911 13.2513 6.07849C13.1091 4.38788 12.325 2.81587 11.0601 1.6852C9.79515 0.554524 8.14538 -0.0490261 6.44946 -0.00154744C4.75353 0.0459312 3.14012 0.740836 1.94045 1.94051C0.740775 3.14018 0.0458701 4.75359 -0.00160848 6.44952C-0.0490871 8.14545 0.554463 9.79521 1.68514 11.0601C2.81581 12.325 4.38782 13.1091 6.07843 13.2514C7.76905 13.3937 9.44999 12.8834 10.7762 11.8252L14.7349 15.7839C14.8044 15.8527 14.8869 15.907 14.9774 15.9439C15.068 15.9808 15.165 15.9995 15.2628 15.9989C15.3606 15.9983 15.4573 15.9784 15.5475 15.9405C15.6376 15.9025 15.7194 15.8471 15.7881 15.7776C15.8568 15.708 15.9112 15.6256 15.9481 15.535C15.985 15.4444 16.0036 15.3474 16.0031 15.2496C16.0025 15.1518 15.9826 15.0551 15.9446 14.965C15.9067 14.8748 15.8513 14.7931 15.7817 14.7243V14.7328ZM6.63737 11.7913C5.61803 11.7913 4.62157 11.4891 3.77402 10.9228C2.92646 10.3564 2.26587 9.5515 1.87578 8.60975C1.4857 7.668 1.38363 6.63172 1.5825 5.63196C1.78136 4.6322 2.27222 3.71386 2.99301 2.99307C3.7138 2.27229 4.63214 1.78142 5.6319 1.58256C6.63166 1.38369 7.66793 1.48576 8.60969 1.87585C9.55144 2.26593 10.3564 2.92652 10.9227 3.77408C11.489 4.62163 11.7913 5.61809 11.7913 6.63743C11.7896 8.00382 11.2461 9.31376 10.2799 10.2799C9.3137 11.2461 8.00376 11.7897 6.63737 11.7913Z' fill='" + $svg-calendar-error-stroke-color + "'/%3E%3C/svg%3E%0A";
$svg-select-icon: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e"; $svg-select-icon: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e";
$svg-shop-list-button-green-availability: "data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Cpath d='M5.697 7.95252C5.5927 7.95252 5.49289 7.91102 5.41999 7.837L3.90597 6.32299C3.75233 6.16934 3.75233 5.92149 3.90597 5.76785C4.05962 5.6142 4.30747 5.6142 4.46111 5.76785L5.69812 7.00373L8.53999 4.16186C8.69364 4.00822 8.94149 4.00822 9.09513 4.16186C9.24878 4.31551 9.24878 4.56336 9.09513 4.717L5.97626 7.83588C5.90224 7.9099 5.80242 7.9514 5.69925 7.9514L5.697 7.95252Z' fill='%23006A36'/%3E%3C/svg%3E%0A"; $svg-shop-list-button-green-availability: "data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 12C3.19159 12 0.5 9.30841 0.5 6C0.5 2.69159 3.19159 0 6.5 0C9.80841 0 12.5 2.69159 12.5 6C12.5 9.30841 9.80841 12 6.5 12ZM6.5 0.785047C3.62449 0.785047 1.28505 3.12449 1.28505 6C1.28505 8.87551 3.62449 11.215 6.5 11.215C9.37551 11.215 11.715 8.87551 11.715 6C11.715 3.12449 9.37551 0.785047 6.5 0.785047Z' fill='%23006A36'/%3E%3Cpath d='M5.697 7.95252C5.5927 7.95252 5.49289 7.91102 5.41999 7.837L3.90597 6.32299C3.75233 6.16934 3.75233 5.92149 3.90597 5.76785C4.05962 5.6142 4.30747 5.6142 4.46111 5.76785L5.69812 7.00373L8.53999 4.16186C8.69364 4.00822 8.94149 4.00822 9.09513 4.16186C9.24878 4.31551 9.24878 4.56336 9.09513 4.717L5.97626 7.83588C5.90224 7.9099 5.80242 7.9514 5.69925 7.9514L5.697 7.95252Z' fill='%23006A36'/%3E%3C/svg%3E%0A";
$svg-shop-list-button-orange-availability: "data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 0C9.81368 0 12.5 2.68632 12.5 6C12.5 9.31368 9.81368 12 6.5 12C3.18632 12 0.5 9.31368 0.5 6C0.5 2.68632 3.18632 0 6.5 0ZM6.5 0.84C3.6548 0.84 1.34 3.1548 1.34 6C1.34 8.8452 3.6548 11.16 6.5 11.16C9.3452 11.16 11.66 8.8452 11.66 6C11.66 3.1548 9.3452 0.84 6.5 0.84ZM7.90018 4.00596C8.06422 3.84204 8.33002 3.84192 8.49406 4.00596C8.6581 4.17 8.6581 4.43592 8.49406 4.59996L7.0939 6L8.49406 7.40004C8.6581 7.56408 8.6581 7.83 8.49406 7.99404C8.4121 8.076 8.30458 8.11704 8.19706 8.11704C8.08966 8.11704 7.98214 8.076 7.90018 7.99404L6.50002 6.594L5.09986 7.99404C5.01778 8.076 4.91038 8.11704 4.80286 8.11704C4.69546 8.11704 4.58794 8.076 4.50598 7.99404C4.34182 7.83 4.34182 7.56408 4.50598 7.40004L5.90614 6L4.50598 4.59996C4.34182 4.43592 4.34182 4.17 4.50598 4.00596C4.66978 3.84192 4.93582 3.84204 5.09986 4.00596L6.50002 5.406L7.90018 4.00596Z' fill='%23E86421'/%3E%3C/svg%3E%0A"; $svg-shop-list-button-orange-availability: "data:image/svg+xml,%3Csvg viewBox='0 0 13 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.5 0C9.81368 0 12.5 2.68632 12.5 6C12.5 9.31368 9.81368 12 6.5 12C3.18632 12 0.5 9.31368 0.5 6C0.5 2.68632 3.18632 0 6.5 0ZM6.5 0.84C3.6548 0.84 1.34 3.1548 1.34 6C1.34 8.8452 3.6548 11.16 6.5 11.16C9.3452 11.16 11.66 8.8452 11.66 6C11.66 3.1548 9.3452 0.84 6.5 0.84ZM7.90018 4.00596C8.06422 3.84204 8.33002 3.84192 8.49406 4.00596C8.6581 4.17 8.6581 4.43592 8.49406 4.59996L7.0939 6L8.49406 7.40004C8.6581 7.56408 8.6581 7.83 8.49406 7.99404C8.4121 8.076 8.30458 8.11704 8.19706 8.11704C8.08966 8.11704 7.98214 8.076 7.90018 7.99404L6.50002 6.594L5.09986 7.99404C5.01778 8.076 4.91038 8.11704 4.80286 8.11704C4.69546 8.11704 4.58794 8.076 4.50598 7.99404C4.34182 7.83 4.34182 7.56408 4.50598 7.40004L5.90614 6L4.50598 4.59996C4.34182 4.43592 4.34182 4.17 4.50598 4.00596C4.66978 3.84192 4.93582 3.84204 5.09986 4.00596L6.50002 5.406L7.90018 4.00596Z' fill='%23E86421'/%3E%3C/svg%3E%0A";

View file

@ -235,3 +235,6 @@ $border-input-focus: 2.5px solid $heritage-blue-primary;
$svg-calendar-error-fill-color: $alert-red-bg; $svg-calendar-error-fill-color: $alert-red-bg;
$svg-calendar-error-stroke-color: '%23db0020'; // URL encoded #db0020 $svg-calendar-error-stroke-color: '%23db0020'; // URL encoded #db0020
// Swing easing to replicate Heritage's slideVisible
$transition-swing: all 400ms cubic-bezier(.02, .01, .47, 1);