diff --git a/.eslintrc.js b/.eslintrc.js index 440245f9..33b69824 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,7 +44,8 @@ module.exports = { math: 'always' }], 'import/extensions': ['error', 'always', { js: 'ignorePackages' }], - 'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['item'] }] + 'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['item'] }], + 'no-restricted-syntax': ['off', 'ForOfStatement'] }, settings: { 'import/resolver': { diff --git a/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap b/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap index ae4e42e9..5baa028a 100644 --- a/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap +++ b/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap @@ -2,12 +2,7 @@ exports[`cart-dropdown component initial data rendered as expected 1`] = ` Object { - "baseServiceLineItems": Array [], - "deductible": null, - "glassToReplace": Array [], "isExpanded": false, - "isRepair": null, - "vapsInOrder": Array [], "widget": Object { "amountDue": "AmountDueTextWidget", "basePrice": "BasePriceWidget", diff --git a/src/iss-components/cart-dropdown/cart-dropdown.spec.js b/src/iss-components/cart-dropdown/cart-dropdown.spec.js index a123f42e..fdb2362d 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.spec.js +++ b/src/iss-components/cart-dropdown/cart-dropdown.spec.js @@ -4,7 +4,7 @@ import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue'; // Supporting Files import { getMountOptions } from '@/helpers/unit-test-helper.js'; -import { useMainStore } from '@/store'; +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'; @@ -455,7 +455,193 @@ describe('cart-dropdown component', () => { // TODO when method implemented }); describe('salesTax', () => { - // TODO when method implemented + test('salesTax is treated as 0 when line items are null', () => { + // Arrange + const storeData = getDefaultState(); + storeData.order.lineItems.glassParts = null; + storeData.order.lineItems.otherParts = null; + storeData.order.lineItems.supportingItems = null; + storeData.order.lineItems.vaps = null; + + 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 = getDefaultState(); + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: null }, + { partType: 'mock', salesTax: undefined } + ]; + storeData.order.lineItems.supportingItems = [ + { partType: 'mock', salesTax: null }, + { partType: 'mock', salesTax: undefined } + ]; + storeData.order.lineItems.vaps = [ + { partType: 'mock', salesTax: null }, + { partType: 'mock', salesTax: undefined } + ]; + + 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 = getDefaultState(); + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: 10 } + ]; + storeData.order.lineItems.vaps = []; + storeData.order.payment.insuranceCoverage.isVerified = false; + + 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 = getDefaultState(); + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: 10 } + ]; + storeData.order.lineItems.vaps = [ + { partType: 'mock', salesTax: 1 }, + { partType: 'mock', salesTax: 2 } + ]; + storeData.order.payment.insuranceCoverage.isVerified = false; + + 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 = getDefaultState(); + storeData.order.currentDeductible = 250; + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: 10, sellingPrice: 150 } + ]; + storeData.order.lineItems.supportingItems = [ + { partNumber: partNumberStrings.RECYCLE_FEE, partType: 'mock', salesTax: 10, sellingPrice: 39.99 } + ]; + storeData.order.lineItems.vaps = null; + storeData.order.payment.insuranceCoverage.isVerified = 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 = getDefaultState(); + storeData.order.currentDeductible = 250; + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: 10, sellingPrice: 100 } + ]; + storeData.order.lineItems.otherParts = [ + { partType: 'mock', salesTax: 10, kitPrice: 100 } + ]; + storeData.order.lineItems.supportingItems = [ + { partNumber: partNumberStrings.RECYCLE_FEE, partType: 'mock', salesTax: 10, sellingPrice: 39.99 }, + { partType: 'mock', salesTax: 10, kitPrice: 100 } + ]; + storeData.order.lineItems.vaps = [ + { partType: 'mock', salesTax: 2 }, + { partType: 'mock', salesTax: 3 } + ]; + storeData.order.payment.insuranceCoverage.isVerified = 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 = getDefaultState(); + storeData.order.currentDeductible = 0; + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: 10, sellingPrice: 100 } + ]; + storeData.order.lineItems.otherParts = [ + { partType: 'mock', salesTax: 10, kitPrice: 100 } + ]; + storeData.order.lineItems.supportingItems = [ + { partType: 'mock', salesTax: 10, kitPrice: 100 } + ]; + storeData.order.lineItems.vaps = [ + { partType: 'mock', salesTax: 5 } + ]; + storeData.order.payment.insuranceCoverage.isVerified = true; + storeData.order.policy.isITAC = 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 = getDefaultState(); + storeData.order.currentDeductible = 0; + storeData.order.lineItems.glassParts = [ + { partType: 'mock', salesTax: 10, sellingPrice: 100 } + ]; + storeData.order.lineItems.otherParts = [ + { partType: 'mock', salesTax: 10, kitPrice: 100 } + ]; + storeData.order.lineItems.supportingItems = [ + { partType: 'mock', salesTax: 10, kitPrice: 100 } + ]; + storeData.order.lineItems.vaps = [ + { partType: 'mock', salesTax: 5 } + ]; + storeData.order.payment.insuranceCoverage.isVerified = true; + storeData.order.policy.noCoverage = true; + + const { wrapper } = getMountedComponent(storeData); + + // Act + const result = wrapper.vm.salesTax; + + // Assert + expect(result).toBe(35); + }); }); describe('availableLineItems', () => { test('returns expected when glassParts null', async () => { @@ -997,19 +1183,11 @@ describe('cart-dropdown component', () => { describe('getDisplayed', () => { test('returns "Verifying coverage" when not no comp, not itac, and deductible null', () => { // Arrange - const storeData = { - order: { - payment: { - insuranceCoverage: { - coverageStatus: coverageStatuses.VERIFIED - } - }, - policy: { - isITAC: false - }, - currentDeductible: null - } - }; + const storeData = getDefaultState(); + storeData.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.VERIFIED; + storeData.order.policy.isITAC = false; + storeData.order.currentDeductible = null; + const { wrapper } = getMountedComponent(storeData); const amount = 123; @@ -1021,19 +1199,11 @@ describe('cart-dropdown component', () => { }); test('returns "Verifying coverage" when not no comp, not itac, and coverage status PENDING', () => { // Arrange - const storeData = { - order: { - payment: { - insuranceCoverage: { - coverageStatus: coverageStatuses.PENDING - } - }, - policy: { - isITAC: false - }, - currentDeductible: 321 - } - }; + const storeData = getDefaultState(); + storeData.order.payment.insuranceCoverage.coverageStatus = coverageStatuses.PENDING; + storeData.order.policy.isITAC = false; + storeData.order.currentDeductible = 321; + const { wrapper } = getMountedComponent(storeData); const amount = 123; diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index e6e09442..59e661f3 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -173,20 +173,7 @@ export default { availableVaps: Array }, data() { - const { currentDeductible } = useMainStore().order; - const { glassToReplace, isRepair } = useMainStore().damage; - const { supportingItems, glassParts, otherParts, vaps } = useMainStore().lineItems; - return { - isRepair, - glassToReplace: glassToReplace ?? [], - vapsInOrder: vaps ?? [], - baseServiceLineItems: [ - ...(supportingItems ?? []), - ...(glassParts ?? []), - ...(otherParts ?? []) - ], - deductible: currentDeductible, isExpanded: false, widget: { amountDue: 'AmountDueTextWidget', @@ -202,9 +189,20 @@ export default { }; }, computed: { + deductible() { + // Note: added as computed so it can be used in the template. + return useMainStore().order.currentDeductible; + }, showDeductibleCartItem() { return !useMainStore().isNoComp && !useMainStore().policy.isITAC; }, + baseServiceLineItems() { + return [ + ...(useMainStore().lineItems.supportingItems ?? []), + ...(useMainStore().lineItems.glassParts ?? []), + ...(useMainStore().lineItems.otherParts ?? []) + ]; + }, baseServicePrice() { return getPriceOfLineItems(this.baseServiceLineItems); }, @@ -219,7 +217,26 @@ export default { return basePrice + this.packagePrice; }, salesTax() { - return 0; // TODO + function sumTax(lineItems) { + return lineItems?.reduce((accumulator, lineItem) => accumulator + (lineItem.salesTax ?? 0), 0) ?? 0; + } + + let result = 0; + + if (useMainStore().payment.insuranceCoverage.isVerified + && (useMainStore().isITAC || useMainStore().isNoComp)) { + result += sumTax(this.baseServiceLineItems); + } else { + // deductible or non-verified still need to show tax for Recycle Fee or Mobile Fee + const recycleFeeLineItem = useMainStore().lineItems.supportingItems + ?.find((lineItem) => lineItem.partNumber === partNumberStrings.RECYCLE_FEE); + + result += recycleFeeLineItem != null ? recycleFeeLineItem.salesTax : 0; + } + + result += sumTax(useMainStore().lineItems.vaps ?? []); + + return result; }, amountDue() { return this.showAsPaid @@ -236,17 +253,17 @@ export default { }, servicePackageTier() { return getHighestFullySatisfiedTier( - this.glassToReplace, + useMainStore().damage.glassToReplace ?? [], this.availableLineItems, - this.isRepair, - this.vapsInOrder + useMainStore().damage.isRepair, + useMainStore().lineItems.vaps ?? [] ); }, servicePackageCartItems() { const packageContentTypes = getPackageContents( - this.glassToReplace, + useMainStore().damage.glassToReplace ?? [], this.availableLineItems, - this.isRepair, + useMainStore().damage.isRepair, this.servicePackageTier ) ?? []; const items = []; @@ -271,7 +288,7 @@ export default { return this.getCartItemForVapsPart(partTypeStrings.RAIN_DEFENSE); }, packagePrice() { - return getPriceOfLineItems(this.vapsInOrder); + return getPriceOfLineItems(useMainStore().lineItems.vaps ?? []); }, amountDueLabel() { return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT); @@ -355,7 +372,7 @@ export default { }, getCartItemForVapsPart(partType) { const label = this.getCmsContentForVapsType(partType); - const lineItems = this.vapsInOrder + const lineItems = useMainStore().lineItems.vaps ?.filter((vapsLineItem) => vapsLineItem.partType === partType); return this.getCartItem(label, lineItems); }, diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index a3f97d78..43c56607 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -115,6 +115,8 @@ export default { const resultMap = await settleAllPromises(promiseResultMap); + await useMainStore().setPriceAndSalesTaxForOrderLineItems(); + const unpricedVaps = [...resultMap.wipers, resultMap.rainDefense]; let hasBailedOut = false; diff --git a/src/store/index.js b/src/store/index.js index 5add699d..f2b17519 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -54,7 +54,7 @@ function getTimeSlotsAdditionalEventData( return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ProvisionalTriggers:${provisionalTriggers.join(',')}`; } -const getDefaultState = () => ({ +export const getDefaultState = () => ({ order: { vehicle: { policyVehicleId: null, @@ -1556,11 +1556,28 @@ export const useMainStore = defineStore({ this.order.lineItems.mobileFee = mobileFee; }, + async setPriceAndSalesTaxForOrderLineItems() { + const lineItemsToTax = [ + ...(this.order.lineItems.supportingItems ?? []), + ...(this.order.lineItems.glassParts ?? []), + ...(this.order.lineItems.otherParts ?? []), + ...(this.order.lineItems.vaps ?? []) + ]; + + if (this.order.lineItems.mobileFee != null) { + lineItemsToTax.push(this.order.lineItems.mobileFee); + } + + const pricedLineItemsToTax = await this.getPriceOrderItems(lineItemsToTax); + + this.taxOrderItemsAndSaveServerData(pricedLineItemsToTax); + }, + updateVehicle(vehicle) { // Assuming that the method caller pass all the properties. // otherwise need to check for undefined for every property. - this.vehicle.policyVehicleId = vehicle.policyVehicleId; - this.vehicle.carId = vehicle.carId; + this.order.vehicle.policyVehicleId = vehicle.policyVehicleId; + this.order.vehicle.carId = vehicle.carId; this.order.vehicle.category = vehicle.category; this.order.vehicle.year = vehicle.year; this.order.vehicle.make = vehicle.make;