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/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/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/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"> @@ -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..7773a132 100644 --- a/src/layouts/payment-method/payment-method.spec.js +++ b/src/layouts/payment-method/payment-method.spec.js @@ -48,15 +48,19 @@ function setupMocks({ customMountOptions = {}, queryString }, mainInitialState = }) } }; + 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(); diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 676af3ef..fee96d3a 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,7 +167,12 @@ export default { reviewDropdown, cartDropdown, paymentMethodQuestion, - alert + alert, + textBlock, + buttonQuestion, + textboxQuestion, + contactDetailsDrawer, + buttonMain }, mixins: [baseFormMixin], async beforeRouteEnter(to, from, next) { @@ -126,20 +201,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 +240,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: { @@ -245,7 +383,12 @@ export default { && contactInfoReqs); }, 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() { if (this.isPayInAdvanceDisabled) { @@ -258,7 +401,14 @@ export default { this.$refs.siteFooter.updateButtonText(newValue); }, async forwardButtonAction() { - useMainStore().savePaymentMethodChoice(this.paymentMethod); + this.mainStore.savePaymentMethodChoice(this.paymentMethod); + 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( @@ -284,10 +434,56 @@ 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 ); + }, + 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'; } } }; @@ -295,20 +491,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 @@