diff --git a/package-lock.json b/package-lock.json index 3eaedf24..e838e486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6057,7 +6057,9 @@ } }, "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, "license": "MIT", "dependencies": { @@ -6088,14 +6090,16 @@ } }, "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, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -8088,14 +8092,16 @@ } }, "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, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -15565,14 +15571,16 @@ } }, "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, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -19434,7 +19442,9 @@ } }, "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, "license": "MIT", "dependencies": { @@ -20952,14 +20962,16 @@ } }, "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, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -21055,14 +21067,16 @@ } }, "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, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -21129,7 +21143,9 @@ "license": "MIT" }, "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, "license": "MIT", "dependencies": { diff --git a/src/assets/img/icons/chevron-circle_review_dropdown.png b/src/assets/img/icons/chevron-circle_review_dropdown.png new file mode 100644 index 00000000..c2360997 Binary files /dev/null and b/src/assets/img/icons/chevron-circle_review_dropdown.png differ diff --git a/src/constants/bailoutCode.js b/src/constants/bailoutCode.js index f108570a..e6728697 100644 --- a/src/constants/bailoutCode.js +++ b/src/constants/bailoutCode.js @@ -1,3 +1,4 @@ +// When adding a new bailout, the Digital Consumer Order Service must also be updated to map the new bailout code to the SV2BailoutCode const bailoutCode = Object.freeze({ Unknown: 0, SaveSessionError: 1, @@ -15,7 +16,8 @@ const bailoutCode = Object.freeze({ VehicleYMMSLookupError: 13, ApplicationError: 14, ApiError: 15, - RouterError: 16 + RouterError: 16, + CoverageCancelledByUser: 17 }); export default bailoutCode; diff --git a/src/constants/bailoutMessage.js b/src/constants/bailoutMessage.js index 77292a1e..8894f05c 100644 --- a/src/constants/bailoutMessage.js +++ b/src/constants/bailoutMessage.js @@ -93,7 +93,11 @@ const bailoutMessage = Object.freeze({ code: bailoutCode.VehicleYMMSLookupError, message: getMessage(year, make, model, style) }; - } + }, + CoverageCancelledByUser: () => ({ + code: bailoutCode.CoverageCancelledByUser, + message: 'User canceled claim on ITAC/No Comp coverage statement.' + }) }); export default bailoutMessage; diff --git a/src/constants/progress-bar-mapper.js b/src/constants/progress-bar-mapper.js index efb6225d..c47c738a 100644 --- a/src/constants/progress-bar-mapper.js +++ b/src/constants/progress-bar-mapper.js @@ -60,10 +60,11 @@ export const pageProgressMapper = { percent: 85 }, 'payment-method': { - percent: 90 + percent: 95 }, 'payment-page': { - percent: 95 + // No progress bar on this page + percent: 0 }, 'tpa-search': { percent: 80 diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue index eaa5e343..e74b1708 100644 --- a/src/digital-components/textbox-question/textbox-question.vue +++ b/src/digital-components/textbox-question/textbox-question.vue @@ -47,7 +47,7 @@ :data-bs-toggle="includeSelectIcon ? 'modal' : ''" :data-bs-target="'#' + cmsWidgetName" @change="handleChange" - @blur="handleChange" + @blur="blurPlus" @focus="$emit('focus', $event.target.value)" @paste="trimOnPaste" @drop="trimOnPaste" /> @@ -92,7 +92,7 @@ export default { buttonMain }, props: { - alwaysEmitFieldErrorOnClick: { + alwaysEmitFieldErrorOnEvent: { type: Boolean, default: false }, @@ -222,6 +222,12 @@ export default { this.$emit('textboxQuestionEvent.inputIdAssigned', this.inputId); }, methods: { + blurPlus() { + this.handleChange(this.value); + if (this.alwaysEmitFieldErrorOnEvent) { + this.$emit('field-has-error', !this.meta.valid); + } + }, trimOnPaste(evt) { evt.stopPropagation(); evt.preventDefault(); @@ -258,7 +264,7 @@ export default { if (result.valid) { this.handleChange(this.value); this.$emit('click-event'); - } else if (this.alwaysEmitFieldErrorOnClick) { + } else if (this.alwaysEmitFieldErrorOnEvent) { this.$emit('field-has-error', true); } } @@ -297,6 +303,18 @@ input::-webkit-date-and-time-value { } .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 { color: $black; font-weight: 600; diff --git a/src/global-methods.js b/src/global-methods.js index 280f318d..7f7ad72b 100644 --- a/src/global-methods.js +++ b/src/global-methods.js @@ -94,10 +94,11 @@ export default { } 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) { - global.bailoutOnAxiosError({ url, error }); + global.bailoutOnAxiosError(errorData); } } return reject(error.response); diff --git a/src/global-methods.spec.js b/src/global-methods.spec.js index fbb084ff..e5e82e45 100644 --- a/src/global-methods.spec.js +++ b/src/global-methods.spec.js @@ -21,7 +21,8 @@ global.$logger = { function setupMocksForHttpClient({ endpoint = null, isError = false, - additionalData = null + additionalData = null, + bailoutOnError = false }) { // Clear node module axios.mockClear(); @@ -57,7 +58,8 @@ function setupMocksForHttpClient({ return { endpoint, - logApiCall: true + logApiCall: true, + bailoutOnError }; } @@ -92,3 +94,21 @@ it('Global Methods - Call Http Client - Should Reject Promise', () => { 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); + }); +}); \ No newline at end of file diff --git a/src/helpers/cart-helper.js b/src/helpers/cart-helper.js index af2cb1b9..405dea57 100644 --- a/src/helpers/cart-helper.js +++ b/src/helpers/cart-helper.js @@ -19,11 +19,12 @@ export function getLineItems(order) { * @returns {[]} array of service line items */ export function getServiceLineItems(order) { - const { supportingItems, glassParts, otherParts } = getLineItems(order); + const { supportingItems, glassParts, otherParts, feeItems } = getLineItems(order); return [ ...(supportingItems ?? []), ...(glassParts ?? []), - ...(otherParts ?? []) + ...(otherParts ?? []), + ...(feeItems ?? []) ]; } @@ -33,10 +34,9 @@ export function getServiceLineItems(order) { * @returns {[]} array of non-service line items */ export function getNonServiceLineItems(order) { - const { vaps, feeItems } = getLineItems(order); + const { vaps } = getLineItems(order); return [ - ...(vaps ?? []), - ...(feeItems ?? []) + ...(vaps ?? []) ]; } @@ -142,3 +142,18 @@ export function getSalesTax(order) { export function getCartTotal(order) { 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); +} \ No newline at end of file diff --git a/src/helpers/cart-helper.spec.js b/src/helpers/cart-helper.spec.js index aec728c0..5a70feab 100644 --- a/src/helpers/cart-helper.spec.js +++ b/src/helpers/cart-helper.spec.js @@ -93,10 +93,12 @@ describe('cart-helper', () => { const result = getServiceLineItems(order); // Assert - expect(result).toHaveLength(3); + expect(result).toHaveLength(5); expect(result).toContain(supportingLineItem); expect(result).toContain(glassLineItem); expect(result).toContain(otherLineItem); + expect(result).toContain(mobileFeeLineItem); + expect(result).toContain(recycleFeeLineItem); }); test('Returns empty array when line items are null', () => { @@ -124,10 +126,8 @@ describe('cart-helper', () => { const result = getNonServiceLineItems(order); // Assert - expect(result).toHaveLength(3); + expect(result).toHaveLength(1); expect(result).toContain(vapsLineItem); - expect(result).toContain(mobileFeeLineItem); - expect(result).toContain(recycleFeeLineItem); }); test('Returns empty array when line items are null', () => { @@ -403,7 +403,7 @@ describe('cart-helper', () => { describe.each([ [330, coverageType.ITAC], [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)}`, () => { // Arrange const order = { @@ -480,7 +480,7 @@ describe('cart-helper', () => { describe.each([ [21, coverageType.ITAC], [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)}`, () => { // Arrange const order = { @@ -565,7 +565,7 @@ describe('cart-helper', () => { describe.each([ [351, coverageType.ITAC], [351, coverageType.NO_COMP], - [325, coverageType.Deductible] + [164, coverageType.Deductible] ])('getCartTotal items', (expected, type) => { test(`Returns ${expected} cart total when verified ${getEnumName(coverageType, type)}`, () => { // Arrange diff --git a/src/helpers/logger.js b/src/helpers/logger.js index e4f23353..fc22939a 100644 --- a/src/helpers/logger.js +++ b/src/helpers/logger.js @@ -32,9 +32,16 @@ export class Logger { } formatLogEntry(message, details) { - return `Application: ${applicationConfig.APPLICATION_NAME}\n${message}\n${ - details ? JSON.stringify(details, undefined, 2) : '' - }`; + let json = ''; + 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) { diff --git a/src/helpers/order-helper.js b/src/helpers/order-helper.js index f1ab7c72..cbc5c890 100644 --- a/src/helpers/order-helper.js +++ b/src/helpers/order-helper.js @@ -4,8 +4,8 @@ import submitType from '@/constants/submit-type'; /* Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing */ -async function saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA }) { - const savedSessionInfo = await store.saveSession({ submitAfterSave, createWorkOrderNumberForPIA }); +async function saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError }) { + const savedSessionInfo = await store.saveSession({ submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError }); if (savedSessionInfo) { 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 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 saveSessionPromise = store.applicationUser.saveSessionPromise - ? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA })) - : saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA }); + ? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError })) + : saveSessionHelper(store, { submitAfterSave, createWorkOrderNumberForPIA, bailoutOnError }); store.setSaveSessionPromise(saveSessionPromise); @@ -38,7 +38,8 @@ export async function submitWorkOrder({ submitType }) { store.resetSubmittedOrder(); await saveSession({ shouldAwaitSaveSessionQueue: true, - submitAfterSave: true + submitAfterSave: true, + bailoutOnError: true }); await store.createSubmittedOrder(submitType); } diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js index 7e10ccd2..72572256 100644 --- a/src/helpers/service-location-helper.js +++ b/src/helpers/service-location-helper.js @@ -1,4 +1,6 @@ import { useMainStore } from '@/store'; +import { AppointmentTypeStrings } from '@/constants/schedule-constants'; +import settleAllPromises from '@/helpers/layout-helper'; function processZipCodeResults(request) { return request.then((serviceZipValidationResponse) => ({ @@ -29,7 +31,7 @@ export async function getPricedMobileFeePart(serviceZipCode) { // Get the Mobile Fee Part const mobileFeePart = await useMainStore().getMobileFeePart(); - if (mobileFeePart.data == null || mobileFeePart.data === '') { + if (mobileFeePart?.data == null || mobileFeePart.data === '') { return Promise.resolve(null); } @@ -40,17 +42,6 @@ export async function getPricedMobileFeePart(serviceZipCode) { 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( startDate, endDate, @@ -69,3 +60,45 @@ export async function getAvailabilityRating( 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); +} \ No newline at end of file diff --git a/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap b/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap index 2f061236..8aa5504e 100644 --- a/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap +++ b/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap @@ -2,7 +2,6 @@ exports[`cart-dropdown component initial data rendered as expected 1`] = ` Object { - "RECYCLING_MODAL_REF_NAME": "RecycleModal", "cartItemType": Object { "MOBILE_FEE": "MOBILE FEE", "RECYCLE_FEE": "RECYCLE FEE", @@ -16,6 +15,7 @@ Object { "deductible": "DeductibleWidget", "guaranteeText": "GuaranteeCartItemTextWidget", "mobileFee": "MobileServiceWidget", + "recalibration": "RecalibrationWidget", "recycleFee": "RecycleFeeWidget", "salesTax": "SalesTaxWidget", "servicePackage": "ServicePackageTitle", diff --git a/src/iss-components/cart-dropdown/cart-dropdown.spec.js b/src/iss-components/cart-dropdown/cart-dropdown.spec.js index 69fbabb9..b1ca9cdd 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.spec.js +++ b/src/iss-components/cart-dropdown/cart-dropdown.spec.js @@ -73,10 +73,22 @@ describe('cart-dropdown component', () => { expect(wrapper.vm.$data).toMatchSnapshot(); }); describe('displays', () => { - test('cart table', () => { + test('cart table when not deductible only', () => { // Arrange 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 const cartTable = wrapper.find(reference); @@ -84,9 +96,9 @@ describe('cart-dropdown component', () => { // Assert expect(cartTable.exists()).toBeTruthy(); }); - test('cart deductible when showDeductibleCartItem is true', () => { + test('deductible-box when deductible only', () => { // Arrange - const reference = '#deductible-label'; + const reference = '.deductible-box'; const storeData = { order: { insuranceCoverage: { @@ -127,9 +139,9 @@ describe('cart-dropdown component', () => { // Assert expect(cartBasePrice.exists()).toBeTruthy(); }); - test('non service package cart items', () => { + test('additional cart items', () => { // Arrange - const reference = '.non-packaged.cart-item'; + const reference = '.cart-item'; const part1 = { partType: 'apple' }; const part2 = { partType: 'banana' }; const part3 = { partType: 'orange' }; @@ -155,38 +167,27 @@ describe('cart-dropdown component', () => { const { wrapper } = getMountedComponent(storeData, {}, {}); // Act - const nonPackagedCartItems = wrapper.find(reference); + const cartItems = wrapper.findAll(reference); // 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 - const reference = '#recycle-fee-label'; - const isExpanded = true; - const initialPropsData = { isExpanded }; + const reference = '.cart-footer'; + const part1 = { partType: partTypeStrings.FRONT_WIPER }; const storeData = { order: { + insuranceCoverage: { + coverageStatus: coverageStatuses.VERIFIED, + coverageType: coverageType.Deductible + }, lineItems: { - feeItems: [ - { partNumber: partNumberStrings.RECYCLE_FEE } - ] + vaps: [part1], } } }; - const { wrapper } = getMountedComponent(storeData, {}, initialPropsData); - - // 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({}, {}, {}); + const { wrapper } = getMountedComponent(storeData, {}, {}); // Act const cartFooter = wrapper.find(reference); @@ -197,7 +198,19 @@ describe('cart-dropdown component', () => { test('subtotal', () => { // Arrange 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 const subtotal = wrapper.find(reference); @@ -208,7 +221,19 @@ describe('cart-dropdown component', () => { test('tax', () => { // Arrange 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 const salesTax = wrapper.find(reference); @@ -222,8 +247,16 @@ describe('cart-dropdown component', () => { const isExpanded = true; const showAsPaid = true; const initialPropsData = { isExpanded, showAsPaid }; + const part1 = { partType: partTypeStrings.FRONT_WIPER }; const storeData = { order: { + insuranceCoverage: { + coverageStatus: coverageStatuses.VERIFIED, + coverageType: coverageType.Deductible + }, + lineItems: { + vaps: [part1], + }, payment: { isPayInAdvance: true } @@ -240,7 +273,19 @@ describe('cart-dropdown component', () => { test('bottom amount due', () => { // Arrange 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 const bottomAmountDue = wrapper.find(reference); @@ -293,27 +338,6 @@ describe('cart-dropdown component', () => { // Assert 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('amountDue', () => { @@ -475,7 +499,7 @@ describe('cart-dropdown component', () => { expect(result.sort(sortByPartType)).toStrictEqual(expected.sort(sortByPartType)); }); }); - describe('servicePackageCartItems', () => { + describe('cartItems', () => { test.each([ [undefined], [null], [[]] ])('returns empty list when package contents %p', (packageContents) => { @@ -485,7 +509,7 @@ describe('cart-dropdown component', () => { const expected = []; // Act - const result = wrapper.vm.servicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result).toStrictEqual(expected); @@ -531,7 +555,7 @@ describe('cart-dropdown component', () => { const wrapper = shallowMount(cartDropdown, mountOptions); // Act - const result = wrapper.vm.servicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result.length).toBe(1); @@ -577,7 +601,7 @@ describe('cart-dropdown component', () => { const wrapper = shallowMount(cartDropdown, mountOptions); // Act - const result = wrapper.vm.servicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result.length).toBe(1); @@ -623,7 +647,7 @@ describe('cart-dropdown component', () => { const wrapper = shallowMount(cartDropdown, mountOptions); // Act - const result = wrapper.vm.servicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result.length).toBe(1); @@ -685,7 +709,7 @@ describe('cart-dropdown component', () => { const wrapper = shallowMount(cartDropdown, mountOptions); // Act - const result = wrapper.vm.servicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result.length).toBe(3); @@ -699,8 +723,6 @@ describe('cart-dropdown component', () => { name: expectedRainDefenseName })); }); - }); - describe('nonServicePackageCartItems', () => { test('when recycleFee, includes recycle fee', () => { // Arrange const storeData = { @@ -717,7 +739,9 @@ describe('cart-dropdown component', () => { const { wrapper } = getMountedComponent(storeData); // Act - const result = wrapper.vm.nonServicePackageCartItems; + const result = wrapper.vm.cartItems; + + console.log(result); // Assert expect(result).toContainEqual(expect.objectContaining({ @@ -736,163 +760,13 @@ describe('cart-dropdown component', () => { const { wrapper } = getMountedComponent(storeData); // Act - const result = wrapper.vm.nonServicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result).toContainEqual(expect.objectContaining({ 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', () => { // Arrange getPriceOfLineItems.mockImplementation(() => 123); @@ -911,143 +785,13 @@ describe('cart-dropdown component', () => { const { wrapper } = getMountedComponent(storeData); // Act - const result = wrapper.vm.nonServicePackageCartItems; + const result = wrapper.vm.cartItems; // Assert expect(result).not.toContainEqual(expect.objectContaining({ 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', () => { test.each([ diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index fa106ffe..6d48a69a 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -1,39 +1,39 @@ @@ -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 textLink from '@/ux-components/text-link/text-link.vue'; import { formatAmountInDollars } from '@/helpers/text-helper.js'; -import { getHighestFullySatisfiedTier, getPackageContents } from '@/helpers/service-package-helper.js'; // Constants import { experimentSettings } from '@/constants/experiments'; @@ -128,6 +106,7 @@ import { getCartTotal, getDeductible, getLineItems, getMobileFeeLineItem, + getRecalibrationTotal, getRecycleFeeLineItem, getSalesTax, getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp, isOrderUnverified @@ -136,19 +115,15 @@ import { getPriceOfLineItems, getTaxOfLineItems } from '@/helpers/price-calculat import { processIfStatements } from '@/helpers/cms-content-helper'; const VERIFYING_COVERAGE = 'Verifying coverage'; -const RECYCLING_MODAL_REF_NAME = 'RecycleModal'; export default { name: 'confirmation-cart', components: { - contentGroupModal, - textBlock, textLink }, props: { readOnly: Boolean, showAsPaid: Boolean, - recyclingModalCmsWidgetName: String, submittedOrder: Object }, data() { @@ -158,6 +133,7 @@ export default { amountPaid: 'AmountPaidTextWidget', deductible: 'DeductibleWidget', basePrice: 'BasePriceWidget', + recalibration: 'RecalibrationWidget', subtotal: 'SubtotalWidget', salesTax: 'SalesTaxWidget', recycleFee: 'RecycleFeeWidget', @@ -167,8 +143,7 @@ export default { warrantyText: 'WarrantyCartItemTextWidget', guaranteeText: 'GuaranteeCartItemTextWidget' }, - cartItemType, - RECYCLING_MODAL_REF_NAME + cartItemType }; }, computed: { @@ -181,9 +156,18 @@ export default { deductible() { return getDeductible(this.cartOrder); }, + isDeductibleOnly() { + return this.cartItems.length === 0; + }, showDeductibleCartItem() { return this.isUnverified || (!this.isNoComp && !this.isITAC); }, + showRecalibrationCartItem() { + return this.recalibrationPrice > 0; + }, + recalibrationPrice() { + return getRecalibrationTotal(this.cartOrder); + }, lineItems() { return getLineItems(this.cartOrder); }, @@ -191,7 +175,8 @@ export default { return getRecycleFeeLineItem(this.cartOrder); }, servicePrice() { - return getPriceOfLineItems(getServiceLineItems(this.cartOrder)) ?? 0; + const price = getPriceOfLineItems(getServiceLineItems(this.cartOrder)) ?? 0; + return price - this.recalibrationPrice; }, isUnverified() { return isOrderUnverified(this.cartOrder); @@ -229,47 +214,21 @@ export default { ]; return result; }, - vehicleDamage() { - 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() { + cartItems() { const items = []; - if (this.partTypesInServicePackage.includes(partTypeStrings.FRONT_WIPER) ?? this.frontWipersCartItem) { - items.push(this.frontWipersCartItem); + 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.isITAC || this.isNoComp) { + items.push(this.warrantyCartItem); } - if (this.partTypesInServicePackage.includes(partTypeStrings.REAR_WIPER) ?? this.rearWipersCartItem) { - items.push(this.rearWipersCartItem); + if (this.recycleFeeCartItem && !isRecycleFeeHidden) { + items.push(this.recycleFeeCartItem); } - if (this.partTypesInServicePackage.includes(partTypeStrings.RAIN_DEFENSE) ?? this.rainDefenseCartItem) { - items.push(this.rainDefenseCartItem); + if (this.mobileFeeCartItem && !isMobileFeeHidden) { + items.push(this.mobileFeeCartItem); } - return items; - }, - nonServicePackageCartItems() { const vapPartTypesInOrder = Array.from(new Set(this.lineItems.vaps?.map((vap) => vap.partType) ?? [])); - const vapPartTypesInOrderButNotPackage = vapPartTypesInOrder - .filter((partType) => !this.partTypesInServicePackage.includes(partType)) - ?? []; - const vapsCartItemsNotInPackage = vapPartTypesInOrderButNotPackage.map((partType) => { + const vapsCartItems = vapPartTypesInOrder.map((partType) => { switch (partType) { case partTypeStrings.FRONT_WIPER: return this.frontWipersCartItem; @@ -281,18 +240,7 @@ export default { return null; } }); - const items = vapsCartItemsNotInPackage.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); - } + items.push(...vapsCartItems.filter((item) => item != null)); return items; }, frontWipersCartItem() { @@ -304,12 +252,6 @@ export default { rainDefenseCartItem() { return this.getCartItemForVapsPart(partTypeStrings.RAIN_DEFENSE); }, - allCartItems() { - return [ - ...this.servicePackageCartItems, - ...this.nonServicePackageCartItems - ]; - }, amountDueLabel() { return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT); }, @@ -327,27 +269,15 @@ export default { this.getCustomValueFromString ); }, + recalibrationLabel() { + return this.getCmsContent(this.widget.recalibration, widgetFields.TEXT_BLOCK_WIDGET.TEXT); + }, subtotalLabel() { return this.getCmsContent(this.widget.subtotal, widgetFields.TEXT_BLOCK_WIDGET.TEXT); }, salesTaxLabel() { 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() { return this.recycleFeeLineItem ? this.getCartItem( @@ -452,8 +382,6 @@ export default { diff --git a/src/layouts/tpa-submit/contact-details-drawer/__snapshots__/contact-details-drawer.spec.js.snap b/src/iss-components/contact-details-drawer/__snapshots__/contact-details-drawer.spec.js.snap similarity index 88% rename from src/layouts/tpa-submit/contact-details-drawer/__snapshots__/contact-details-drawer.spec.js.snap rename to src/iss-components/contact-details-drawer/__snapshots__/contact-details-drawer.spec.js.snap index 842c5af2..effbe1a8 100644 --- a/src/layouts/tpa-submit/contact-details-drawer/__snapshots__/contact-details-drawer.spec.js.snap +++ b/src/iss-components/contact-details-drawer/__snapshots__/contact-details-drawer.spec.js.snap @@ -2,6 +2,7 @@ exports[`contact-details-drawer snapshot matches returns the initial data 1`] = ` Object { + "detailsSaved": false, "emailAddress": "fred.tay@gmail.com", "extension": null, "firstName": "Frederick", @@ -15,8 +16,8 @@ Object { "rules": Object { "emailAddress": "email-required|email-address-format", "extension": "extension-format", - "firstName": "first-name-required", - "lastName": "last-name-required", + "firstName": "policyholder-first-name-required", + "lastName": "policyholder-last-name-required", "phoneNumber": "phone-number-required|phone-number-format", }, "widget": Object { diff --git a/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.spec.js b/src/iss-components/contact-details-drawer/contact-details-drawer.spec.js similarity index 98% rename from src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.spec.js rename to src/iss-components/contact-details-drawer/contact-details-drawer.spec.js index 04fe6ce6..dd30a043 100644 --- a/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.spec.js +++ b/src/iss-components/contact-details-drawer/contact-details-drawer.spec.js @@ -1,7 +1,7 @@ // Components import { shallowMount } from '@vue/test-utils'; 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 import { getMountOptions } from '@/helpers/unit-test-helper.js'; diff --git a/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue b/src/iss-components/contact-details-drawer/contact-details-drawer.vue similarity index 82% rename from src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue rename to src/iss-components/contact-details-drawer/contact-details-drawer.vue index 7d5b0572..660ae1c3 100644 --- a/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue +++ b/src/iss-components/contact-details-drawer/contact-details-drawer.vue @@ -9,25 +9,29 @@ @isModalOpened="setModalStatus" @footerButtonEvent="clickFooterButtonEvent"> + + + + \ No newline at end of file diff --git a/src/layouts/coverage-statement/coverage-statement.spec.js b/src/layouts/coverage-statement/coverage-statement.spec.js index ea913872..a8b77aa5 100644 --- a/src/layouts/coverage-statement/coverage-statement.spec.js +++ b/src/layouts/coverage-statement/coverage-statement.spec.js @@ -63,7 +63,8 @@ const loadingModalStub = { function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) { const mountOptions = getMountOptions({ router: { - navigate: jest.fn() + navigate: jest.fn(), + navigateBailout: jest.fn() } }); @@ -439,6 +440,109 @@ describe('coverageStatement.vue', () => { 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('arePagePrerequisitesValid', () => { @@ -587,7 +691,7 @@ describe('coverageStatement.vue', () => { undefined ); }); - test('If Verified ITAC, selected Cancel, navigate forward w/ CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP', () => { + test('If Verified ITAC, selected Cancel, navigateBailout', () => { // Arrange const deductible = servicePrice + 1; const mainInitialState = { @@ -613,11 +717,7 @@ describe('coverageStatement.vue', () => { wrapper.vm.cancelClaim(); // Assert - expect(wrapper.vm.$router.navigate) - .toHaveBeenCalledWith( - navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP, - undefined - ); + expect(wrapper.vm.$router.navigateBailout).toHaveBeenCalled(); }); test('If No Comp and selected Safelite, navigate forward with CLICKED_FORWARD_WITH_SAFELITE scenario', () => { // Arrange @@ -650,7 +750,7 @@ describe('coverageStatement.vue', () => { 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 const mainInitialState = { order: { @@ -675,11 +775,7 @@ describe('coverageStatement.vue', () => { wrapper.vm.cancelClaim(); // Assert - expect(wrapper.vm.$router.navigate) - .toHaveBeenCalledWith( - navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP, - undefined - ); + expect(wrapper.vm.$router.navigateBailout).toHaveBeenCalled(); }); }); describe('openCancelClaimModal', () => { diff --git a/src/layouts/coverage-statement/coverage-statement.vue b/src/layouts/coverage-statement/coverage-statement.vue index e0475f47..2026f289 100644 --- a/src/layouts/coverage-statement/coverage-statement.vue +++ b/src/layouts/coverage-statement/coverage-statement.vue @@ -70,6 +70,10 @@ class="d-flex justify-content-center cost mb-0 cost-underline"> {{ formatAmountInDollars(totalServicePrice) }} + 0); } }, watch: { @@ -386,18 +397,7 @@ export default { // We only call the ITAC pricing endpoint if we are not repair or we are NoComp if (!this.isRepair || this.mainStore.isNoComp) { - 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); - }); + const pricingResults = await useMainStore().getITACPriceOrderItems(availableLineItems); this.setBaseServiceLineItems(pricingResults); } }, @@ -408,8 +408,7 @@ export default { this.mainStore.updateIsSafeliteProvider(true); this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE); } else { - this.mainStore.setBailout(bailoutMessage.coverageStatementInvalidState()); - this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_INVALID_STATE); + this.$router.navigateBailout(bailoutMessage.coverageStatementInvalidState()); } }, navigateWithScenario(scenario) { @@ -463,8 +462,7 @@ export default { }, cancelClaim() { this.mainStore.updateIsSafeliteProvider(false); - this.mainStore.setBailout(bailoutMessage.RequestCallback()); - this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP); + this.$router.navigateBailout(bailoutMessage.CoverageCancelledByUser()); } } }; diff --git a/src/layouts/duplicate-check/duplicate-check.spec.js b/src/layouts/duplicate-check/duplicate-check.spec.js index 10be35e2..95346c26 100644 --- a/src/layouts/duplicate-check/duplicate-check.spec.js +++ b/src/layouts/duplicate-check/duplicate-check.spec.js @@ -357,11 +357,15 @@ describe('duplicateCheck.vue', () => { useMainStore().loadSession = jest.fn().mockImplementation(() => Promise.reject(error)); // Act - await wrapper.vm.forwardButtonAction(); // 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.$router.navigate).toHaveBeenCalledTimes(1); }); test('coverageType deductible and policy vehicles returned => CLICKED_FORWARD_POLICY_VERIFIED_WITH_VEHICLES', async () => { // Arrange diff --git a/src/layouts/duplicate-check/duplicate-check.vue b/src/layouts/duplicate-check/duplicate-check.vue index 1cac5a1e..3c4ead5f 100644 --- a/src/layouts/duplicate-check/duplicate-check.vue +++ b/src/layouts/duplicate-check/duplicate-check.vue @@ -140,18 +140,14 @@ export default { return; } - try { - const selectedReferral = - this.mainStore.applicationUser.duplicateOrders.find((o) => o.correlationId === this.selectedAnswer); + const selectedReferral = + this.mainStore.applicationUser.duplicateOrders.find((o) => o.correlationId === this.selectedAnswer); - if (selectedReferral) { - await this.mainStore.loadSession(selectedReferral); - } - } catch (err) { - console.error(`Error on loading session from duplicate check ${err}`); - } finally { - this.navigateForward(); + if (selectedReferral) { + await this.mainStore.loadSession(selectedReferral); } + + this.navigateForward(); }, navigateForward() { this.mainStore.updateDuplicateCheckVisited(true); diff --git a/src/layouts/entry-page/entry-page.vue b/src/layouts/entry-page/entry-page.vue index e9880117..47031709 100644 --- a/src/layouts/entry-page/entry-page.vue +++ b/src/layouts/entry-page/entry-page.vue @@ -34,44 +34,51 @@ export default { computed: { }, 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 { - // Check cookie - const issCookie = getISSCookie(); - if (issCookie !== null && issCookie.VehicleMake && issCookie.VehicleModel) { - const clientParentAccountNumber = clientData.parentAccountNumber; - const cookieParentAccountNumber = issCookie.ReferralParentAccountNumber; + const queryStringParams = this.parseQueryParms(); - if (clientParentAccountNumber === cookieParentAccountNumber) { - const savedSessionTimeStamp = new Date(issCookie.SavedSessionTimeoutDate); - const isSavedSessionTimedOut = new Date(new Date().toUTCString()) > savedSessionTimeStamp; + const { isAuthorized, clientData, decryptedParams } = await this.validateClientTagOnEntry(queryStringParams); - if (!isSavedSessionTimedOut) { - this.mainStore.issConfig.enableContinueFromCookie = true; + 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 { + // 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); } - } catch { - updateOrCreateISSCookie(true); - } - if (clientData.parameters?.length > 0) { - const finalParams = this.combineClientParameters(clientData.parameters, { ...queryStringParams, ...decryptedParams }); - this.populateStoreItemsFromParams(finalParams); + if (clientData.parameters?.length > 0) { + const finalParams = this.combineClientParameters(clientData.parameters, { ...queryStringParams, ...decryptedParams }); + 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; @@ -151,37 +158,33 @@ export default { this.mainStore.issConfig.isCoverageEnabled = data.coverageEnabled; this.mainStore.issConfig.siteType = data.siteType; - try { - if (data.clientFlags) { - const clientFlags = JSON.parse(data.clientFlags); + if (data.clientFlags) { + const clientFlags = JSON.parse(data.clientFlags); - if (clientFlags.TPAEnabled) { - this.mainStore.issConfig.enableTPAFlow = true; - } - - if (clientFlags.ClientFullName != null) { - this.mainStore.issConfig.clientFullName = clientFlags.ClientFullName; - } - - if (clientFlags.ClientDisplayName != null) { - this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName; - this.mainStore.issConfig.clientPossessiveName = toPossessive(clientFlags.ClientDisplayName); - } - - if (clientFlags.ClientPossessiveName != null) { - this.mainStore.issConfig.clientPossessiveName = clientFlags.ClientPossessiveName; - } - - if (clientFlags.ClaimRegistrationRequired) { - this.mainStore.issConfig.isClaimRegistrationRequired = true; - } - - if (clientFlags.EnableNoCompQuote) { - this.mainStore.issConfig.enableNoCompQuote = true; - } + if (clientFlags.TPAEnabled) { + this.mainStore.issConfig.enableTPAFlow = true; + } + + if (clientFlags.ClientFullName != null) { + this.mainStore.issConfig.clientFullName = clientFlags.ClientFullName; + } + + if (clientFlags.ClientDisplayName != null) { + this.mainStore.issConfig.clientDisplayName = clientFlags.ClientDisplayName; + this.mainStore.issConfig.clientPossessiveName = toPossessive(clientFlags.ClientDisplayName); + } + + if (clientFlags.ClientPossessiveName != null) { + this.mainStore.issConfig.clientPossessiveName = clientFlags.ClientPossessiveName; + } + + if (clientFlags.ClaimRegistrationRequired) { + this.mainStore.issConfig.isClaimRegistrationRequired = true; + } + + if (clientFlags.EnableNoCompQuote) { + this.mainStore.issConfig.enableNoCompQuote = true; } - } catch (e) { - console.error(`Error parsing client flags: ${e}`); } }, combineClientParameters(configParams, queryStringParams) { diff --git a/src/layouts/molding-questions/molding-questions.vue b/src/layouts/molding-questions/molding-questions.vue index cb0f4f04..44d6af81 100644 --- a/src/layouts/molding-questions/molding-questions.vue +++ b/src/layouts/molding-questions/molding-questions.vue @@ -148,8 +148,7 @@ export default { this.navigateForward(partsOrQuestions, null); }, requestCallbackBailout() { - this.mainStore.setBailout(bailoutMessage.RequestCallback()); - this.$router.navigate(navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT, this.$route); + this.$router.navigateBailout(bailoutMessage.RequestCallback()); } } }; diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index 34ed6789..ab3d2bbe 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -94,6 +94,7 @@ diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 1230016a..9bed6345 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -148,8 +148,7 @@ export default { this.navigateForward(glassPartsForStore, null); }, requestCallbackBailout() { - this.mainStore.setBailout(bailoutMessage.RequestCallback()); - this.$router.navigate(navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT, this.$route); + this.$router.navigateBailout(bailoutMessage.RequestCallback()); } }, }; diff --git a/src/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button.vue b/src/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button.vue index 0f834cbc..6bd161eb 100644 --- a/src/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button.vue +++ b/src/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button.vue @@ -2,32 +2,30 @@ -
+ buttonWrapperClasses="list-group base-input-button list-button payment-method d-flex flex-column w-100 no-hover"> +
+
+ + + + {{ getInlineAltText(token) }} + + + {{ token }} + + +
-
-

- - - - {{ getInlineAltText(token) }} - - - {{ token }} - - -

-
@@ -74,52 +72,41 @@ export default { diff --git a/src/layouts/payment-method/payment-method-question/payment-method-question.vue b/src/layouts/payment-method/payment-method-question/payment-method-question.vue index d8376879..33d82adf 100644 --- a/src/layouts/payment-method/payment-method-question/payment-method-question.vue +++ b/src/layouts/payment-method/payment-method-question/payment-method-question.vue @@ -19,6 +19,7 @@ import buttonQuestion from '@/digital-components/button-question/button-question.vue'; import widgetFields from '@/constants/cms-widget-fields.js'; import paymentMethodListButton from './payment-method-list-button/payment-method-list-button.vue'; +import { markRaw } from 'vue'; export default { name: 'payment-method-question', @@ -33,7 +34,7 @@ export default { emits: ['update:modelValue'], data() { return { - paymentMethodListButton + paymentMethodListButton: markRaw(paymentMethodListButton) }; }, computed: { diff --git a/src/layouts/payment-method/payment-method.spec.js b/src/layouts/payment-method/payment-method.spec.js index 5acf7f33..edee79f9 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -45,18 +45,23 @@ function setupMocks({ customMountOptions = {}, queryString }, mainInitialState = } return 'false'; - }) + }), + savePageDataToStore: jest.fn() } }; + const cmsMixin = { + methods: { + getCmsContent: jest.fn() + } + } mountOptions.global.plugins = [testingPinia]; - mountOptions.global.mixins = [mockMixin]; + mountOptions.mixins = [mockMixin, cmsMixin]; const wrapper = shallowMount(paymentMethod, mountOptions); return wrapper; } - beforeEach(() => { const store = useMainStore(); const defaultState = getDefaultState(); @@ -80,33 +85,10 @@ describe('payment-method.vue', () => { const wrapper = setupMocks({}, store); // Act - const paymethod = wrapper.vm.getPaymentMethodFromStore(); + wrapper.vm.initializeComponent(); // Assert - expect(paymethod).toBe(payLaterMethod); - }); - test('getting payment method when method is pay in advance', async () => { - // Arrange - const payInAdvanceMethod = paymentMethods.CREDIT_CARD; - const store = { - order: { - payment: { - paymentMethod: payInAdvanceMethod - }, - insuranceCoverage: { - coverageStatus: coverageStatuses.VERIFIED, - coverageType: coverageType.Deductible - }, - currentDeductible: 1000 - } - }; - const wrapper = setupMocks({}, store); - - // Act - const paymethod = wrapper.vm.getPaymentMethodFromStore(); - - // Assert - expect(paymethod).toBe(payInAdvanceMethod); + expect(wrapper.vm.paymentMethod).toBe(payLaterMethod); }); test('getting PAY_AT_TIME_OF_SERVICE when deductible is 0', async () => { // Arrange @@ -120,16 +102,18 @@ describe('payment-method.vue', () => { coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageType.Deductible }, - currentDeductible: 0 + currentDeductible: { + replace: 0 + } } }; const wrapper = setupMocks({}, store); // Act - const paymethod = wrapper.vm.getPaymentMethodFromStore(); + wrapper.vm.initializeComponent(); // Assert - expect(paymethod).toBe(payInAdvanceMethod); + expect(wrapper.vm.paymentMethod).toBe(payInAdvanceMethod); }); test('getting PAY_AT_TIME_OF_SERVICE when Unverified', async () => { // Arrange @@ -143,16 +127,18 @@ describe('payment-method.vue', () => { coverageStatus: coverageStatuses.PENDING, coverageType: coverageType.Deductible }, - currentDeductible: 1000 + currentDeductible: { + replace: 1000 + } } }; const wrapper = setupMocks({}, store); // Act - const paymethod = wrapper.vm.getPaymentMethodFromStore(); + wrapper.vm.initializeComponent(); // Assert - expect(paymethod).toBe(payInAdvanceMethod); + expect(wrapper.vm.paymentMethod).toBe(payInAdvanceMethod); }); test('getting PAY_AT_TIME_OF_SERVICE when PIA Experiment disabled', async () => { // Arrange @@ -176,16 +162,18 @@ describe('payment-method.vue', () => { coverageStatus: coverageStatuses.VERIFIED, coverageType: coverageType.Deductible }, - currentDeductible: 1000 + currentDeductible: { + replace: 1000 + } } }; const wrapper = setupMocks({}, store, mixin); // Act - const paymethod = wrapper.vm.getPaymentMethodFromStore(); + wrapper.vm.initializeComponent(); // Assert - expect(paymethod).toBe(payInAdvanceMethod); + expect(wrapper.vm.paymentMethod).toBe(payInAdvanceMethod); }); }); diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index ba1f761b..90144474 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -10,23 +10,32 @@ ref="siteHeader" cmsWidgetName="SiteHeaderWidget" />
-
-
+
+
+ + class="subheader" + cmsWidgetName="SiteSubHeaderWidget" />
-
- -
- -
+
+ +
+ {{ cartHeaderText }} + {{ cartTotal }} +
+
+ +
+
- +
+
+
{{ smsOptInHeaderText }}
+ + +
+
+ +
+
+
+
+
+ +
+
{{ wiperOfferPanel.header }}
+
{{ wiperOfferPanel.body }}
+
+
+
@@ -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 paymentMethodQuestion from '@/layouts/payment-method/payment-method-question/payment-method-question.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 import settleAllPromises from '@/helpers/layout-helper'; @@ -86,6 +152,10 @@ import { experimentSettings } from '@/constants/experiments'; import submitType from '@/constants/submit-type'; import routerParams from '@/router/router-constants/router-params'; 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 { name: 'payment-method', @@ -97,12 +167,18 @@ export default { reviewDropdown, cartDropdown, paymentMethodQuestion, - alert + alert, + textBlock, + buttonQuestion, + textboxQuestion, + contactDetailsDrawer, + buttonMain }, mixins: [baseFormMixin], async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.issPage); + const pageData = useMainStore().pageData(to.query.issPage); const promiseResultMap = [ { @@ -117,7 +193,7 @@ export default { next((vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.initializeComponent(); + vm.initializeComponent(pageData); vm.updateFooterButtonText(vm.customCallToActionButtonCopy); }); }, @@ -126,20 +202,29 @@ export default { paymentMethod: null, widget: { paymentMethod: 'PaymentMethodWidget', - paymentMethodApplePay: 'PaymentMethodWidgetApplePay' + paymentMethodApplePay: 'PaymentMethodWidgetApplePay', + amountDue: 'AmountDueCartHeaderTextWidget', + payAtAppointment: 'PayAtAppointmentTextWidget', + smsOptInHeader: 'SMSOptInHeaderWidget', + smsOptInQuestion: 'SMSOptInQuestionWidget', + smsPhoneNumber: 'SMSPhoneNumberWidget', + smsDisclaimer: 'SMSDisclaimerWidget', + wiperOffer: 'WiperOfferPanelWidget' }, rules: { - optionRequired: globalRules.OPTION_REQUIRED - } + optionRequired: globalRules.OPTION_REQUIRED, + phoneNumber: `${globalRules.PHONE_NUMBER_REQUIRED}|${globalRules.PHONE_NUMBER_FORMAT}` + }, + smsOptIn: '', + smsPhoneNumber: '' }; }, computed: { customCallToActionButtonCopy() { switch (this.paymentMethod) { case paymentMethods.PayNow: - return 'Continue to checkout'; case paymentMethods.AFTERPAY: - return 'Continue to Afterpay'; + return 'Continue to checkout'; default: return null; } @@ -156,6 +241,60 @@ export default { }, paymentMethodWidgetName() { 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: { @@ -244,41 +383,40 @@ export default { && customerReqs && contactInfoReqs); }, - initializeComponent() { - this.paymentMethod = this.getPaymentMethodFromStore(); - }, - getPaymentMethodFromStore() { - if (this.isPayInAdvanceDisabled) { - return paymentMethods.PAY_AT_TIME_OF_SERVICE; + initializeComponent(pageData) { + if (pageData) { + this.smsOptIn = pageData.smsOptIn; + } + this.smsPhoneNumber = this.getSMSPhoneFromStore(); + if (this.isPayInAdvanceDisabled) { + console.log('Pay in Advance is disabled, defaulting to Pay at Time of Service'); + this.paymentMethod = paymentMethods.PAY_AT_TIME_OF_SERVICE; } - - return useMainStore().order.payment.paymentMethod; }, updateFooterButtonText(newValue) { this.$refs.siteFooter.updateButtonText(newValue); }, async forwardButtonAction() { - useMainStore().savePaymentMethodChoice(this.paymentMethod); - if (this.paymentMethod === paymentMethods.PAY_AT_TIME_OF_SERVICE) { - try { - await submitWorkOrder({ submitType: submitType.SAFELITE }); - this.$router.navigate( - this.navigationScenarios.CLICKED_FORWARD, - 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}`); + this.mainStore.savePaymentMethodChoice(this.paymentMethod); + this.savePageDataToStore(issPageValues.PAYMENT_METHOD, { smsOptIn: this.smsOptIn }); + if (!this.hideSMSOptIn) { + const requestTextUpdates = this.smsOptIn === 'Yes'; + this.mainStore.updateContactInfo({ requestTextUpdates }); + if (requestTextUpdates) { + this.mainStore.updatePhoneNumbers({ alternative: this.smsPhoneNumber, service: this.smsPhoneNumber }); } + } + if (this.paymentMethod === paymentMethods.PAY_AT_TIME_OF_SERVICE) { + await submitWorkOrder({ submitType: submitType.SAFELITE }); + this.$router.navigate( + this.navigationScenarios.CLICKED_FORWARD, + this.$route + ); } else { await saveSession({ createWorkOrderNumberForPIA: true, - shouldAwaitSaveSessionQueue: true + shouldAwaitSaveSessionQueue: true, + bailoutOnError: true }); this.$router.navigate( @@ -293,10 +431,53 @@ export default { const scenario = this.mainStore.isMobileAppointment ? this.navigationScenarios.CLICKED_BACK_MOBILE : this.navigationScenarios.CLICKED_BACK_INSHOP; - this.$router.navigate( + this.$router.navigateWithSpinner( scenario, this.$route ); + }, + 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 +485,145 @@ export default { diff --git a/src/layouts/payment-method/review-dropdown/review-block/review-block.vue b/src/layouts/payment-method/review-dropdown/review-block/review-block.vue index 2b7cf661..1ed4ddda 100644 --- a/src/layouts/payment-method/review-dropdown/review-block/review-block.vue +++ b/src/layouts/payment-method/review-dropdown/review-block/review-block.vue @@ -1,34 +1,55 @@ diff --git a/src/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review.vue b/src/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review.vue index e0c615df..3b0a5fee 100644 --- a/src/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review.vue +++ b/src/layouts/payment-method/review-dropdown/review-sections/schedule-review/schedule-review.vue @@ -1,7 +1,8 @@ diff --git a/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.spec.js b/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.spec.js deleted file mode 100644 index 981980d0..00000000 --- a/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.spec.js +++ /dev/null @@ -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']); - }); -}); diff --git a/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue b/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue index 56bee25e..b0500009 100644 --- a/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue +++ b/src/layouts/payment-method/review-dropdown/review-sections/vehicle-review/vehicle-review.vue @@ -1,10 +1,13 @@