From 6957bcc9515f1a1e857ddff5b4aba2b4e21bee61 Mon Sep 17 00:00:00 2001 From: Josh Dassinger Date: Thu, 2 May 2024 14:16:12 -0500 Subject: [PATCH] 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) {