From 6957bcc9515f1a1e857ddff5b4aba2b4e21bee61 Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Thu, 2 May 2024 14:16:12 -0500 Subject: [PATCH 1/6] SSR-1205 Initial Pricing Fixes --- src/helpers/cart-helper.js | 143 ++++ src/helpers/cart-helper.spec.js | 630 ++++++++++++++++++ src/helpers/price-calculator.js | 42 +- src/helpers/price-calculator.spec.js | 256 +++++-- .../cart-dropdown/cart-dropdown.spec.js | 434 +----------- .../cart-dropdown/cart-dropdown.vue | 119 +--- .../coverage-statement.spec.js | 6 +- .../coverage-statement/coverage-statement.vue | 4 +- src/layouts/payment-page/payment-page.spec.js | 7 - src/layouts/payment-page/payment-page.vue | 7 +- .../time-slot-modal-question.vue | 4 +- .../service-package-question.vue | 8 +- src/mixins/base-mixin.js | 56 -- src/router/index.js | 22 +- src/store/index.js | 158 +++-- 15 files changed, 1196 insertions(+), 700 deletions(-) create mode 100644 src/helpers/cart-helper.js create mode 100644 src/helpers/cart-helper.spec.js diff --git a/src/helpers/cart-helper.js b/src/helpers/cart-helper.js new file mode 100644 index 00000000..0875c5b6 --- /dev/null +++ b/src/helpers/cart-helper.js @@ -0,0 +1,143 @@ +import { useMainStore } from '@/store'; +import { getPriceOfLineItems, getTaxOfLineItems } from '@/helpers/price-calculator'; +import partNumberStrings from '@/constants/part-number-strings'; +import partTypeStrings from '@/constants/part-type-strings'; + +/** + * Returns the lineItems object from the order + * @param {object} order order object + * @returns {object} lineItems object from the order + */ +export function getLineItems(order) { + return order.lineItems; +} + +/** + * Returns an array containing only the service line items + * @param {object} order order object + * @returns {[]} array of service line items + */ +export function getServiceLineItems(order) { + const { supportingItems, glassParts, otherParts } = getLineItems(order); + return [ + ...(supportingItems ?? []), + ...(glassParts ?? []), + ...(otherParts ?? []) + ]; +} + +/** + * Returns an array containing only the non-service line items (Vaps and Fees) + * @param {object} order order object + * @returns {[]} array of non-service line items + */ +export function getNonServiceLineItems(order) { + const { vaps, feeItems } = getLineItems(order); + return [ + ...(vaps ?? []), + ...(feeItems ?? []) + ]; +} + +/** + * Returns an array containing all line items + * @param {object} order order object + * @returns {[]} array of all line items + */ +export function getAllLineItems(order) { + return [ + ...getServiceLineItems(order), + ...getNonServiceLineItems(order) + ]; +} + +/** + * Returns the recycle fee line item if it exists on the order + * @param {object} order order object + * @returns {{}|undefined} recycle fee line item or undefined + */ +export function getRecycleFee(order) { + return getLineItems(order).feeItems?.find((lineItem) => lineItem.partNumber === partNumberStrings.RECYCLE_FEE); +} + +/** + * Returns the mobile fee line item if it exists on the order + * @param {object} order order object + * @returns {{}|undefined} mobile fee line item or undefined + */ +export function getMobileFee(order) { + return getLineItems(order).feeItems?.find((lineItem) => lineItem.partType === partTypeStrings.MOBILE_FEE); +} + +/** + * Returns the current deductible on the order + * @param {object} order order object + * @returns {number} current deductible + */ +export function getDeductible(order) { + return order.currentDeductible ?? 0; +} + +/** + * Returns if the order is unverified + * @param {object} order order object + * @returns {boolean} order unverified state + */ +export function isOrderUnverified(order) { + return order.isUnverified ?? useMainStore().isUnverified; +} + +/** + * Returns if the order is NO COMP + * @param {object} order order object + * @returns {boolean} order NO COMP flag + */ +export function isOrderNoComp(order) { + return order.policy?.noCoverage ?? false; +} + +/** + * Returns if the order is ITAC + * @param {object} order order object + * @returns {boolean} order ITAC flag + */ +export function isOrderITAC(order) { + return order.policy?.isITAC ?? false; +} + +/** + * Returns the subtotal for the order + * @param {object} order order object + * @returns {number} sub total for the order + */ +export function getSubtotal(order) { + if (!isOrderNoComp(order) && !isOrderITAC(order)) { + return getDeductible(order) + getPriceOfLineItems(getNonServiceLineItems(order), false); + } + + return getPriceOfLineItems(getAllLineItems(order), false); +} + +/** + * Returns the sales tax for the order + * @param {object} order order object + * @returns {number} sales tax for the order + */ +export function getSalesTax(order) { + let tax = 0; + if (isOrderITAC(order) || isOrderNoComp(order)) { + tax += getTaxOfLineItems(getServiceLineItems(order)); + } + + tax += getTaxOfLineItems(getNonServiceLineItems(order)); + return tax; +} + +/** + * Returns the cart total rounded to two decimal places + * @param {object} order order object + * @returns {number} cart total for the order + */ +export function getCartTotal(order) { + return Math.round((getSubtotal(order) + getSalesTax(order)) * 100) / 100; +} diff --git a/src/helpers/cart-helper.spec.js b/src/helpers/cart-helper.spec.js new file mode 100644 index 00000000..b978f5ec --- /dev/null +++ b/src/helpers/cart-helper.spec.js @@ -0,0 +1,630 @@ +import { + getAllLineItems, + getCartTotal, + getDeductible, + getLineItems, + getMobileFee, getNonServiceLineItems, + getRecycleFee, getSalesTax, + getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp, + isOrderUnverified +} from '@/helpers/cart-helper'; +import partTypeStrings from '@/constants/part-type-strings'; +import partNumberStrings from '@/constants/part-number-strings'; + +describe('cart-helper', () => { + function createDummyItem(partNumber, price, salesTax) { + return { + partNumber, + sellingPrice: price, + kitPrice: 0, + laborAmount: 0, + salesTax + }; + } + + function createDummyFeeItem(partNumber, partType, price, salesTax) { + return { + partNumber, + partType, + sellingPrice: price, + kitPrice: 0, + laborAmount: 0, + salesTax + }; + } + + const nullLineItems = { + supportingItems: null, + glassParts: null, + otherParts: null, + vaps: null, + feeItems: null + }; + + const emptyLineItems = { + supportingItems: [], + glassParts: [], + otherParts: [], + vaps: [], + feeItems: [] + }; + + const supportingLineItem = createDummyItem('supporting', 30, 1); + const glassLineItem = createDummyItem('glass', 40, 2); + const otherLineItem = createDummyItem('other', 50, 3); + const vapsLineItem = createDummyItem('vaps', 60, 4); + const mobileFeeLineItem = createDummyFeeItem(null, partTypeStrings.MOBILE_FEE, 70, 5); + const recycleFeeLineItem = createDummyFeeItem(partNumberStrings.RECYCLE_FEE, null, 80, 6); + + const defaultLineItems = { + supportingItems: [supportingLineItem], + glassParts: [glassLineItem], + otherParts: [otherLineItem], + vaps: [vapsLineItem], + feeItems: [mobileFeeLineItem, recycleFeeLineItem] + }; + + describe('getLineItems', () => { + test('Returns line items', () => { + // Arrange + const order = { + lineItems: emptyLineItems + }; + + // Act + const result = getLineItems(order); + + // Assert + expect(result).toMatchObject(order.lineItems); + }); + }); + + describe('getServiceLineItems', () => { + test('Returns only service line items', () => { + // Arrange + const order = { + lineItems: defaultLineItems + }; + + // Act + const result = getServiceLineItems(order); + + // Assert + expect(result).toHaveLength(3); + expect(result).toContain(supportingLineItem); + expect(result).toContain(glassLineItem); + expect(result).toContain(otherLineItem); + }); + + test('Returns empty array when line items are null', () => { + // Arrange + const order = { + lineItems: nullLineItems + }; + + // Act + const result = getServiceLineItems(order); + + // Assert + expect(result).toHaveLength(0); + }); + }); + + describe('getNonServiceLineItems', () => { + test('Returns only service line items', () => { + // Arrange + const order = { + lineItems: defaultLineItems + }; + + // Act + const result = getNonServiceLineItems(order); + + // Assert + expect(result).toHaveLength(3); + expect(result).toContain(vapsLineItem); + expect(result).toContain(mobileFeeLineItem); + expect(result).toContain(recycleFeeLineItem); + }); + + test('Returns empty array when line items are null', () => { + // Arrange + const order = { + lineItems: nullLineItems + }; + + // Act + const result = getNonServiceLineItems(order); + + // Assert + expect(result).toHaveLength(0); + }); + }); + + describe('getAllLineItems', () => { + test('Returns all line items', () => { + // Arrange + const order = { + lineItems: defaultLineItems + }; + + // Act + const result = getAllLineItems(order); + + // Assert + expect(result).toHaveLength(6); + expect(result).toContain(supportingLineItem); + expect(result).toContain(glassLineItem); + expect(result).toContain(otherLineItem); + expect(result).toContain(vapsLineItem); + expect(result).toContain(mobileFeeLineItem); + expect(result).toContain(recycleFeeLineItem); + }); + + test('Returns empty array when line items are null', () => { + // Arrange + const order = { + lineItems: nullLineItems + }; + + // Act + const result = getAllLineItems(order); + + // Assert + expect(result).toHaveLength(0); + }); + }); + + describe('getRecycleFee', () => { + test('Returns recycle fee when present', () => { + // Arrange + const order = { + lineItems: defaultLineItems + }; + + // Act + const result = getRecycleFee(order); + + // Assert + expect(result).not.toBeNull(); + expect(result).toMatchObject(recycleFeeLineItem); + }); + + test('Returns undefined recycle fee when not present', () => { + // Arrange + const order = { + lineItems: emptyLineItems + }; + + // Act + const result = getRecycleFee(order); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe('getMobileFee', () => { + test('Returns mobile fee when present', () => { + // Arrange + const order = { + lineItems: defaultLineItems + }; + + // Act + const result = getMobileFee(order); + + // Assert + expect(result).not.toBeNull(); + expect(result).toMatchObject(mobileFeeLineItem); + }); + + test('Returns undefined mobile fee when not present', () => { + // Arrange + const order = { + lineItems: emptyLineItems + }; + + // Act + const result = getMobileFee(order); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe('getDeductible', () => { + test('Returns deductible on the order', () => { + // Arrange + const deductible = 100; + const order = { + currentDeductible: deductible + }; + + // Act + const result = getDeductible(order); + + // Assert + expect(result).not.toBeNull(); + expect(result).toBe(deductible); + }); + }); + + describe('isOrderUnverified', () => { + test('Returns true if order has isUnverified field and is true', () => { + // Arrange + const order = { + isUnverified: true + }; + + // Act + const result = isOrderUnverified(order); + + // Assert + expect(result).toBeTruthy(); + }); + + test('Returns false if order has isUnverified field and is false', () => { + // Arrange + const order = { + isUnverified: true + }; + + // Act + const result = isOrderUnverified(order); + + // Assert + expect(result).toBeTruthy(); + }); + }); + + describe('isOrderNoComp', () => { + test('Returns null when policy is null', () => { + // Arrange + const order = {}; + + // Act + const result = isOrderNoComp(order); + + // Assert + expect(result).toBeFalsy(); + }); + + test('Returns true when policy.NoCoverage is true', () => { + // Arrange + const order = { + policy: { + noCoverage: true + } + }; + + // Act + const result = isOrderNoComp(order); + + // Assert + expect(result).toBeTruthy(); + }); + + test('Returns false when policy.NoCoverage is false', () => { + // Arrange + const order = { + policy: { + noCoverage: false + } + }; + + // Act + const result = isOrderNoComp(order); + + // Assert + expect(result).toBeFalsy(); + }); + }); + + describe('isOrderITAC', () => { + test('Returns null when policy is null', () => { + // Arrange + const order = {}; + + // Act + const result = isOrderITAC(order); + + // Assert + expect(result).toBeFalsy(); + }); + + test('Returns true when policy.isITAC is true', () => { + // Arrange + const order = { + policy: { + isITAC: true + } + }; + + // Act + const result = isOrderITAC(order); + + // Assert + expect(result).toBeTruthy(); + }); + + test('Returns false when policy.isITAC is false', () => { + // Arrange + const order = { + policy: { + isITAC: false + } + }; + + // Act + const result = isOrderITAC(order); + + // Assert + expect(result).toBeFalsy(); + }); + }); + + describe('getSubtotal', () => { + test('Returns 0 when there are null items', () => { + // Arrange + const order = { + policy: { + noCoverage: true + }, + lineItems: nullLineItems + }; + + // Act + const result = getSubtotal(order); + + // Assert + expect(result).toBe(0); + }); + + test('Returns 0 when there are no items', () => { + // Arrange + const order = { + policy: { + noCoverage: true + }, + lineItems: emptyLineItems + }; + + // Act + const result = getSubtotal(order); + + // Assert + expect(result).toBe(0); + }); + + test('Returns sub total of deductible + vaps + fee line items', () => { + // Arrange + const order = { + policy: { + noCoverage: false, + isITAC: false + }, + currentDeductible: 100, + lineItems: defaultLineItems + }; + + // Act + const result = getSubtotal(order); + + // Assert + expect(result).toBe(310); + }); + + test('Returns sub total of all line items when no comp', () => { + // Arrange + const order = { + policy: { + noCoverage: true, + isITAC: false + }, + currentDeductible: 0, + lineItems: defaultLineItems + }; + + // Act + const result = getSubtotal(order); + + // Assert + expect(result).toBe(330); + }); + + test('Returns sub total of all line items when ITAC', () => { + // Arrange + const order = { + policy: { + noCoverage: false, + isITAC: true + }, + currentDeductible: 0, + lineItems: defaultLineItems + }; + + // Act + const result = getSubtotal(order); + + // Assert + expect(result).toBe(330); + }); + }); + + describe('getSalesTax', () => { + test('Returns 0 when there are null items', () => { + // Arrange + const order = { + policy: { + noCoverage: true + }, + lineItems: nullLineItems + }; + + // Act + const result = getSalesTax(order); + + // Assert + expect(result).toBe(0); + }); + + test('Returns 0 when there are no items', () => { + // Arrange + const order = { + policy: { + noCoverage: true + }, + lineItems: emptyLineItems + }; + + // Act + const result = getSalesTax(order); + + // Assert + expect(result).toBe(0); + }); + + test('Returns sales tax vaps + fee line items', () => { + // Arrange + const order = { + policy: { + noCoverage: false, + isITAC: false + }, + currentDeductible: 100, + lineItems: defaultLineItems + }; + + // Act + const result = getSalesTax(order); + + // Assert + expect(result).toBe(15); + }); + + test('Returns sales tax of all line items when no comp', () => { + // Arrange + const order = { + policy: { + noCoverage: true, + isITAC: false + }, + currentDeductible: 0, + lineItems: defaultLineItems + }; + + // Act + const result = getSalesTax(order); + + // Assert + expect(result).toBe(21); + }); + + test('Returns sales tax of all line items when ITAC', () => { + // Arrange + const order = { + policy: { + noCoverage: false, + isITAC: true + }, + currentDeductible: 0, + lineItems: defaultLineItems + }; + + // Act + const result = getSalesTax(order); + + // Assert + expect(result).toBe(21); + }); + }); + + describe('getCartTotal', () => { + test('Returns 0 when there are null items', () => { + // Arrange + const order = { + policy: { + noCoverage: true + }, + lineItems: nullLineItems + }; + + // Act + const result = getCartTotal(order); + + // Assert + expect(result).toBe(0); + }); + + test('Returns 0 when there are no items', () => { + // Arrange + const order = { + policy: { + noCoverage: true + }, + lineItems: emptyLineItems + }; + + // Act + const result = getCartTotal(order); + + // Assert + expect(result).toBe(0); + }); + + test('Returns total of vaps + fee line items', () => { + // Arrange + const order = { + policy: { + noCoverage: false, + isITAC: false + }, + currentDeductible: 100, + lineItems: defaultLineItems + }; + + // Act + const result = getCartTotal(order); + + // Assert + expect(result).toBe(325); + }); + + test('Returns total of all line items when no comp', () => { + // Arrange + const order = { + policy: { + noCoverage: true, + isITAC: false + }, + currentDeductible: 0, + lineItems: defaultLineItems + }; + + // Act + const result = getCartTotal(order); + + // Assert + expect(result).toBe(351); + }); + + test('Returns total of all line items when ITAC', () => { + // Arrange + const order = { + policy: { + noCoverage: false, + isITAC: true + }, + currentDeductible: 0, + lineItems: defaultLineItems + }; + + // Act + const result = getCartTotal(order); + + // Assert + expect(result).toBe(351); + }); + }); +}); diff --git a/src/helpers/price-calculator.js b/src/helpers/price-calculator.js index b44abc5a..72634df7 100644 --- a/src/helpers/price-calculator.js +++ b/src/helpers/price-calculator.js @@ -1,12 +1,36 @@ -function getPriceOfLineItem(lineItem) { - return (lineItem?.kitPrice ?? 0) - + (lineItem?.laborAmount ?? 0) - + (lineItem?.sellingPrice ?? 0); +/** + * Returns the price of a single line item. Will include sales tax if includeTax is true + * @param {object} lineItem line item to price + * @param {boolean} includeTax if sales tax should be included + * @returns {number} price of the line item + */ +export function getPriceOfLineItem(lineItem, includeTax = false) { + let price = (lineItem.kitPrice ?? 0) + (lineItem.laborAmount ?? 0) + (lineItem.sellingPrice ?? 0); + if (includeTax) { + price += lineItem.salesTax ?? 0; + } + if (lineItem.childParts && lineItem.childParts.length !== 0) { + // eslint-disable-next-line no-use-before-define + price += getPriceOfLineItems(lineItem.childParts, includeTax); + } + return price; } -export default function getPriceOfLineItems(lineItems) { - return lineItems.reduce( - (accumulator, lineItem) => accumulator + getPriceOfLineItem(lineItem), - 0 - ); +/** + * Returns the price for the given array of line items. Will include sales tax if includeTax is true + * @param {Array} lineItems array of line items to be priced + * @param {boolean} includeTax if sales tax should be included + * @returns {number} price of the lines items + */ +export function getPriceOfLineItems(lineItems, includeTax = false) { + return lineItems?.reduce((accumulator, lineItem) => accumulator + getPriceOfLineItem(lineItem, includeTax), 0) ?? 0; +} + +/** + * Returns the sales tax for the given array of line items + * @param {Array} lineItems array of line items to get the sales tax for + * @returns {number} sales tax of the line items + */ +export function getTaxOfLineItems(lineItems) { + return lineItems?.reduce((accumulator, lineItem) => accumulator + (lineItem.salesTax ?? 0), 0) ?? 0; } diff --git a/src/helpers/price-calculator.spec.js b/src/helpers/price-calculator.spec.js index 5ef5669d..b415d0ab 100644 --- a/src/helpers/price-calculator.spec.js +++ b/src/helpers/price-calculator.spec.js @@ -1,56 +1,226 @@ -import getPriceOfLineItems from '@/helpers/price-calculator.js'; +import { + getPriceOfLineItem, + getPriceOfLineItems, + getTaxOfLineItems +} from '@/helpers/price-calculator.js'; -describe('getPriceOfLineItems', () => { - test('Returns zero when no line items', () => { - // Arrange - const lineItems = []; +describe('price-calculator', () => { + describe('getPriceOfLineItem', () => { + test('Empty lineItem returns 0', () => { + // Arrange + const lineItem = {}; - // Act - const result = getPriceOfLineItems(lineItems); + // Act + const result = getPriceOfLineItem(lineItem, false); - // Assert - expect(result).toBe(0); - }); - test('Returns expected when one line item', () => { - // Arrange - const lineItems = [ - { + // Assert + expect(result).toBe(0); + }); + + test('Empty lineItem with salesTax returns 0', () => { + // Arrange + const lineItem = {}; + + // Act + const result = getPriceOfLineItem(lineItem, true); + + // Assert + expect(result).toBe(0); + }); + + test('Returns kit, labor, sellingPrice total when salesTax is false', () => { + // Arrange + const lineItem = { kitPrice: 1, laborAmount: 2, - sellingPrice: 3 - } - ]; - const expected = 6; + sellingPrice: 3, + salesTax: 4 + }; - // Act - const result = getPriceOfLineItems(lineItems); + // Act + const result = getPriceOfLineItem(lineItem, false); - // Assert - expect(result).toBe(expected); - }); - test('Returns expected when multiple line items', () => { - // Arrange - const lineItems = [ - { + // Assert + expect(result).toBe(6); + }); + + test('Returns kit, labor, sellingPrice, salesTax total when salesTax is true', () => { + // Arrange + const lineItem = { kitPrice: 1, laborAmount: 2, - sellingPrice: 3 - }, - { - kitPrice: 1 - }, - { - kitPrice: 10, - laborAmount: 100, - sellingPrice: 1000 - } - ]; - const expected = 1117; + sellingPrice: 3, + salesTax: 4 + }; - // Act - const result = getPriceOfLineItems(lineItems); + // Act + const result = getPriceOfLineItem(lineItem, true); - // Assert - expect(result).toBe(expected); + // Assert + expect(result).toBe(10); + }); + }); + + describe('getPriceOfLineItems', () => { + test('Returns zero when null', () => { + // Arrange + const lineItems = null; + + // Act + const result = getPriceOfLineItems(lineItems, true); + + // Assert + expect(result).toBe(0); + }); + + test('Returns zero when no line items', () => { + // Arrange + const lineItems = []; + + // Act + const result = getPriceOfLineItems(lineItems, true); + + // Assert + expect(result).toBe(0); + }); + test('Returns expected when one line item', () => { + // Arrange + const lineItems = [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3 + } + ]; + const expected = 6; + + // Act + const result = getPriceOfLineItems(lineItems, false); + + // Assert + expect(result).toBe(expected); + }); + test('Returns expected when multiple line items', () => { + // Arrange + const lineItems = [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3 + }, + { + kitPrice: 1 + }, + { + kitPrice: 10, + laborAmount: 100, + sellingPrice: 1000 + } + ]; + const expected = 1117; + + // Act + const result = getPriceOfLineItems(lineItems, false); + + // Assert + expect(result).toBe(expected); + }); + test('Returns expected when multiple line items with sales tax', () => { + // Arrange + const lineItems = [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3, + salesTax: 4 + }, + { + kitPrice: 1, + salesTax: 2 + }, + { + kitPrice: 10, + laborAmount: 100, + sellingPrice: 1000, + salesTax: 50 + } + ]; + const expected = 1173; + + // Act + const result = getPriceOfLineItems(lineItems, true); + + // Assert + expect(result).toBe(expected); + }); + }); + + describe('getTaxOfLineItems', () => { + test('Returns zero when null line items', () => { + // Arrange + const lineItems = null; + + // Act + const result = getTaxOfLineItems(lineItems); + + // Assert + expect(result).toBe(0); + }); + test('Returns zero when no line items', () => { + // Arrange + const lineItems = []; + + // Act + const result = getTaxOfLineItems(lineItems); + + // Assert + expect(result).toBe(0); + }); + test('Returns expected when one line item', () => { + // Arrange + const lineItems = [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3, + salesTax: 4 + } + ]; + const expected = 4; + + // Act + const result = getTaxOfLineItems(lineItems); + + // Assert + expect(result).toBe(expected); + }); + test('Returns expected when multiple line items', () => { + // Arrange + const lineItems = [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3, + salesTax: 4 + }, + { + kitPrice: 1, + salesTax: 2 + }, + { + kitPrice: 10, + laborAmount: 100, + sellingPrice: 1000, + salesTax: 50 + } + ]; + const expected = 56; + + // Act + const result = getTaxOfLineItems(lineItems); + + // Assert + expect(result).toBe(expected); + }); }); }); diff --git a/src/iss-components/cart-dropdown/cart-dropdown.spec.js b/src/iss-components/cart-dropdown/cart-dropdown.spec.js index 8fdc9898..1427ea02 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.spec.js +++ b/src/iss-components/cart-dropdown/cart-dropdown.spec.js @@ -5,19 +5,22 @@ import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue'; // Supporting Files import { getMountOptions } from '@/helpers/unit-test-helper.js'; import { useMainStore, getDefaultState } from '@/store'; -import coverageStatuses from '@/constants/coverage-statuses'; import { formatAmountInDollars } from '@/helpers/text-helper.js'; import partTypeStrings from '@/constants/part-type-strings'; import partNumberStrings from '@/constants/part-number-strings'; import { getHighestFullySatisfiedTier, getPackageContents } from '@/helpers/service-package-helper.js'; -import getPriceOfLineItems from '@/helpers/price-calculator.js'; +import { getPriceOfLineItems } from '@/helpers/price-calculator.js'; const VERIFYING_COVERAGE = 'Verifying coverage'; jest.mock('@/helpers/text-helper', () => ({ formatAmountInDollars: jest.fn() })); -jest.mock('@/helpers/price-calculator.js', () => jest.fn()); +jest.mock('@/helpers/price-calculator.js', () => ({ + getPriceOfLineItems: jest.fn(), + getTaxOfLineItems: jest.fn(), + getPriceOfLineItem: jest.fn() +})); jest.mock('@/helpers/service-package-helper', () => ({ getHighestFullySatisfiedTier: jest.fn(), @@ -201,7 +204,7 @@ describe('cart-dropdown component', () => { const storeData = { order: { lineItems: { - supportingItems: [ + feeItems: [ { partNumber: partNumberStrings.RECYCLE_FEE } ] } @@ -384,399 +387,6 @@ describe('cart-dropdown component', () => { // TODO when subTotal and salesTax are finished }); }); - describe('subTotal', () => { - const supportingItemPartNumber = 'supporter'; - const glassPartNumber = 'glass part'; - const otherPartNumber = 'other'; - const vapPartNumber = 'vap'; - const mobileFeePartNumber = 'mobile'; - const supportingItems = [{ partNumber: supportingItemPartNumber }]; - const glassParts = [{ partNumber: glassPartNumber }]; - const otherParts = [{ partNumber: otherPartNumber }]; - const vaps = [{ partNumber: vapPartNumber }]; - const mobileFee = { partNumber: mobileFeePartNumber }; - const includesPartNumber = (lineItems, partNumber) => lineItems.some((item) => item?.partNumber === partNumber); - const deductiblePrice = 100; - const supportingAndGlassAndOtherPrice = 30; - const feesAndVapsPrice = 50; - const allLineItemsPrice = 70; - getPriceOfLineItems.mockImplementation((lineItems) => { - const allPartNumbers = [ - supportingItemPartNumber, - glassPartNumber, - otherPartNumber, - mobileFeePartNumber, - vapPartNumber - ]; - if (allPartNumbers.every((partNumber) => includesPartNumber(lineItems, partNumber))) { - return allLineItemsPrice; - } - const supportingAndGlassAndOtherPartNumbers = [ - supportingItemPartNumber, - glassPartNumber, - otherPartNumber - ]; - if (supportingAndGlassAndOtherPartNumbers.every((partNumber) => includesPartNumber(lineItems, partNumber))) { - return supportingAndGlassAndOtherPrice; - } - if (includesPartNumber(lineItems, mobileFeePartNumber) && includesPartNumber(lineItems, vapPartNumber)) { - return feesAndVapsPrice; - } - return 1; - }); - const lineItems = { - supportingItems, - glassParts, - otherParts, - vaps, - mobileFee - }; - test('when not no comp and not itac, includes deductible price plus fee and vap price', () => { - // Arrange - const storeData = { - order: { - lineItems, - policy: { - noCoverage: false, - isITAC: false - }, - currentDeductible: deductiblePrice - } - }; - const { wrapper } = getMountedComponent(storeData); - const expected = deductiblePrice + feesAndVapsPrice; - - // Act - const result = wrapper.vm.subTotal; - - // Assert - expect(result).toBe(expected); - }); - test('when no comp, returns all line items price', () => { - // Arrange - const storeData = { - order: { - lineItems, - policy: { - noCoverage: true, - isITAC: false - }, - currentDeductible: deductiblePrice - } - }; - const { wrapper } = getMountedComponent(storeData); - const expected = allLineItemsPrice; - - // Act - const result = wrapper.vm.subTotal; - - // Assert - expect(result).toBe(expected); - }); - test('when ITAC, returns all line items price', () => { - // Arrange - const storeData = { - order: { - lineItems, - policy: { - noCoverage: false, - isITAC: true - }, - currentDeductible: deductiblePrice - } - }; - const { wrapper } = getMountedComponent(storeData); - const expected = allLineItemsPrice; - - // Act - const result = wrapper.vm.subTotal; - - // Assert - expect(result).toBe(expected); - }); - }); - describe('salesTax', () => { - test('salesTax is treated as 0 when line items are null', () => { - // Arrange - const storeData = { - order: { - policy: { - policyLookupSuccessful: true - }, - currentDeductible: 250, - lineItems: { - glassParts: null, - otherParts: null, - supportingItems: null, - vaps: null - }, - payment: { - insuranceCoverage: { isVerified: true } - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(0); - }); - - test("salesTax is treated as 0 when null or undefined is set as the line item's sales tax.", () => { - // Arrange - const storeData = { - order: { - policy: { - policyLookupSuccessful: true - }, - currentDeductible: 250, - lineItems: { - glassParts: [ - { partType: 'mock', salesTax: null }, - { partType: 'mock', salesTax: undefined } - ], - supportingItems: [ - { partType: 'mock', salesTax: null }, - { partType: 'mock', salesTax: undefined } - ], - vaps: [ - { partType: 'mock', salesTax: null }, - { partType: 'mock', salesTax: undefined } - ] - }, - payment: { - insuranceCoverage: { isVerified: true } - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(0); - }); - test('Returns 0 as sales tax when coverage is unverified and order does not contain vaps.', () => { - // Arrange - const storeData = { - order: { - policy: { - policyLookupSuccessful: true - }, - currentDeductible: 250, - lineItems: { - glassParts: [{ partType: 'mock', salesTax: 10 }], - supportingItems: [ - { - partNumber: partNumberStrings.RECYCLE_FEE, - partType: 'mock', - salesTax: 10, - sellingPrice: 39.99 - } - ], - vaps: [] - }, - payment: { - insuranceCoverage: { isVerified: false } - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(0); - }); - - test('Returns sum of vaps sales tax when coverage is unverified and order has vaps.', () => { - // Arrange - const storeData = { - order: { - policy: { - policyLookupSuccessful: true - }, - currentDeductible: 250, - lineItems: { - glassParts: [{ partType: 'mock', salesTax: 10 }], - vaps: [ - { partType: 'mock', salesTax: 1 }, - { partType: 'mock', salesTax: 2 } - ] - }, - payment: { - insuranceCoverage: { isVerified: false } - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(3); - }); - - test('Returns Recycle Fee tax when coverage is Verified-Deductible, replace service, and no vaps.', () => { - // Arrange - const storeData = { - order: { - policy: { - policyLookupSuccessful: true, - noCoverage: false, - isITAC: false - }, - currentDeductible: 250, - lineItems: { - glassParts: [{ partType: 'mock', salesTax: 10, sellingPrice: 150 }], - supportingItems: [ - { - partNumber: partNumberStrings.RECYCLE_FEE, - partType: 'mock', - salesTax: 10, - sellingPrice: 39.99 - } - ], - vaps: null - }, - payment: { - insuranceCoverage: { isVerified: true } - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(10); - }); - - test('Returns sum of vaps + recycle fee sales tax when Verified-Deductible, replace service, and has vaps.', () => { - // Arrange - const storeData = { - order: { - policy: { - policyLookupSuccessful: true - }, - currentDeductible: 250, - lineItems: { - glassParts: [{ partType: 'mock', salesTax: 10, sellingPrice: 100 }], - otherParts: [{ partType: 'mock', salesTax: 10, kitPrice: 100 }], - supportingItems: [ - { partNumber: partNumberStrings.RECYCLE_FEE, partType: 'mock', salesTax: 10, sellingPrice: 39.99 }, - { partType: 'mock', salesTax: 10, kitPrice: 100 } - ], - vaps: [ - { partType: 'mock', salesTax: 2 }, - { partType: 'mock', salesTax: 3 } - ] - }, - payment: { - insuranceCoverage: { isVerified: true } - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(15); - }); - - test('Returns sum of sales tax when Verified-ITAC.', () => { - // Arrange - const storeData = { - order: { - currentDeductible: 0, - lineItems: { - glassParts: [{ partType: 'mock', salesTax: 10, sellingPrice: 100 }], - otherParts: [{ partType: 'mock', salesTax: 10, kitPrice: 100 }], - supportingItems: [{ partType: 'mock', salesTax: 10, kitPrice: 100 }], - vaps: [{ partType: 'mock', salesTax: 5 }] - }, - payment: { - insuranceCoverage: { isVerified: true } - }, - policy: { - isITAC: true, - policyLookupSuccessful: true - } - }, - issConfig: { - isClaimRegistrationRequired: true - } - }; - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(35); - }); - - test('Returns sum of sales tax when Verified-NoComp.', () => { - // Arrange - const storeData = { - order: { - currentDeductible: 0, - lineItems: { - glassParts: [{ partType: 'mock', salesTax: 10, sellingPrice: 100 }], - otherParts: [{ partType: 'mock', salesTax: 10, kitPrice: 100 }], - supportingItems: [{ partType: 'mock', salesTax: 10, kitPrice: 100 }], - vaps: [{ partType: 'mock', salesTax: 5 }] - }, - payment: { - insuranceCoverage: { isVerified: true } - }, - policy: { - noCoverage: true, - policyLookupSuccessful: true - } - }, - issConfig: { - isClaimRegistrationRequired: true, - enableNoCompQuote: true - } - }; - const { wrapper } = getMountedComponent(storeData); - - // Act - const result = wrapper.vm.salesTax; - - // Assert - expect(result).toBe(35); - }); - }); describe('availableLineItems', () => { test('returns expected when all null', () => { // Arrange @@ -848,7 +458,9 @@ describe('cart-dropdown component', () => { const mobileFee = { partNumber: 'mobile' }; const storeData = { order: { - lineItems: { mobileFee } + lineItems: { + feeItems: [mobileFee] + } } }; const propsData = { availableVaps: null }; @@ -888,7 +500,7 @@ describe('cart-dropdown component', () => { glassParts: [part1], supportingItems: [part2, part3], otherParts: [], - mobileFee + feeItems: [mobileFee] } } }; @@ -1147,7 +759,7 @@ describe('cart-dropdown component', () => { const storeData = { order: { lineItems: { - supportingItems: [ + feeItems: [ { partNumber: partNumberStrings.RECYCLE_FEE } @@ -1170,9 +782,7 @@ describe('cart-dropdown component', () => { const storeData = { order: { lineItems: { - mobileFee: { - partType: partTypeStrings.MOBILE_FEE - } + feeItems: [{ partType: partTypeStrings.MOBILE_FEE }] } } }; @@ -1368,14 +978,10 @@ describe('cart-dropdown component', () => { const storeData = { order: { lineItems: { - supportingItems: [ - { - partNumber: partNumberStrings.RECYCLE_FEE - } + feeItems: [ + { partType: partTypeStrings.MOBILE_FEE }, + { partNumber: partNumberStrings.RECYCLE_FEE } ], - mobileFee: { - partType: partTypeStrings.MOBILE_FEE - }, vaps: [ { partType: partTypeStrings.RECALIBRATION }, { partType: partTypeStrings.FRONT_WIPER }, @@ -1533,7 +1139,7 @@ describe('cart-dropdown component', () => { // Assert expect(result).toStrictEqual(expected); }); - test('returns expected when recycle fee in supporting items', () => { + test('returns expected when recycle fee in fee items', () => { // Arrange const mountOptions = getMountOptions({ router: { navigate: jest.fn() } @@ -1549,7 +1155,7 @@ describe('cart-dropdown component', () => { const storeData = { order: { lineItems: { - supportingItems: [{ partNumber: partNumberStrings.RECYCLE_FEE }] + feeItems: [{ partNumber: partNumberStrings.RECYCLE_FEE }] } } }; @@ -1587,7 +1193,7 @@ describe('cart-dropdown component', () => { // Assert expect(result).toStrictEqual(expected); }); - test('returns expected when mobile fee in supporting items', () => { + test('returns expected when mobile fee in fee items', () => { // Arrange const mountOptions = getMountOptions({ router: { navigate: jest.fn() } @@ -1603,7 +1209,7 @@ describe('cart-dropdown component', () => { const storeData = { order: { lineItems: { - mobileFee: { partType: partTypeStrings.MOBILE_FEE } + feeItems: [{ partType: partTypeStrings.MOBILE_FEE }] } } }; diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index 5aba7f8d..a6c6e726 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -40,7 +40,7 @@ id="cart-base-price" class="d-flex justify-content-between align-items-center"> {{ basePriceLabel }} - {{ getDisplayed(baseServicePrice) }} + {{ getDisplayed(servicePrice) }} @@ -159,15 +159,22 @@ import { useMainStore } from '@/store'; import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue'; import textBlock from '@/digital-components/text-block/text-block.vue'; import textLink from '@/ux-components/text-link/text-link.vue'; -import getPriceOfLineItems from '@/helpers/price-calculator.js'; import { formatAmountInDollars } from '@/helpers/text-helper.js'; import { getHighestFullySatisfiedTier, getPackageContents } from '@/helpers/service-package-helper.js'; // Constants import partTypeStrings from '@/constants/part-type-strings.js'; import cartItemType from '@/constants/cart-item-type.js'; -import partNumberStrings from '@/constants/part-number-strings.js'; import widgetFields from '@/constants/cms-widget-fields.js'; +import { + getCartTotal, + getDeductible, + getLineItems, getMobileFee, + getRecycleFee, getSalesTax, + getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp, + isOrderUnverified +} from '@/helpers/cart-helper'; +import {getPriceOfLineItems, getTaxOfLineItems} from '@/helpers/price-calculator'; const VERIFYING_COVERAGE = 'Verifying coverage'; @@ -206,106 +213,58 @@ export default { }; }, computed: { + order() { + return this.submittedOrder ?? useMainStore().order; + }, deductible() { - // Note: added as computed so it can be used in the template. - return this.submittedOrder ? this.submittedOrder.currentDeductible : useMainStore().order.currentDeductible; + return getDeductible(this.order); }, showDeductibleCartItem() { - const { isUnverified } = useMainStore(); - return isUnverified || (!this.isNoComp && !this.isITAC); + return this.isUnverified || (!this.isNoComp && !this.isITAC); }, lineItems() { - return this.submittedOrder ? this.submittedOrder.lineItems : useMainStore().lineItems; + return getLineItems(this.order); }, recycleFeeLineItem() { - return this.lineItems.supportingItems - ?.find((lineItem) => lineItem.partNumber === partNumberStrings.RECYCLE_FEE); + return getRecycleFee(this.order); }, - baseServiceLineItems() { - const { supportingItems, glassParts, otherParts } = this.lineItems; - const parts = [ - ...(supportingItems ?? []), - ...(glassParts ?? []), - ...(otherParts ?? []) - ]; - return parts.filter((part) => part !== this.recycleFeeLineItem); + servicePrice() { + return getPriceOfLineItems(getServiceLineItems(this.order), false) ?? 0; }, - baseServicePrice() { - return getPriceOfLineItems(this.baseServiceLineItems) ?? 0; + isUnverified() { + return isOrderUnverified(this.order); }, isITAC() { - return this.submittedOrder ? this.submittedOrder.policy.isITAC : useMainStore().isITAC; + return isOrderITAC(this.order); }, isNoComp() { - return this.submittedOrder ? this.submittedOrder.policy.noCoverage : useMainStore().isNoComp; + return isOrderNoComp(this.order); }, // TODO fix rounding subTotal() { - const { supportingItems, glassParts, otherParts, vaps, mobileFee } = this.lineItems; - const allLineItems = [ - ...(supportingItems ?? []), - ...(glassParts ?? []), - ...(otherParts ?? []), - ...(vaps ?? []), - mobileFee - ]; - return !this.isNoComp && !this.isITAC - ? this.deductible + getPriceOfLineItems([...this.feeLineItems, ...(vaps ?? [])]) - : getPriceOfLineItems(allLineItems); - }, - feeLineItems() { - const { mobileFee } = this.lineItems; - const result = []; - if (mobileFee) { - result.push(mobileFee); - } - if (this.recycleFeeLineItem) { - result.push(this.recycleFeeLineItem); - } - return result; + return getSubtotal(this.order); }, salesTax() { - const { isUnverified } = useMainStore(); - function sumTax(lineItems) { - return lineItems?.reduce((accumulator, lineItem) => accumulator + (lineItem.salesTax ?? 0), 0) ?? 0; - } - - let result = 0; - - if (!isUnverified) { - if (this.isITAC || this.isNoComp) { - result += sumTax(this.baseServiceLineItems); - } else { - // deductible-case need to show tax for Recycle Fee - result += this.recycleFeeLineItem?.salesTax ?? 0; - } - } - - result += sumTax(this.lineItems.vaps ?? []); - - return result; + return getSalesTax(this.order); + }, + total() { + return getCartTotal(this.order); }, amountDue() { - return this.showAsPaid - ? 0 - : this.subTotal + this.salesTax; + return this.showAsPaid ? 0 : this.total; }, amountPaid() { - return !this.showAsPaid - ? 0 - : this.subTotal + this.salesTax; + return !this.showAsPaid ? 0 : this.total; }, availableLineItems() { - const { supportingItems, glassParts, otherParts, mobileFee } = this.lineItems; + const { supportingItems, glassParts, otherParts, feeItems } = this.lineItems; const result = [ ...(supportingItems ?? []), ...(glassParts ?? []), ...(otherParts ?? []), - ...(this.availableVaps ?? []) + ...(this.availableVaps ?? []), + ...(feeItems ?? []) ]; - if (mobileFee) { - result.push(mobileFee); - } return result; }, vehicleDamage() { @@ -428,7 +387,7 @@ export default { : null; }, mobileFeeCartItem() { - const mobileFeeLineItem = this.lineItems.mobileFee; + const mobileFeeLineItem = getMobileFee(this.order); return mobileFeeLineItem ? this.getCartItem( this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), @@ -447,8 +406,7 @@ export default { this.isExpanded = !this.isExpanded; }, getDisplayed(amount) { - const { isUnverified } = useMainStore(); - return isUnverified + return this.isUnverified ? VERIFYING_COVERAGE : formatAmountInDollars(amount); }, @@ -460,11 +418,8 @@ export default { name: label, cartItemType: cartItemTypeString, partType, - subTotal: getPriceOfLineItems(lineItems), - salesTax: lineItems?.reduce( - (accumulator, lineItem) => accumulator + lineItem.salesTax, - 0 - ) ?? 0 + subTotal: getPriceOfLineItems(lineItems, false), + salesTax: getTaxOfLineItems(lineItems) }; } diff --git a/src/layouts/coverage-statement/coverage-statement.spec.js b/src/layouts/coverage-statement/coverage-statement.spec.js index e47df660..e90a3804 100644 --- a/src/layouts/coverage-statement/coverage-statement.spec.js +++ b/src/layouts/coverage-statement/coverage-statement.spec.js @@ -11,10 +11,12 @@ import { getRandomString, getRandomInt } from '@/helpers/data-generation.js'; import settleAllPromises from '@/helpers/layout-helper.js'; import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import { useMainStore, getDefaultState } from '@/store'; -import getPriceOfLineItems from '@/helpers/price-calculator.js'; +import { getPriceOfLineItems } from '@/helpers/price-calculator.js'; jest.mock('@/helpers/layout-helper.js', () => jest.fn()); -jest.mock('@/helpers/price-calculator.js', () => jest.fn()); +jest.mock('@/helpers/price-calculator.js', () => ({ + getPriceOfLineItems: jest.fn() +})); jest.mock('@/helpers/cms-content-helper', () => ({ fetchCmsContentForPage: jest.fn(), diff --git a/src/layouts/coverage-statement/coverage-statement.vue b/src/layouts/coverage-statement/coverage-statement.vue index d618aecf..0bc42c12 100644 --- a/src/layouts/coverage-statement/coverage-statement.vue +++ b/src/layouts/coverage-statement/coverage-statement.vue @@ -123,9 +123,9 @@ import issPageValues from '@/router/router-constants/issPage-values'; import bailoutMessage from '@/constants/bailoutMessage'; import coverageStatuses from '@/constants/coverage-statuses'; import widgetFields from '@/constants/cms-widget-fields.js'; -import getPriceOfLineItems from '@/helpers/price-calculator.js'; import { formatAmountInDollars } from '@/helpers/text-helper.js'; import showIssLoadingModal from '@/helpers/loading-modal-helper'; +import { getPriceOfLineItems } from '@/helpers/price-calculator'; const SAFELITE_PROVIDER = 'Safelite'; @@ -344,7 +344,7 @@ export default { ); }, totalServicePrice() { - return getPriceOfLineItems(this.baseServiceLineItems); + return getPriceOfLineItems(this.baseServiceLineItems, false); }, serviceProviderQuestionText() { return this.getCmsContent( diff --git a/src/layouts/payment-page/payment-page.spec.js b/src/layouts/payment-page/payment-page.spec.js index f12f5461..b2bdc2e4 100644 --- a/src/layouts/payment-page/payment-page.spec.js +++ b/src/layouts/payment-page/payment-page.spec.js @@ -107,13 +107,6 @@ const taxedParts = { // Setup global mocks let mockCmsContent = {}; -jest.mock('@/mixins/base-mixin.js', () => ({ - methods: { - getAmountDue: jest.fn().mockImplementation(() => 5), - getDisplayAmountDue: jest.fn().mockImplementation(() => '$5.00') - } -})); - jest.mock('@/helpers/cms-content-helper', () => ({ fetchCmsContentForPage: () => Promise.resolve('content') })); diff --git a/src/layouts/payment-page/payment-page.vue b/src/layouts/payment-page/payment-page.vue index 726bc1f9..34ccc2a1 100644 --- a/src/layouts/payment-page/payment-page.vue +++ b/src/layouts/payment-page/payment-page.vue @@ -396,8 +396,8 @@ import issPageValues from '@/router/router-constants/issPage-values.js'; import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import settleAllPromises from '@/helpers/layout-helper'; import { Form } from 'vee-validate'; -import baseMixin from '@/mixins/base-mixin.js'; import BaseFormMixin from '@/mixins/base-form-mixin.js'; +import { getCartTotal } from '@/helpers/cart-helper'; import { useMainStore } from '@/store'; import iframeResize from 'iframe-resizer/js/iframeResizer.js'; import externalUrls from '@/router/router-constants/externalUrl-values.js'; @@ -410,6 +410,7 @@ import { AppointmentTypeStrings } from '@/constants/schedule-constants.js'; import queryStrings from '@/constants/query-strings'; import alert from '@/ux-components/alert/alert.vue'; import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue'; +import { formatAmountInDollars } from '@/helpers/text-helper'; export default { name: 'payment-page', @@ -715,10 +716,10 @@ export default { this.payInAdvanceLineItems = lineItems.join('||'); }, getAmountDue() { - return baseMixin.methods.getAmountDue(useMainStore().lineItems); + return getCartTotal(useMainStore().order); }, getDisplayAmountDue() { - return baseMixin.methods.getDisplayAmountDue(useMainStore().lineItems); + return formatAmountInDollars(this.getAmountDue()); }, fetchSignatureInfo(signatureInfo) { this.authToken = signatureInfo.token; diff --git a/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue index 35226055..c26e739b 100644 --- a/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue +++ b/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue @@ -47,6 +47,8 @@ import buttonQuestion from '@/digital-components/button-question/button-question // Helpers import { deepClone } from '@/helpers/object-helper'; +import { formatAmountInDollars } from '@/helpers/text-helper'; +import { getPriceOfLineItem } from '@/helpers/price-calculator'; // Validation import { defineRule, useField } from 'vee-validate'; @@ -421,7 +423,7 @@ export default { }, getPremiumAppointmentTimeSlot(timeSlotData) { const formattedPrice = - `+$${this.getTotalLineItemPrice(this.premiumAppointmentFee).toFixed(2)}`; + `+${formatAmountInDollars(getPriceOfLineItem(this.premiumAppointmentFee, false))}`; return { // Unique value is required for each and the premium appoinment shares an id diff --git a/src/layouts/service-packages/service-package-question/service-package-question.vue b/src/layouts/service-packages/service-package-question/service-package-question.vue index 9c87d796..5092d581 100644 --- a/src/layouts/service-packages/service-package-question/service-package-question.vue +++ b/src/layouts/service-packages/service-package-question/service-package-question.vue @@ -18,6 +18,7 @@ import servicePackageRadio from '@/layouts/service-packages/service-package-ques import partTypeStrings from '@/constants/part-type-strings'; import { useMainStore } from '@/store'; import allGlassPartsAndItemsHavePrices from '@/layouts/service-packages/service-package-helper/service-package-helper'; +import { getPriceOfLineItem } from '@/helpers/price-calculator'; const glassLocations = damageLocationsSelected; @@ -186,7 +187,7 @@ export default { && item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) || (priceRearWipers && item.partType.toUpperCase() === partTypeStrings.REAR_WIPER) ) { - vapsPrice += this.getTotalLineItemPrice(item); + vapsPrice += getPriceOfLineItem(item, false); } }); return vapsPrice; @@ -205,7 +206,7 @@ export default { || (priceRainDefense && item.partType.toUpperCase() === partTypeStrings.RAIN_DEFENSE) ) { - vapsPrice += this.getTotalLineItemPrice(item); + vapsPrice += getPriceOfLineItem(item, false); } }); return vapsPrice; @@ -309,9 +310,6 @@ export default { const glassLocationMatches = store.order.damage.glassToReplace?.filter((glassToReplace) => glassToReplace.glassLocation === glassLocation) ?? []; return !!glassLocationMatches.length; - }, - getTotalLineItemPrice(lineItem) { - return lineItem.kitPrice + lineItem.laborAmount + lineItem.sellingPrice; } } }; diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index f4a3028d..ac36cbf0 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -36,62 +36,6 @@ export default { savePageDataToStore(page, data) { useMainStore().updatePageData({ page, data }); }, - getTotalPriceOfAllLineItemsAndChildParts(lineItems, includeTax) { - let totalPrice = 0; - lineItems.forEach((lineItem) => { - totalPrice += this.getTotalLineItemPrice(lineItem, includeTax); - if (lineItem.childParts) { - totalPrice += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItem.childParts, - includeTax - ); - } - }); - return totalPrice; - }, - getTotalLineItemPrice(lineItem, includeTax) { - if (includeTax) { - return ( - lineItem.kitPrice - + lineItem.laborAmount - + lineItem.sellingPrice - + lineItem.salesTax - ); - } - return lineItem.kitPrice + lineItem.laborAmount + lineItem.sellingPrice; - }, - getDisplayAmountDue(lineItems) { - return this.getAmountDue(lineItems).toLocaleString('en-US', { - style: 'currency', - currency: 'USD' - }); - }, - getAmountDue(lineItems) { - if (!useMainStore().isNoComp && !useMainStore().isITAC) { - return useMainStore().order.currentDeductible; - } - - let amountDue = 0; - if (lineItems.glassParts) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItems.glassParts, - false - ); - } - if (lineItems.supportingItems) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( - lineItems.supportingItems, - false - ); - } - if (lineItems.vaps) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts(lineItems.vaps, false); - } - if (lineItems.promos) { - amountDue += this.getTotalPriceOfAllLineItemsAndChildParts(lineItems.promos, false); - } - return ((amountDue * 100) / 100).toFixed(2); - }, scrollToPageTop() { const container = document.getElementsByClassName('fade-on-route-transition')[0]; container.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); diff --git a/src/router/index.js b/src/router/index.js index 96a905b9..25a72e96 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -170,16 +170,18 @@ router.afterEach(async (to, from) => { store.clearSaveSessionPromise(); } - const saveSessionSynchronous = !!from.params[routerParams.SAVE_SESSION_SYNCHRONOUS]; - await saveSession({ shouldAwaitSaveSessionQueue: saveSessionSynchronous }).catch((error) => { - if (from.name === issPageValues.WELCOME_PAGE) { - store.setBailout(bailoutMessage.saveSessionError(error.data)); - router.navigate( - navigationScenarios.SAVE_SESSION_FAILED, - { query: { issPage: issPageValues.WELCOME_PAGE } } - ); - } - }); + if (to.query.issPage !== issPageValues.ORDER_CONFIRMATION) { + const saveSessionSynchronous = !!from.params[routerParams.SAVE_SESSION_SYNCHRONOUS]; + await saveSession({ shouldAwaitSaveSessionQueue: saveSessionSynchronous }).catch((error) => { + if (from.name === issPageValues.WELCOME_PAGE) { + store.setBailout(bailoutMessage.saveSessionError(error.data)); + router.navigate( + navigationScenarios.SAVE_SESSION_FAILED, + { query: { issPage: issPageValues.WELCOME_PAGE } } + ); + } + }); + } if (to.query.issPage !== issPageValues.ENTRY_PAGE) { // Push page view to GA diff --git a/src/store/index.js b/src/store/index.js index 391e4f85..98f0a690 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -24,6 +24,7 @@ import { import partTypeStrings from '@/constants/part-type-strings'; import bailoutMessage from '@/constants/bailoutMessage'; import bailoutCode from '@/constants/bailoutCode'; +import partNumberStrings from "@/constants/part-number-strings"; const storeId = 'main'; @@ -149,8 +150,9 @@ export const getDefaultState = () => ({ glassParts: null, otherParts: null, supportingItems: null, + feeItems: [], vaps: null, - mobileFee: null + serverData: null }, payment: { insuranceCoverage: { @@ -273,6 +275,19 @@ export const useMainStore = defineStore({ policy: (state) => state.order.policy, hasExactlyOneChip: () => state.order.damage.numberOfChips === 1, isPolicyVehicle: () => state.order.vehicle.policyVehicleId != null, + hasWindshieldReplacement: (s) => { + const { damage } = s.order; + if (damage.isRepair) { + return false; + } + + const { glassParts } = s.order.lineItems; + if (!glassParts || glassParts.length === 0) { + return false; + } + + return glassParts.findIndex((gp) => gp.partType === 'WINDSHIELD') !== -1; + }, hasAnyNonWindshieldGlassParts: (s) => !s.order.policy.isDamageGlassOnly, isMobileAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE || state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP, @@ -848,8 +863,9 @@ export const useMainStore = defineStore({ let lineItems = [ ...(order.lineItems.supportingItems ?? []), ...(order.lineItems.vaps ?? []), - ...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts) + ...(order.lineItems.glassParts) ]; + lineItems = getLineItemsFlattened(lineItems); lineItems = lineItems.map((lineItem) => ({ partNumber: lineItem.partNumber, partType: lineItem.partType @@ -909,8 +925,9 @@ export const useMainStore = defineStore({ let lineItems = [ ...(order.lineItems.supportingItems ?? []), ...(order.lineItems.vaps ?? []), - ...getFlattenedArrayOfLineItemsWithChildParts(order.lineItems.glassParts) + ...(order.lineItems.glassParts) ]; + lineItems = getLineItemsFlattened(lineItems); lineItems = lineItems.map((lineItem) => ({ partNumber: lineItem.partNumber, partType: lineItem.partType @@ -1044,16 +1061,17 @@ export const useMainStore = defineStore({ this.order.serviceLocation.zipCodeCtu = zipInfo.data.zipCodeCtu; } const ctuToUse = this.order.serviceLocation.zipCodeCtu; - const availableLineItemsFormattedForRequest = getLineItemQueryStringForPricing(availableLineItems); const deductibleToUse = this.order.currentDeductible ?? 0; + const lineItemsQueryString = getLineItemQueryString(availableLineItems, 'lineItems'); + let queryString = `ParentAccountNumber=${this.order.accountNumber}` + `&BillToAccountNumber=${this.billToNumberToUse}` + `&CTU=${ctuToUse}` + `&Deductible=${deductibleToUse}` + `&ZipCode=${zipCodeToUse}` - + `${availableLineItemsFormattedForRequest}`; + + `&${lineItemsQueryString}`; const lineItemServerData = this.order.lineItems?.serverData; if (lineItemServerData) { @@ -1069,8 +1087,13 @@ export const useMainStore = defineStore({ throw error; }); - if (response.data?.lineItems) { - const retAvailableLineItems = addPricesToLineItems(availableLineItems, response.data.lineItems); + const { lineItems, serverData } = response.data; + if (serverData) { + this.order.lineItems.serverData = serverData; + } + + if (lineItems) { + const retAvailableLineItems = addPricesToLineItems(availableLineItems, lineItems); return retAvailableLineItems; } return availableLineItems; @@ -1086,10 +1109,7 @@ export const useMainStore = defineStore({ }, getServiceabilityDetails({ serviceZipCode }) { - const lineItemsWithOnlyPartNumbers = this.order.lineItems.supportingItems.map((lineItem) => ({ - partNumber: lineItem.partNumber - })); - const lineItems = buildQueryStringParameterFromArrayOfComplexObjects(lineItemsWithOnlyPartNumbers, 'lineItems'); + const lineItems = getLineItemQueryString(this.order.lineItems.supportingItems, 'lineItems'); const { vehicle } = this.order; const { carId } = vehicle; @@ -1209,6 +1229,10 @@ export const useMainStore = defineStore({ return Promise.resolve(null); } + if (this.hasSubmittedOrder()) { + return Promise.resolve(null); + } + const loadedFromDupeCheck = !!(this.order.loadedFromDupeCheck && !this.order.loadedSessionClearedPreviousData); const newGlassToReplace = convertGlassPieceNamingForApi(damage.glassToReplace); @@ -1593,20 +1617,50 @@ export const useMainStore = defineStore({ return; } - let parts = partsData; - if (!this.isNoComp && !this.isITAC) { - parts = parts.filter((i) => i.partNumber !== partTypeStrings.RECYCLE_FEE); - } + // Process Recycle Fee + this.updateRecycleFee(partsData.find((rf) => rf.partNumber === partNumberStrings.RECYCLE_FEE)); - this.order.lineItems.supportingItems = parts; + // Remove RecycleFee from supported items as it's saved in feeItems; + this.order.lineItems.supportingItems = partsData.filter((i) => i.partNumber !== partNumberStrings.RECYCLE_FEE); }, updateVaps(partsData) { this.order.lineItems.vaps = partsData; }, - updateMobileFee(mobileFee) { - this.order.lineItems.mobileFee = mobileFee; + updateMobileFee(fee) { + // Mobile Fee is only added for NO COMP or ITAC + if (fee && (this.isNoComp || this.isITAC)) { + this.addPartTypeFeeItem(fee, partTypeStrings.MOBILE_FEE); + } else { + this.addPartTypeFeeItem(null, partTypeStrings.MOBILE_FEE); + } + }, + + updateRecycleFee(fee) { + // Recycle Fee is only added for NO COMP or ITAC and has Windshield Replacement + if (fee && (this.isNoComp || this.isITAC) && this.hasWindshieldReplacement) { + this.addPartNumberFeeItem(fee, partNumberStrings.RECYCLE_FEE); + } else { + this.addPartNumberFeeItem(null, partNumberStrings.RECYCLE_FEE); + } + }, + + addPartNumberFeeItem(feeItem, partNumber) { + this.addFeeItem(feeItem, (fi) => fi.partNumber !== partNumber); + }, + + addPartTypeFeeItem(feeItem, partType) { + this.addFeeItem(feeItem, (fi) => fi.partType !== partType); + }, + + addFeeItem(feeItem, filter) { + let { feeItems } = this.order.lineItems; + // Remove all fee items with the given filter + feeItems = feeItems.filter(filter); + if (feeItem) { + feeItems.push(feeItem); + } }, async setPriceAndSalesTaxForOrderLineItems() { @@ -1614,16 +1668,13 @@ export const useMainStore = defineStore({ ...(this.order.lineItems.supportingItems ?? []), ...(this.order.lineItems.glassParts ?? []), ...(this.order.lineItems.otherParts ?? []), - ...(this.order.lineItems.vaps ?? []) + ...(this.order.lineItems.vaps ?? []), + ...(this.order.lineItems.feeItems ?? [] + ) ]; - if (this.order.lineItems.mobileFee != null) { - lineItemsToTax.push(this.order.lineItems.mobileFee); - } - - const pricedLineItemsToTax = await this.getPriceOrderItems(lineItemsToTax); - - this.taxOrderItemsAndSaveServerData(pricedLineItemsToTax); + await this.getPriceOrderItems(lineItemsToTax); + await this.getTaxOrderItems(lineItemsToTax); }, updateVehicle(vehicle) { @@ -1731,6 +1782,8 @@ export const useMainStore = defineStore({ resetGlassPartsState() { this.order.lineItems.glassParts = null; this.order.lineItems.supportingItems = null; + this.order.lineItems.feeItems = []; + this.order.lineItems.serverData = null; this.order.damage.partQuestionAnswers = null; this.order.damage.moldingQuestionAnswers = null; this.order.damage.capabilityQuestionAnswers = null; @@ -1960,7 +2013,7 @@ export const useMainStore = defineStore({ }, // Tax order actions - async taxOrderItemsAndSaveServerData(pricedLineItems) { + async getTaxOrderItems(pricedLineItems) { const { order } = this; const { serviceLocation } = order; const { providerNumber } = serviceLocation.provider; @@ -1969,39 +2022,27 @@ export const useMainStore = defineStore({ const serviceLocationState = serviceLocation.state; const serviceLocationZipCode = serviceLocation.zipCode; - const flattenedLineItemsWithChildParts = getFlattenedArrayOfLineItemsWithChildParts(pricedLineItems); - - const lineItemsWithOnlyPriceInfo = flattenedLineItemsWithChildParts.map((lineItem) => ({ - partNumber: lineItem.partNumber, - laborAmount: lineItem.laborAmount ?? 0, - kitPrice: lineItem.kitPrice ?? 0, - sellingPrice: lineItem.sellingPrice ?? 0 - })); - - const pricedLineItemsFormattedForRequest = buildQueryStringParameterFromArrayOfComplexObjects( - lineItemsWithOnlyPriceInfo, - 'lineItems' - ); + const lineItemsQueryString = getLineItemQueryString(pricedLineItems, 'lineItems'); let queryString = ''; if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) { queryString = - `ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` + `ParentAccountNumber=${this.order.accountNumber}` + `&BillToAccountNumber=${this.billToNumberToUse}` + `&ProviderNumber=${providerNumber}` + `&AppointmentType=${appointmentType}` + `&ServiceLocation.City=${serviceLocationCity}` + `&ServiceLocation.State=${serviceLocationState}` + `&ServiceLocation.ZipCode=${serviceLocationZipCode}` - + `&${pricedLineItemsFormattedForRequest}`; + + `&${lineItemsQueryString}`; } else { queryString = - `ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}` + `ParentAccountNumber=${this.order.accountNumber}` + `&BillToAccountNumber=${this.billToNumberToUse}` + `&ProviderNumber=${providerNumber}` + `&AppointmentType=${appointmentType}` - + `&${pricedLineItemsFormattedForRequest}`; + + `&${lineItemsQueryString}`; } const lineItemServerData = this.order.lineItems.serverData; @@ -2370,7 +2411,6 @@ export const useMainStore = defineStore({ resetPartsAndDependencies() { this.resetGlassPartsState(); this.updateVaps(null); - this.updateMobileFee(null); this.resetServiceLocationAndDependencies(); }, @@ -2412,6 +2452,8 @@ export const useMainStore = defineStore({ const { experiments } = this.applicationUser; const { issConfig } = this; + submittedOrder.isUnverified = this.isUnverified; + // set to local storage window.sessionStorage.setItem(webStorageConstants.SUBMITTED_ORDER, JSON.stringify(submittedOrder)); @@ -2543,29 +2585,13 @@ function addTaxesToPricedLineItems(pricedLineItems, taxingLineItems = []) { return pricedLineItems; } -function getLineItemQueryStringForPricing(lineItems) { - return lineItems.map((lineItem, index) => { - let queryStringSnippet = `&LineItems[${index}].partNumber=${lineItem.partNumber}`; - if (lineItem.childParts) { - queryStringSnippet += getLineItemQueryStringForPricing(lineItem.childParts); - } - - return queryStringSnippet; - }).join(''); +function getLineItemQueryString(lineItems, parameterName) { + return getLineItemsFlattened(lineItems).map((lineItem, index) => + `${parameterName}[${index}].partNumber=${lineItem.partNumber}`).join('&'); } -function getFlattenedArrayOfLineItemsWithChildParts(lineItems) { - let flattenedArray = []; - lineItems?.forEach((lineItem) => { - flattenedArray.push(lineItem); - if (lineItem.childParts) { - flattenedArray = [ - ...flattenedArray, - ...getFlattenedArrayOfLineItemsWithChildParts(lineItem.childParts) - ]; - } - }); - return flattenedArray; +function getLineItemsFlattened(lineItems) { + return lineItems?.flatMap((li) => [li, ...(getLineItemsFlattened(li.childParts) ?? [])]) ?? []; } function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, parameterName) { From 2a6e6bd82a18a784661ac66d933ab12675e53eb1 Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Mon, 6 May 2024 14:10:44 -0500 Subject: [PATCH 2/6] SSR-1205 Fix Payment-Page Cart Item List Fix feeItems list not updating --- .../cart-dropdown/cart-dropdown.vue | 6 ++++++ src/layouts/payment-page/payment-page.vue | 16 ++-------------- src/store/index.js | 7 +++---- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index a6c6e726..a29509c5 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -337,6 +337,12 @@ export default { rainDefenseCartItem() { return this.getCartItemForVapsPart(partTypeStrings.RAIN_DEFENSE); }, + allCartItems() { + return [ + ...this.servicePackageCartItems, + ...this.nonServicePackageCartItems + ]; + }, packagePrice() { return this.servicePackageCartItems?.reduce( (accumulator, cartItem) => accumulator + (cartItem?.subTotal ?? 0), diff --git a/src/layouts/payment-page/payment-page.vue b/src/layouts/payment-page/payment-page.vue index 34ccc2a1..acf89e2f 100644 --- a/src/layouts/payment-page/payment-page.vue +++ b/src/layouts/payment-page/payment-page.vue @@ -42,6 +42,7 @@ class="px-2">
{ vm.setCmsContent(resultMap.cmsContent); - vm.$nextTick(() => { if (vm.$refs.cart) { - const { cartItems } = vm.$refs.cart; + const cartItems = vm.$refs.cart.allCartItems; vm.getPayInAdvanceLineItems(cartItems); - } else { - vm.getMockPiaLineItems(); } vm.fetchSignatureInfo(resultMap.paymentSignature); @@ -705,16 +703,6 @@ export default { getPayInAdvanceFormattedLineItem(itemName, price, quantity) { return `${itemName}|${price}|${quantity}`; }, - getMockPiaLineItems() { - let lineItems = []; - lineItems = [ - this.getPayInAdvanceFormattedLineItem('Parts and labor', 0, 1) - ]; - lineItems.push(this.getPayInAdvanceFormattedLineItem('New wiper blades', 75.22, 1)); - lineItems.push(this.getPayInAdvanceFormattedLineItem('Recycling', 37.6, 1)); - - this.payInAdvanceLineItems = lineItems.join('||'); - }, getAmountDue() { return getCartTotal(useMainStore().order); }, diff --git a/src/store/index.js b/src/store/index.js index 98f0a690..79685c64 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1661,6 +1661,7 @@ export const useMainStore = defineStore({ if (feeItem) { feeItems.push(feeItem); } + this.order.lineItems.feeItems = feeItems; }, async setPriceAndSalesTaxForOrderLineItems() { @@ -2558,10 +2559,8 @@ function addPricesToLineItems(lineItems, pricingLineItems) { addPricesToLineItems(lineItem.childParts, pricingLineItems); } - const lineItemIndex = pricingLineItems.findIndex((pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber); - - if (lineItemIndex > -1) { - const pricedLineItem = pricingLineItems[lineItemIndex]; + const pricedLineItem = pricingLineItems.find((pricingLineItem) => pricingLineItem.partNumber === lineItem.partNumber); + if (pricedLineItem) { lineItem.laborAmount = pricedLineItem.laborAmount; lineItem.sellingPrice = pricedLineItem.sellingPrice; lineItem.kitPrice = pricedLineItem.kitPrice; From ed8a60dfd3846be031d95ade3f19e562fd07013b Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Tue, 7 May 2024 10:00:27 -0500 Subject: [PATCH 3/6] SSR-1205 Rename cart-dropdown order-> cartOrder --- .../cart-dropdown/cart-dropdown.vue | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index a29509c5..b2e0e337 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -213,42 +213,42 @@ export default { }; }, computed: { - order() { + cartOrder() { return this.submittedOrder ?? useMainStore().order; }, deductible() { - return getDeductible(this.order); + return getDeductible(this.cartOrder); }, showDeductibleCartItem() { return this.isUnverified || (!this.isNoComp && !this.isITAC); }, lineItems() { - return getLineItems(this.order); + return getLineItems(this.cartOrder); }, recycleFeeLineItem() { - return getRecycleFee(this.order); + return getRecycleFee(this.cartOrder); }, servicePrice() { - return getPriceOfLineItems(getServiceLineItems(this.order), false) ?? 0; + return getPriceOfLineItems(getServiceLineItems(this.cartOrder), false) ?? 0; }, isUnverified() { - return isOrderUnverified(this.order); + return isOrderUnverified(this.cartOrder); }, isITAC() { - return isOrderITAC(this.order); + return isOrderITAC(this.cartOrder); }, isNoComp() { - return isOrderNoComp(this.order); + return isOrderNoComp(this.cartOrder); }, // TODO fix rounding subTotal() { - return getSubtotal(this.order); + return getSubtotal(this.cartOrder); }, salesTax() { - return getSalesTax(this.order); + return getSalesTax(this.cartOrder); }, total() { - return getCartTotal(this.order); + return getCartTotal(this.cartOrder); }, amountDue() { return this.showAsPaid ? 0 : this.total; @@ -393,7 +393,7 @@ export default { : null; }, mobileFeeCartItem() { - const mobileFeeLineItem = getMobileFee(this.order); + const mobileFeeLineItem = getMobileFee(this.cartOrder); return mobileFeeLineItem ? this.getCartItem( this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), From 789e98e0b106ed22985d59e7bd9e056d5a57e5a7 Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Tue, 7 May 2024 10:01:03 -0500 Subject: [PATCH 4/6] SSR-1205 Fix glass parts error for repair --- src/store/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index 79685c64..f9029ea1 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -925,7 +925,7 @@ export const useMainStore = defineStore({ let lineItems = [ ...(order.lineItems.supportingItems ?? []), ...(order.lineItems.vaps ?? []), - ...(order.lineItems.glassParts) + ...(order.lineItems.glassParts ?? []) ]; lineItems = getLineItemsFlattened(lineItems); lineItems = lineItems.map((lineItem) => ({ From 8e20fe084c1d429ce5c1a533b894acf280245e9f Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Tue, 7 May 2024 12:12:08 -0500 Subject: [PATCH 5/6] SSR-1205 PR Changes --- src/constants/part-type-strings.js | 1 + src/helpers/cart-helper.js | 16 +- src/helpers/cart-helper.spec.js | 110 +++----- src/helpers/price-calculator.js | 13 +- src/helpers/price-calculator.spec.js | 116 ++++----- .../cart-dropdown/cart-dropdown.vue | 14 +- .../coverage-statement/coverage-statement.vue | 2 +- src/layouts/payment-page/payment-page.spec.js | 237 +++++++++--------- .../time-slot-modal-question.vue | 2 +- .../service-package-question.vue | 4 +- src/store/index.js | 20 +- 11 files changed, 244 insertions(+), 291 deletions(-) diff --git a/src/constants/part-type-strings.js b/src/constants/part-type-strings.js index 786a069d..62d5452c 100644 --- a/src/constants/part-type-strings.js +++ b/src/constants/part-type-strings.js @@ -1,4 +1,5 @@ const partTypeStrings = Object.freeze({ + WINDSHIELD: 'WINDSHIELD', FRONT_WIPER: 'FRONT WIPER', REAR_WIPER: 'REAR WIPER', RAIN_DEFENSE: 'RAIN DEFENSE', diff --git a/src/helpers/cart-helper.js b/src/helpers/cart-helper.js index 0875c5b6..724496fd 100644 --- a/src/helpers/cart-helper.js +++ b/src/helpers/cart-helper.js @@ -56,7 +56,7 @@ export function getAllLineItems(order) { * @param {object} order order object * @returns {{}|undefined} recycle fee line item or undefined */ -export function getRecycleFee(order) { +export function getRecycleFeeLineItem(order) { return getLineItems(order).feeItems?.find((lineItem) => lineItem.partNumber === partNumberStrings.RECYCLE_FEE); } @@ -65,17 +65,17 @@ export function getRecycleFee(order) { * @param {object} order order object * @returns {{}|undefined} mobile fee line item or undefined */ -export function getMobileFee(order) { +export function getMobileFeeLineItem(order) { return getLineItems(order).feeItems?.find((lineItem) => lineItem.partType === partTypeStrings.MOBILE_FEE); } /** * Returns the current deductible on the order * @param {object} order order object - * @returns {number} current deductible + * @returns {number|undefined|null} current deductible */ export function getDeductible(order) { - return order.currentDeductible ?? 0; + return order.currentDeductible; } /** @@ -111,11 +111,11 @@ export function isOrderITAC(order) { * @returns {number} sub total for the order */ export function getSubtotal(order) { - if (!isOrderNoComp(order) && !isOrderITAC(order)) { - return getDeductible(order) + getPriceOfLineItems(getNonServiceLineItems(order), false); + if (!isOrderUnverified(order) && !isOrderNoComp(order) && !isOrderITAC(order)) { + return getDeductible(order) + getPriceOfLineItems(getNonServiceLineItems(order)); } - return getPriceOfLineItems(getAllLineItems(order), false); + return getPriceOfLineItems(getAllLineItems(order)); } /** @@ -125,7 +125,7 @@ export function getSubtotal(order) { */ export function getSalesTax(order) { let tax = 0; - if (isOrderITAC(order) || isOrderNoComp(order)) { + if (!isOrderUnverified(order) && (isOrderITAC(order) || isOrderNoComp(order))) { tax += getTaxOfLineItems(getServiceLineItems(order)); } diff --git a/src/helpers/cart-helper.spec.js b/src/helpers/cart-helper.spec.js index b978f5ec..6d0898a6 100644 --- a/src/helpers/cart-helper.spec.js +++ b/src/helpers/cart-helper.spec.js @@ -3,8 +3,8 @@ import { getCartTotal, getDeductible, getLineItems, - getMobileFee, getNonServiceLineItems, - getRecycleFee, getSalesTax, + getMobileFeeLineItem, getNonServiceLineItems, + getRecycleFeeLineItem, getSalesTax, getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp, isOrderUnverified } from '@/helpers/cart-helper'; @@ -183,7 +183,7 @@ describe('cart-helper', () => { }; // Act - const result = getRecycleFee(order); + const result = getRecycleFeeLineItem(order); // Assert expect(result).not.toBeNull(); @@ -197,7 +197,7 @@ describe('cart-helper', () => { }; // Act - const result = getRecycleFee(order); + const result = getRecycleFeeLineItem(order); // Assert expect(result).toBeUndefined(); @@ -212,7 +212,7 @@ describe('cart-helper', () => { }; // Act - const result = getMobileFee(order); + const result = getMobileFeeLineItem(order); // Assert expect(result).not.toBeNull(); @@ -226,7 +226,7 @@ describe('cart-helper', () => { }; // Act - const result = getMobileFee(order); + const result = getMobileFeeLineItem(order); // Assert expect(result).toBeUndefined(); @@ -251,35 +251,22 @@ describe('cart-helper', () => { }); describe('isOrderUnverified', () => { - test('Returns true if order has isUnverified field and is true', () => { + test.each([[false, false], [true, true]])('Returns %p when policy.isUnverified is %p', (expected, isUnverified) => { // Arrange const order = { - isUnverified: true + isUnverified }; // Act const result = isOrderUnverified(order); // Assert - expect(result).toBeTruthy(); - }); - - test('Returns false if order has isUnverified field and is false', () => { - // Arrange - const order = { - isUnverified: true - }; - - // Act - const result = isOrderUnverified(order); - - // Assert - expect(result).toBeTruthy(); + expect(result).toBe(expected); }); }); describe('isOrderNoComp', () => { - test('Returns null when policy is null', () => { + test('Returns false when policy is null', () => { // Arrange const order = {}; @@ -290,11 +277,11 @@ describe('cart-helper', () => { expect(result).toBeFalsy(); }); - test('Returns true when policy.NoCoverage is true', () => { + test.each([[false, false], [true, true]])('Returns %p when policy.NoCoverage is %p', (expected, noCoverage) => { // Arrange const order = { policy: { - noCoverage: true + noCoverage } }; @@ -302,27 +289,12 @@ describe('cart-helper', () => { const result = isOrderNoComp(order); // Assert - expect(result).toBeTruthy(); - }); - - test('Returns false when policy.NoCoverage is false', () => { - // Arrange - const order = { - policy: { - noCoverage: false - } - }; - - // Act - const result = isOrderNoComp(order); - - // Assert - expect(result).toBeFalsy(); + expect(result).toBe(expected); }); }); describe('isOrderITAC', () => { - test('Returns null when policy is null', () => { + test('Returns false when policy is null', () => { // Arrange const order = {}; @@ -333,11 +305,11 @@ describe('cart-helper', () => { expect(result).toBeFalsy(); }); - test('Returns true when policy.isITAC is true', () => { + test.each([[false, false], [true, true]])('Returns %p when policy.isITAC is %p', (expected, isITAC) => { // Arrange const order = { policy: { - isITAC: true + isITAC } }; @@ -345,22 +317,7 @@ describe('cart-helper', () => { const result = isOrderITAC(order); // Assert - expect(result).toBeTruthy(); - }); - - test('Returns false when policy.isITAC is false', () => { - // Arrange - const order = { - policy: { - isITAC: false - } - }; - - // Act - const result = isOrderITAC(order); - - // Assert - expect(result).toBeFalsy(); + expect(result).toBe(expected); }); }); @@ -368,6 +325,7 @@ describe('cart-helper', () => { test('Returns 0 when there are null items', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true }, @@ -384,6 +342,7 @@ describe('cart-helper', () => { test('Returns 0 when there are no items', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true }, @@ -397,9 +356,10 @@ describe('cart-helper', () => { expect(result).toBe(0); }); - test('Returns sub total of deductible + vaps + fee line items', () => { + test('Returns sub total of deductible + vaps + fee line items when verified deductible', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: false, isITAC: false @@ -415,9 +375,10 @@ describe('cart-helper', () => { expect(result).toBe(310); }); - test('Returns sub total of all line items when no comp', () => { + test('Returns sub total of all line items when verified no comp', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true, isITAC: false @@ -433,9 +394,10 @@ describe('cart-helper', () => { expect(result).toBe(330); }); - test('Returns sub total of all line items when ITAC', () => { + test('Returns sub total of all line items when verified ITAC', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: false, isITAC: true @@ -456,6 +418,7 @@ describe('cart-helper', () => { test('Returns 0 when there are null items', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true }, @@ -472,6 +435,7 @@ describe('cart-helper', () => { test('Returns 0 when there are no items', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true }, @@ -485,9 +449,10 @@ describe('cart-helper', () => { expect(result).toBe(0); }); - test('Returns sales tax vaps + fee line items', () => { + test('Returns sales tax vaps + fee line items when verified', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: false, isITAC: false @@ -503,9 +468,10 @@ describe('cart-helper', () => { expect(result).toBe(15); }); - test('Returns sales tax of all line items when no comp', () => { + test('Returns sales tax of all line items when verified no comp', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true, isITAC: false @@ -521,9 +487,10 @@ describe('cart-helper', () => { expect(result).toBe(21); }); - test('Returns sales tax of all line items when ITAC', () => { + test('Returns sales tax of all line items when verified ITAC', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: false, isITAC: true @@ -544,6 +511,7 @@ describe('cart-helper', () => { test('Returns 0 when there are null items', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true }, @@ -560,6 +528,7 @@ describe('cart-helper', () => { test('Returns 0 when there are no items', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true }, @@ -573,9 +542,10 @@ describe('cart-helper', () => { expect(result).toBe(0); }); - test('Returns total of vaps + fee line items', () => { + test('Returns total of vaps + fee line items when verified', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: false, isITAC: false @@ -591,9 +561,10 @@ describe('cart-helper', () => { expect(result).toBe(325); }); - test('Returns total of all line items when no comp', () => { + test('Returns total of all line items when verified no comp', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: true, isITAC: false @@ -609,9 +580,10 @@ describe('cart-helper', () => { expect(result).toBe(351); }); - test('Returns total of all line items when ITAC', () => { + test('Returns total of all line items when verified ITAC', () => { // Arrange const order = { + isUnverified: false, policy: { noCoverage: false, isITAC: true diff --git a/src/helpers/price-calculator.js b/src/helpers/price-calculator.js index 72634df7..de4cb7e9 100644 --- a/src/helpers/price-calculator.js +++ b/src/helpers/price-calculator.js @@ -1,17 +1,13 @@ /** * Returns the price of a single line item. Will include sales tax if includeTax is true * @param {object} lineItem line item to price - * @param {boolean} includeTax if sales tax should be included * @returns {number} price of the line item */ -export function getPriceOfLineItem(lineItem, includeTax = false) { +export function getPriceOfLineItem(lineItem) { let price = (lineItem.kitPrice ?? 0) + (lineItem.laborAmount ?? 0) + (lineItem.sellingPrice ?? 0); - if (includeTax) { - price += lineItem.salesTax ?? 0; - } if (lineItem.childParts && lineItem.childParts.length !== 0) { // eslint-disable-next-line no-use-before-define - price += getPriceOfLineItems(lineItem.childParts, includeTax); + price += getPriceOfLineItems(lineItem.childParts); } return price; } @@ -19,11 +15,10 @@ export function getPriceOfLineItem(lineItem, includeTax = false) { /** * Returns the price for the given array of line items. Will include sales tax if includeTax is true * @param {Array} lineItems array of line items to be priced - * @param {boolean} includeTax if sales tax should be included * @returns {number} price of the lines items */ -export function getPriceOfLineItems(lineItems, includeTax = false) { - return lineItems?.reduce((accumulator, lineItem) => accumulator + getPriceOfLineItem(lineItem, includeTax), 0) ?? 0; +export function getPriceOfLineItems(lineItems) { + return lineItems?.reduce((accumulator, lineItem) => accumulator + getPriceOfLineItem(lineItem), 0) ?? 0; } /** diff --git a/src/helpers/price-calculator.spec.js b/src/helpers/price-calculator.spec.js index b415d0ab..0150ef96 100644 --- a/src/helpers/price-calculator.spec.js +++ b/src/helpers/price-calculator.spec.js @@ -11,24 +11,13 @@ describe('price-calculator', () => { const lineItem = {}; // Act - const result = getPriceOfLineItem(lineItem, false); + const result = getPriceOfLineItem(lineItem); // Assert expect(result).toBe(0); }); - test('Empty lineItem with salesTax returns 0', () => { - // Arrange - const lineItem = {}; - - // Act - const result = getPriceOfLineItem(lineItem, true); - - // Assert - expect(result).toBe(0); - }); - - test('Returns kit, labor, sellingPrice total when salesTax is false', () => { + test('Returns kit, labor, sellingPrice total', () => { // Arrange const lineItem = { kitPrice: 1, @@ -38,27 +27,11 @@ describe('price-calculator', () => { }; // Act - const result = getPriceOfLineItem(lineItem, false); + const result = getPriceOfLineItem(lineItem); // Assert expect(result).toBe(6); }); - - test('Returns kit, labor, sellingPrice, salesTax total when salesTax is true', () => { - // Arrange - const lineItem = { - kitPrice: 1, - laborAmount: 2, - sellingPrice: 3, - salesTax: 4 - }; - - // Act - const result = getPriceOfLineItem(lineItem, true); - - // Assert - expect(result).toBe(10); - }); }); describe('getPriceOfLineItems', () => { @@ -67,7 +40,7 @@ describe('price-calculator', () => { const lineItems = null; // Act - const result = getPriceOfLineItems(lineItems, true); + const result = getPriceOfLineItems(lineItems); // Assert expect(result).toBe(0); @@ -78,7 +51,7 @@ describe('price-calculator', () => { const lineItems = []; // Act - const result = getPriceOfLineItems(lineItems, true); + const result = getPriceOfLineItems(lineItems); // Assert expect(result).toBe(0); @@ -95,12 +68,12 @@ describe('price-calculator', () => { const expected = 6; // Act - const result = getPriceOfLineItems(lineItems, false); + const result = getPriceOfLineItems(lineItems); // Assert expect(result).toBe(expected); }); - test('Returns expected when multiple line items', () => { + test('Returns expected when multiple line items and child parts', () => { // Arrange const lineItems = [ { @@ -111,44 +84,30 @@ describe('price-calculator', () => { { kitPrice: 1 }, - { - kitPrice: 10, - laborAmount: 100, - sellingPrice: 1000 - } - ]; - const expected = 1117; - - // Act - const result = getPriceOfLineItems(lineItems, false); - - // Assert - expect(result).toBe(expected); - }); - test('Returns expected when multiple line items with sales tax', () => { - // Arrange - const lineItems = [ - { - kitPrice: 1, - laborAmount: 2, - sellingPrice: 3, - salesTax: 4 - }, - { - kitPrice: 1, - salesTax: 2 - }, { kitPrice: 10, laborAmount: 100, sellingPrice: 1000, - salesTax: 50 + childParts: [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3, + salesTax: 4 + }, + { + kitPrice: 2, + laborAmount: 3, + sellingPrice: 4, + salesTax: 5 + } + ] } ]; - const expected = 1173; + const expected = 1132; // Act - const result = getPriceOfLineItems(lineItems, true); + const result = getPriceOfLineItems(lineItems); // Assert expect(result).toBe(expected); @@ -222,5 +181,34 @@ describe('price-calculator', () => { // Assert expect(result).toBe(expected); }); + + test('Returns expected when multiple line items', () => { + // Arrange + const lineItems = [ + { + kitPrice: 1, + laborAmount: 2, + sellingPrice: 3, + salesTax: 4 + }, + { + kitPrice: 1, + salesTax: 2 + }, + { + kitPrice: 10, + laborAmount: 100, + sellingPrice: 1000, + salesTax: 50 + } + ]; + const expected = 56; + + // Act + const result = getTaxOfLineItems(lineItems); + + // Assert + expect(result).toBe(expected); + }); }); }); diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index b2e0e337..d6565c22 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -169,12 +169,12 @@ import widgetFields from '@/constants/cms-widget-fields.js'; import { getCartTotal, getDeductible, - getLineItems, getMobileFee, - getRecycleFee, getSalesTax, + getLineItems, getMobileFeeLineItem, + getRecycleFeeLineItem, getSalesTax, getServiceLineItems, getSubtotal, isOrderITAC, isOrderNoComp, isOrderUnverified } from '@/helpers/cart-helper'; -import {getPriceOfLineItems, getTaxOfLineItems} from '@/helpers/price-calculator'; +import { getPriceOfLineItems, getTaxOfLineItems } from '@/helpers/price-calculator'; const VERIFYING_COVERAGE = 'Verifying coverage'; @@ -226,10 +226,10 @@ export default { return getLineItems(this.cartOrder); }, recycleFeeLineItem() { - return getRecycleFee(this.cartOrder); + return getRecycleFeeLineItem(this.cartOrder); }, servicePrice() { - return getPriceOfLineItems(getServiceLineItems(this.cartOrder), false) ?? 0; + return getPriceOfLineItems(getServiceLineItems(this.cartOrder)) ?? 0; }, isUnverified() { return isOrderUnverified(this.cartOrder); @@ -393,7 +393,7 @@ export default { : null; }, mobileFeeCartItem() { - const mobileFeeLineItem = getMobileFee(this.cartOrder); + const mobileFeeLineItem = getMobileFeeLineItem(this.cartOrder); return mobileFeeLineItem ? this.getCartItem( this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), @@ -424,7 +424,7 @@ export default { name: label, cartItemType: cartItemTypeString, partType, - subTotal: getPriceOfLineItems(lineItems, false), + subTotal: getPriceOfLineItems(lineItems), salesTax: getTaxOfLineItems(lineItems) }; } diff --git a/src/layouts/coverage-statement/coverage-statement.vue b/src/layouts/coverage-statement/coverage-statement.vue index 0bc42c12..aa34079b 100644 --- a/src/layouts/coverage-statement/coverage-statement.vue +++ b/src/layouts/coverage-statement/coverage-statement.vue @@ -344,7 +344,7 @@ export default { ); }, totalServicePrice() { - return getPriceOfLineItems(this.baseServiceLineItems, false); + return getPriceOfLineItems(this.baseServiceLineItems); }, serviceProviderQuestionText() { return this.getCmsContent( diff --git a/src/layouts/payment-page/payment-page.spec.js b/src/layouts/payment-page/payment-page.spec.js index b2bdc2e4..ef3ffc04 100644 --- a/src/layouts/payment-page/payment-page.spec.js +++ b/src/layouts/payment-page/payment-page.spec.js @@ -8,6 +8,10 @@ import { useMainStore } from '@/store'; import { paymentMethods, hopPaymentMethods } from '@/constants/payment-method-constants.js'; import queryStrings from '@/constants/query-strings'; import CartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue'; +import { createTestingPinia } from '@pinia/testing'; +import settleAllPromises from '@/helpers/layout-helper'; +import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; +import baseMixin from '@/mixins/base-mixin'; // Constants const parts = { @@ -105,123 +109,124 @@ const taxedParts = { }; // Setup global mocks -let mockCmsContent = {}; - +// Mock fetchCmsContentForPage jest.mock('@/helpers/cms-content-helper', () => ({ - fetchCmsContentForPage: () => Promise.resolve('content') + fetchCmsContentForPage: jest.fn(), + doesCopyContainRouterLink: jest.fn() })); -function setupMocks({ customMountOptions = {}, queryString }) { +// Mock our module for promises. +jest.mock('@/helpers/layout-helper.js', () => jest.fn()); + +function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRunAfterInitializingStore = () => {}, queryString = {}) { const mountOptions = getMountOptions({ - ...customMountOptions, + router: { + navigate: jest.fn() + }, route: { query: { issPage: 'page-name', ...queryString }, params: {} } }); - // set all mock stuff - mountOptions.mixins = [ - { - methods: { - getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { - if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) { - return mockCmsContent[widgetName][fieldName]; - } - return undefined; - }), - setCmsContent: jest.fn() - } + const testingPinia = createTestingPinia({ + initialState: { + main: mainInitialState } - ]; + }); + useMainStore(testingPinia); + methodToRunAfterInitializingStore(); + + mountOptions.global.plugins = [testingPinia]; + mountOptions.data = () => (initialData); + + const apiResponses = { cmsContent: {} }; + + settleAllPromises.mockImplementation(() => apiResponses); + fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); const wrapper = shallowMount(payment, mountOptions); + wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {}); + wrapper.vm.setCmsContent = jest.fn(); + wrapper.vm.$router.navigateWithSpinner = jest.fn(); + wrapper.vm.navigateBack = baseMixin.methods.navigateBack; - // act on vm - - return wrapper; + return { wrapper }; } -describe('payment-page.vue', () => { - beforeEach(() => { - useMainStore().order = - { - vehicle: { - year: '2020', - make: 'acura', - model: 'mdx', - style: '4-door sedan', - carId: 'dummyCarId', - category: 'dummyCategory', - vin: 'dummyVin' - }, - serviceLocation: { - address: 'add1', - address2: 'add2', - city: 'city', - state: 'state', - zipCode: 'zip', - zipCodeCtu: 'zipCtu', - appointmentType: 'IN_SHOP', - isVehicleProtected: true, - provider: { - providerNumber: 2, - address: { - streetAddress: 'add3', - city: 'city2', - state: 'state2', - zipCode: 'zip2', - zipCodeCtu: 'zipCtu2' - } - }, - techNotes: '' - }, - contactInfo: { - firstName: 'first', - lastName: 'last', - emailAddress: 'builddigitaltest@safelite.com', - servicePhone: '555-555-5555' - }, - damage: { - isRepair: false, - numberOfChips: null, - glassToReplace: [{ location: 'windshield' }] - }, - lineItems: { - glassParts: [parts.windshield], - supportingItems: [], - vaps: [parts.frontWipers], - promos: [] - }, - payment: { - isInsurance: false, - insuranceCoverage: { - isVerified: null, - coverageStatus: null, - coverageVerificationType: null - }, - isPayInAdvance: true, - payInAdvanceType: 'Afterpay', - inactivePromos: [] - }, - schedule: { - date: 'date', - startTime: 'start', - endTime: 'end', - jobMinMinutes: '30', - jobMaxMinutes: '45' +const defaultOrder = { + order: { + vehicle: { + year: '2020', + make: 'acura', + model: 'mdx', + style: '4-door sedan', + carId: 'dummyCarId', + category: 'dummyCategory', + vin: 'dummyVin' + }, + serviceLocation: { + address: 'add1', + address2: 'add2', + city: 'city', + state: 'state', + zipCode: 'zip', + zipCodeCtu: 'zipCtu', + appointmentType: 'IN_SHOP', + isVehicleProtected: true, + provider: { + providerNumber: 2, + address: { + streetAddress: 'add3', + city: 'city2', + state: 'state2', + zipCode: 'zip2', + zipCodeCtu: 'zipCtu2' } - }; - - mockCmsContent = {}; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); + }, + techNotes: '' + }, + contactInfo: { + firstName: 'first', + lastName: 'last', + emailAddress: 'builddigitaltest@safelite.com', + servicePhone: '555-555-5555' + }, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [{location: 'windshield'}] + }, + lineItems: { + glassParts: [parts.windshield], + supportingItems: [], + vaps: [parts.frontWipers], + promos: [] + }, + payment: { + isInsurance: false, + insuranceCoverage: { + isVerified: null, + coverageStatus: null, + coverageVerificationType: null + }, + isPayInAdvance: true, + payInAdvanceType: 'Afterpay', + inactivePromos: [] + }, + schedule: { + date: 'date', + startTime: 'start', + endTime: 'end', + jobMinMinutes: '30', + jobMaxMinutes: '45' + } + } +}; +describe('payment-page.vue', () => { describe('arePagePrerequisitesValid', () => { test('Returns true in nominal conditions', () => { // Arrange // Store defaults are valid - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); // Act const result = wrapper.vm.arePagePrerequisitesValid(); @@ -233,9 +238,9 @@ describe('payment-page.vue', () => { describe('Payment method cases', () => { test('Returns false if pia options are not valid', () => { // Arrange + const { wrapper } = getMountedComponent(defaultOrder); useMainStore().order.payment.isPayInAdvance = null; useMainStore().order.payment.payInAdvanceType = null; - const wrapper = setupMocks({}); // Act const result = wrapper.vm.arePagePrerequisitesValid(); @@ -246,9 +251,9 @@ describe('payment-page.vue', () => { test('Returns false if pay at time of service', () => { // Arrange + const { wrapper } = getMountedComponent(defaultOrder); useMainStore().order.payment.isPayInAdvance = false; useMainStore().order.payment.payInAdvanceType = null; - const wrapper = setupMocks({}); // Act const result = wrapper.vm.arePagePrerequisitesValid(); @@ -263,8 +268,8 @@ describe('payment-page.vue', () => { describe('getPaymentType', () => { test('Maps AFTERPAY -> hopPaymentMethods.AFTERPAY', () => { // Arrange + const { wrapper } = getMountedComponent(defaultOrder); useMainStore().order.payment.payInAdvanceType = paymentMethods.AFTERPAY; - const wrapper = setupMocks({}); // Act const mapped = wrapper.vm.getPaymentType(); @@ -275,8 +280,8 @@ describe('payment-page.vue', () => { test('Maps CREDIT_CARD -> hopPaymentMethods.CREDIT_CARD', () => { // Arrange + const { wrapper } = getMountedComponent(defaultOrder); useMainStore().order.payment.payInAdvanceType = paymentMethods.CREDIT_CARD; - const wrapper = setupMocks({}); // Act const mapped = wrapper.vm.getPaymentType(); @@ -287,8 +292,8 @@ describe('payment-page.vue', () => { test('Maps PAYPAL -> hopPaymentMethods.PAYPAL', () => { // Arrange + const { wrapper } = getMountedComponent(defaultOrder); useMainStore().order.payment.payInAdvanceType = paymentMethods.PAYPAL; - const wrapper = setupMocks({}); // Act const mapped = wrapper.vm.getPaymentType(); @@ -299,8 +304,8 @@ describe('payment-page.vue', () => { test('Maps other values to self', () => { // Arrange + const { wrapper } = getMountedComponent(defaultOrder); useMainStore().order.payment.payInAdvanceType = 'SomeOtherText'; - const wrapper = setupMocks({}); // Act const mapped = wrapper.vm.getPaymentType(); @@ -312,7 +317,7 @@ describe('payment-page.vue', () => { describe('cart-dropdown', () => { test('Show cart dropdown if credit card', async () => { - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); wrapper.vm.paymentType = hopPaymentMethods.CREDIT_CARD; // Act @@ -324,7 +329,7 @@ describe('payment-page.vue', () => { }); test('show cart dropdown if afterpay', async () => { - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); wrapper.vm.paymentType = hopPaymentMethods.AFTERPAY; // Act @@ -336,7 +341,7 @@ describe('payment-page.vue', () => { }); test('Hide cart dropdown if paypal', async () => { - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); wrapper.vm.paymentType = hopPaymentMethods.PAYPAL; // Act @@ -352,7 +357,7 @@ describe('payment-page.vue', () => { test('Is responsive if initial data changes', async () => { // Arrange // note: begins with payment-type = afterpay - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); // Act const preVal = wrapper.vm.isPaypal; @@ -377,7 +382,7 @@ describe('payment-page.vue', () => { signature: 'SIGNATURE', startDate: 'STARTDATE' }; - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); wrapper.vm.submitHopForm = jest.fn(); @@ -396,7 +401,7 @@ describe('payment-page.vue', () => { describe('submitHopForm', () => { test('Submits form', async () => { // Arrange - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); wrapper.vm.$refs.hopForm.submit = jest.fn(); wrapper.vm.$refs.paymentFrame = null; @@ -418,7 +423,7 @@ describe('payment-page.vue', () => { data: 'afterpayClosed' }; - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); wrapper.vm.backButtonAction = jest.fn(); @@ -435,7 +440,7 @@ describe('payment-page.vue', () => { data: 'creditCardSubmit' }; - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); // Act wrapper.vm.handleIFrameContentWindowMessage(event); @@ -449,7 +454,7 @@ describe('payment-page.vue', () => { describe('UI Blocking', () => { test('UI block appears when toggled', async () => { // Arrange - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); // Act wrapper.vm.setUIBlock(true); @@ -464,7 +469,7 @@ describe('payment-page.vue', () => { test("Don't propogate clicks from UI block", async () => { // Arrange - const wrapper = setupMocks({}); + const { wrapper } = getMountedComponent(defaultOrder); const outerDiv = wrapper.find('.container-fluid'); const clickFn = jest.fn(); @@ -490,7 +495,7 @@ describe('payment-page.vue', () => { const queryString = { [queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT]: paymentMethods.CREDIT_CARD }; - const wrapper = setupMocks({ queryString }); + const { wrapper } = getMountedComponent(defaultOrder, {}, () => {}, queryString); // Act @@ -507,7 +512,7 @@ describe('payment-page.vue', () => { const queryString = { [queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT]: paymentMethods.AFTERPAY }; - const wrapper = setupMocks({ queryString }); + const { wrapper } = getMountedComponent(defaultOrder, {}, () => {}, queryString); // Act diff --git a/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue index c26e739b..6f44e2d5 100644 --- a/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue +++ b/src/layouts/schedule-page/time-slot-modal-question/time-slot-modal-question.vue @@ -423,7 +423,7 @@ export default { }, getPremiumAppointmentTimeSlot(timeSlotData) { const formattedPrice = - `+${formatAmountInDollars(getPriceOfLineItem(this.premiumAppointmentFee, false))}`; + `+${formatAmountInDollars(getPriceOfLineItem(this.premiumAppointmentFee))}`; return { // Unique value is required for each and the premium appoinment shares an id diff --git a/src/layouts/service-packages/service-package-question/service-package-question.vue b/src/layouts/service-packages/service-package-question/service-package-question.vue index 5092d581..06a97fdf 100644 --- a/src/layouts/service-packages/service-package-question/service-package-question.vue +++ b/src/layouts/service-packages/service-package-question/service-package-question.vue @@ -187,7 +187,7 @@ export default { && item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) || (priceRearWipers && item.partType.toUpperCase() === partTypeStrings.REAR_WIPER) ) { - vapsPrice += getPriceOfLineItem(item, false); + vapsPrice += getPriceOfLineItem(item); } }); return vapsPrice; @@ -206,7 +206,7 @@ export default { || (priceRainDefense && item.partType.toUpperCase() === partTypeStrings.RAIN_DEFENSE) ) { - vapsPrice += getPriceOfLineItem(item, false); + vapsPrice += getPriceOfLineItem(item); } }); return vapsPrice; diff --git a/src/store/index.js b/src/store/index.js index f9029ea1..8c6c5c7b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -24,7 +24,7 @@ import { import partTypeStrings from '@/constants/part-type-strings'; import bailoutMessage from '@/constants/bailoutMessage'; import bailoutCode from '@/constants/bailoutCode'; -import partNumberStrings from "@/constants/part-number-strings"; +import partNumberStrings from '@/constants/part-number-strings'; const storeId = 'main'; @@ -276,17 +276,9 @@ export const useMainStore = defineStore({ hasExactlyOneChip: () => state.order.damage.numberOfChips === 1, isPolicyVehicle: () => state.order.vehicle.policyVehicleId != null, hasWindshieldReplacement: (s) => { - const { damage } = s.order; - if (damage.isRepair) { - return false; - } - - const { glassParts } = s.order.lineItems; - if (!glassParts || glassParts.length === 0) { - return false; - } - - return glassParts.findIndex((gp) => gp.partType === 'WINDSHIELD') !== -1; + const { damage, lineItems } = s.order; + return !damage.isRepair + && (lineItems.glassParts?.some((gp) => gp.partType === partTypeStrings.WINDSHIELD) ?? false); }, hasAnyNonWindshieldGlassParts: (s) => !s.order.policy.isDamageGlassOnly, isMobileAppointment: (state) => state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE @@ -1630,7 +1622,7 @@ export const useMainStore = defineStore({ updateMobileFee(fee) { // Mobile Fee is only added for NO COMP or ITAC - if (fee && (this.isNoComp || this.isITAC)) { + if (fee && !this.isUnverified && (this.isNoComp || this.isITAC)) { this.addPartTypeFeeItem(fee, partTypeStrings.MOBILE_FEE); } else { this.addPartTypeFeeItem(null, partTypeStrings.MOBILE_FEE); @@ -1639,7 +1631,7 @@ export const useMainStore = defineStore({ updateRecycleFee(fee) { // Recycle Fee is only added for NO COMP or ITAC and has Windshield Replacement - if (fee && (this.isNoComp || this.isITAC) && this.hasWindshieldReplacement) { + if (fee && !this.isUnverified && (this.isNoComp || this.isITAC) && this.hasWindshieldReplacement) { this.addPartNumberFeeItem(fee, partNumberStrings.RECYCLE_FEE); } else { this.addPartNumberFeeItem(null, partNumberStrings.RECYCLE_FEE); From 82d63a5d668d01351471c9db70da2d503601e60c Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Tue, 7 May 2024 14:38:43 -0500 Subject: [PATCH 6/6] SSR-1205 Move store helpers and add unit tests --- src/helpers/line-items-helper.js | 4 ++ src/helpers/line-items-helper.spec.js | 55 ++++++++++++++++++++++++++ src/helpers/querystring-helper.js | 19 +++++++++ src/helpers/querystring-helper.spec.js | 47 ++++++++++++++++++++++ src/store/index.js | 30 +++----------- 5 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 src/helpers/line-items-helper.js create mode 100644 src/helpers/line-items-helper.spec.js create mode 100644 src/helpers/querystring-helper.spec.js diff --git a/src/helpers/line-items-helper.js b/src/helpers/line-items-helper.js new file mode 100644 index 00000000..b31d6f53 --- /dev/null +++ b/src/helpers/line-items-helper.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/prefer-default-export +export function getLineItemsFlattened(lineItems) { + return lineItems?.flatMap((li) => [li, ...(getLineItemsFlattened(li.childParts))]) ?? []; +} diff --git a/src/helpers/line-items-helper.spec.js b/src/helpers/line-items-helper.spec.js new file mode 100644 index 00000000..af8337a7 --- /dev/null +++ b/src/helpers/line-items-helper.spec.js @@ -0,0 +1,55 @@ +import { getLineItemsFlattened } from '@/helpers/line-items-helper'; + +describe('getLineItemsFlattened', () => { + describe('getLineItemsFlattened', () => { + it('Returns empty array when null is passed', () => { + // Arrange + const lineItems = null; + // Act + const result = getLineItemsFlattened(lineItems); + + // Assert + expect(result).not.null; + expect(result).toStrictEqual([]); + }); + + it('Returns empty array when empty array is passed', () => { + // Arrange + const lineItems = []; + // Act + const result = getLineItemsFlattened(lineItems); + + // Assert + expect(result).not.null; + expect(result).toStrictEqual([]); + }); + + it('Returns line items as flat array without child parts', () => { + // Arrange + const lineItems = [{ partType: 'a' }, { partType: 'b' }, { partType: 'c' }]; + // Act + const result = getLineItemsFlattened(lineItems); + + // Assert + expect(result).not.null; + expect(result).toStrictEqual(lineItems); + }); + + it('Returns line items as flat array with child parts', () => { + // Arrange + const a = { partType: 'a' }; + const c = { partType: 'c' }; + const d = { partType: 'd' }; + const b = { partType: 'b', childParts: [c, d] }; + const e = { partType: 'e', childParts: null }; + + const lineItems = [a, b, e]; + // Act + const result = getLineItemsFlattened(lineItems); + + // Assert + expect(result).not.null; + expect(result).toStrictEqual([a, b, c, d, e]); + }); + }); +}); diff --git a/src/helpers/querystring-helper.js b/src/helpers/querystring-helper.js index f4df1ed7..59c1579c 100644 --- a/src/helpers/querystring-helper.js +++ b/src/helpers/querystring-helper.js @@ -1,3 +1,5 @@ +import { getLineItemsFlattened } from '@/helpers/line-items-helper'; + export default function getQueryStringParameter(key) { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); @@ -9,3 +11,20 @@ export default function getQueryStringParameter(key) { return lowerCaseParams.get(key.toLowerCase()); } + +export function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, parameterName) { + let queryStringParameter = ''; + for (let i = 0; i < arrayOfObjects.length; i++) { + for (const [key, value] of Object.entries(arrayOfObjects[i])) { + queryStringParameter += `${parameterName}[${i}].${key}=${value}&`; + } + } + // Remove trailing & + return queryStringParameter.slice(0, -1); +} + +export function getLineItemQueryString(lineItems, parameterName) { + const queryString = getLineItemsFlattened(lineItems).map((lineItem, index) => + `${parameterName}[${index}].partNumber=${lineItem.partNumber}`).join('&'); + return queryString.length !== 0 ? `&${queryString}` : ''; +} diff --git a/src/helpers/querystring-helper.spec.js b/src/helpers/querystring-helper.spec.js new file mode 100644 index 00000000..1c7378be --- /dev/null +++ b/src/helpers/querystring-helper.spec.js @@ -0,0 +1,47 @@ +import { buildQueryStringParameterFromArrayOfComplexObjects, getLineItemQueryString } from '@/helpers/querystring-helper'; + +describe('querystring-helper', () => { + describe('buildQueryStringParameterFromArrayOfComplexObjects', () => { + it('returns a query string from a complex object', () => { + // Arrange + const object = [{ a: 'a', val: 24 }, { a: 'b', val: '52' }]; + + // Act + const result = buildQueryStringParameterFromArrayOfComplexObjects(object, 'param'); + + // Assert + expect(result).toBe('param[0].a=a¶m[0].val=24¶m[1].a=b¶m[1].val=52'); + }); + }); + + describe('getLineItemQueryString', () => { + it('returns empty query string from empty line items', () => { + // Arrange + const lineItems = []; + + // Act + const result = getLineItemQueryString(lineItems, 'param'); + + // Assert + expect(result).toBe(''); + }); + + it('returns a query string from line items', () => { + // Arrange + const a = { partNumber: 'a' }; + const c = { partNumber: 'c' }; + const d = { partNumber: 'd' }; + const b = { partNumber: 'b', childParts: [c, d] }; + const e = { partNumber: 'e' }; + + const lineItems = [a, b, e]; + + // Act + const result = getLineItemQueryString(lineItems, 'param'); + + // Assert + // eslint-disable-next-line max-len + expect(result).toBe('¶m[0].partNumber=a¶m[1].partNumber=b¶m[2].partNumber=c¶m[3].partNumber=d¶m[4].partNumber=e'); + }); + }); +}); diff --git a/src/store/index.js b/src/store/index.js index 8c6c5c7b..1b83b8a2 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -25,6 +25,8 @@ import partTypeStrings from '@/constants/part-type-strings'; import bailoutMessage from '@/constants/bailoutMessage'; import bailoutCode from '@/constants/bailoutCode'; import partNumberStrings from '@/constants/part-number-strings'; +import { getLineItemsFlattened } from '@/helpers/line-items-helper'; +import { buildQueryStringParameterFromArrayOfComplexObjects, getLineItemQueryString } from '@/helpers/querystring-helper'; const storeId = 'main'; @@ -1063,7 +1065,7 @@ export const useMainStore = defineStore({ + `&CTU=${ctuToUse}` + `&Deductible=${deductibleToUse}` + `&ZipCode=${zipCodeToUse}` - + `&${lineItemsQueryString}`; + + `${lineItemsQueryString}`; const lineItemServerData = this.order.lineItems?.serverData; if (lineItemServerData) { @@ -1112,7 +1114,7 @@ export const useMainStore = defineStore({ return globalMethods.callHttpClient({ method: endpoints.GetServiceabilityDetails.method, - endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&${lineItems}&${glassPieces}` + endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}${lineItems}&${glassPieces}` }); }, mapTaxedLineItemsToStoreFormat(availableLineItems, storeLineItems) { @@ -2028,14 +2030,14 @@ export const useMainStore = defineStore({ + `&ServiceLocation.City=${serviceLocationCity}` + `&ServiceLocation.State=${serviceLocationState}` + `&ServiceLocation.ZipCode=${serviceLocationZipCode}` - + `&${lineItemsQueryString}`; + + `${lineItemsQueryString}`; } else { queryString = `ParentAccountNumber=${this.order.accountNumber}` + `&BillToAccountNumber=${this.billToNumberToUse}` + `&ProviderNumber=${providerNumber}` + `&AppointmentType=${appointmentType}` - + `&${lineItemsQueryString}`; + + `${lineItemsQueryString}`; } const lineItemServerData = this.order.lineItems.serverData; @@ -2576,26 +2578,6 @@ function addTaxesToPricedLineItems(pricedLineItems, taxingLineItems = []) { return pricedLineItems; } -function getLineItemQueryString(lineItems, parameterName) { - return getLineItemsFlattened(lineItems).map((lineItem, index) => - `${parameterName}[${index}].partNumber=${lineItem.partNumber}`).join('&'); -} - -function getLineItemsFlattened(lineItems) { - return lineItems?.flatMap((li) => [li, ...(getLineItemsFlattened(li.childParts) ?? [])]) ?? []; -} - -function buildQueryStringParameterFromArrayOfComplexObjects(arrayOfObjects, parameterName) { - let queryStringParameter = ''; - for (let i = 0; i < arrayOfObjects.length; i++) { - for (const [key, value] of Object.entries(arrayOfObjects[i])) { - queryStringParameter += `${parameterName}[${i}].${key}=${value}&`; - } - } - // Remove trailing & - return queryStringParameter.slice(0, -1); -} - function convertGlassPieceToBackEndCompatibleFormat(glassPieces) { return glassPieces.map((glassPiece) => ({ location: glassPiece.glassLocation,