@@ -21,12 +17,12 @@
+ ref="siteFooter"
+ cmsWidgetName="SiteFooterWidget"
+ :isForwardActionDisabled="true"
+ :isForwardButtonHidden="true"
+ @ForwardClicked="forwardButtonAction"
+ @backClicked="backButtonAction" />
@@ -367,6 +363,7 @@ 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 { useMainStore } from '@/store';
import iframeResize from 'iframe-resizer/js/iframeResizer.js';
@@ -402,6 +399,9 @@ export default {
const paymentSignaturePromise = await useMainStore().getPaymentSignature();
+ const wipersPromise = useMainStore().getWipers();
+ const rainDefensePromise = useMainStore().getRainDefense();
+
// Settle promises and get results
const promiseResultMap = [
{
@@ -411,16 +411,50 @@ export default {
{
resultKey: 'paymentSignature',
promise: paymentSignaturePromise
+ },
+ {
+ resultKey: 'wipers',
+ promise: wipersPromise
+ },
+ {
+ resultKey: 'rainDefense',
+ promise: rainDefensePromise
}
];
// use resultMap to populate layout content.
const resultMap = await settleAllPromises(promiseResultMap);
+ const lineItemsFromStore = useMainStore().lineItems;
+ const glassParts = lineItemsFromStore.glassParts ?? [];
+ const supportingItems = lineItemsFromStore.supportingItems ?? [];
+
+ const lineItemsToTax = [
+ resultMap.rainDefense,
+ ...supportingItems,
+ ...resultMap.wipers,
+ ...glassParts
+ ];
+ const availableVaps = [resultMap.rainDefense, ...resultMap.wipers];
+ const pricedLineItemsToTax = await useMainStore().priceOrderItemsAndSaveServerData(lineItemsToTax);
+ const taxedLineItems = await useMainStore().taxOrderItemsAndSaveServerData(pricedLineItemsToTax);
+
+ // Match all line items to the line items as they are in the store
+ // and rebuild the original structure.
+ const taxLineItems = useMainStore().mapTaxedLineItemsToStoreFormat(taxedLineItems, lineItemsFromStore);
+ const taxedVaps = useMainStore().mapTaxedLineItemsToStoreFormat(taxedLineItems, availableVaps);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
+ vm.setData(taxedVaps, taxLineItems);
vm.$nextTick(() => {
+ if (vm.$refs.cart) {
+ const { cartItems } = vm.$refs.cart;
+ vm.getPayInAdvanceLineItems(cartItems);
+ } else {
+ vm.getMockPiaLineItems();
+ }
+
vm.fetchSignatureInfo(resultMap.paymentSignature);
vm.setIFrameListener();
});
@@ -460,11 +494,11 @@ export default {
computed: {
payInAdvanceResponseUrl() {
const { protocol, host } = window.location;
- return `${protocol}//${host}/?issPage=${issPageValues.PAYMENT_RETURN}&src=iss-nextgen`;
+ return `${protocol}//${host}/?issPage=${issPageValues.PAYMENT_RETURN}&src=concept-funnel`;
},
payInAdvanceCancelUrl() {
const { protocol, host } = window.location;
- return `${protocol}//${host}/?issPage=${issPageValues.PAYMENT_METHOD}&src=iss-nextgen`;
+ return `${protocol}//${host}/?issPage=${issPageValues.PAYMENT_METHOD}&src=concept-funnel`;
},
dynamicCSSUrl() {
const { protocol, hostname, port } = window.location;
@@ -553,6 +587,10 @@ export default {
&& paymentMethodReqs
);
},
+ setData(taxedVaps, taxLineItems) {
+ this.availableVaps = taxedVaps;
+ this.lineItems = taxLineItems;
+ },
getWorkOrderNumber() {
const { workOrderNumber } = useMainStore().order;
if (workOrderNumber) {
@@ -597,11 +635,31 @@ export default {
return useMainStore().payment.payInAdvanceType;
}
},
+ getPayInAdvanceLineItems(cartItems) {
+ const { glassParts } = useMainStore().order.lineItems;
+ const lineItems = (glassParts === null) ? ['Labor|0|1', 'Repair supplies|0|1'] : ['Parts and labor|0|1'];
+
+ cartItems.forEach((item) => {
+ if (item.name !== null && item.category !== 'promos') {
+ lineItems.push(`${item.name}|${(item.salesTax + item.subTotal).toFixed(2)}|1`);
+ }
+ });
+
+ this.payInAdvanceLineItems = lineItems.join('||');
+ },
+ getMockPiaLineItems() {
+ let lineItems = [];
+ lineItems = ['Parts and labor|0|1'];
+ lineItems.push('New wiper blades|75.22|1');
+ lineItems.push('Recycling|37.60|1');
+
+ this.payInAdvanceLineItems = lineItems.join('||');
+ },
getAmountDue() {
- return 0;
+ return baseMixin.methods.getAmountDue(useMainStore().lineItems);
},
getDisplayAmountDue() {
- return 0;
+ return baseMixin.methods.getDisplayAmountDue(useMainStore().lineItems);
},
fetchSignatureInfo(signatureInfo) {
this.authToken = signatureInfo.token;
diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js
index 957cc6a1..0cbeec5b 100644
--- a/src/mixins/base-mixin.js
+++ b/src/mixins/base-mixin.js
@@ -36,6 +36,58 @@ 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) {
+ 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('page-container-grouped-styles')[0];
container.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
diff --git a/src/router/index.js b/src/router/index.js
index fda08794..8b1e4823 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -46,6 +46,13 @@ const routes = [
await runExperiments(issPageToUse); // fmg has this further down
}
+ // Intercept all navigation if a submitted order exists in storage
+ if (useMainStore().hasSubmittedOrder()) {
+ if (to.query.issPage !== issPageValues.ENTRY_PAGE) {
+ return await GoToOrderConfirmationPage(next);
+ }
+ }
+
// If the saved session has timed out, clear the session, execute 404 logic.
if (getISSCookie() !== null && !isSavedSessionStillActive()) {
// await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
@@ -127,8 +134,16 @@ router.beforeEach(async (to, from) => {
showIssLoadingModal(true);
}
+ const toQueryPage = to.query?.issPage;
+ const notToPayInAdvanceReturn = toQueryPage !== issPageValues.PAYMENT_RETURN;
const isInIframe = fromQueryPage === issPageValues.PAYMENT_PAGE;
- if (isInIframe) {
+
+ // isFromPaymentPageToOrderConfirmation workaround for navigating from an iframe but
+ // isInIframe evaluates to false for some reason when navigating from payment to confirmation
+ const isFromPaymentPageToOrderConfirmation =
+ fromQueryPage === issPageValues.PAYMENT_PAGE && toQueryPage === issPageValues.ORDER_CONFIRMATION;
+
+ if ((isInIframe && notToPayInAdvanceReturn) || isFromPaymentPageToOrderConfirmation) {
// need to set window.top.location.href directly when navigating out of an iframe
// especially when navigating with browser buttons
const newUrl = `${window.top.location.origin}${to.href}`;
@@ -347,6 +362,20 @@ async function GoToAccessIsDenied(next) {
});
}
+async function GoToOrderConfirmationPage(next) {
+ const nextPageName = issPageValues.ORDER_CONFIRMATION;
+ router.addRoute({
+ path: '/',
+ name: nextPageName,
+ component: lazyLoadComponent(nextPageName)
+ });
+
+ next({
+ name: nextPageName,
+ query: { issPage: nextPageName }
+ });
+}
+
async function GoToStartOn404(next, msgCopy = null, msgHeadline = null) {
const errorPageName = issPageValues.WELCOME_PAGE;
router.addRoute({
diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js
index 99582af8..0a7d4ab7 100644
--- a/src/router/router-constants/routing-table.js
+++ b/src/router/router-constants/routing-table.js
@@ -654,7 +654,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.PAY_IN_ADVANCE_SUCCESS,
- destinationIssPageValue: issPageValues.CONFIRMATION
+ destinationIssPageValue: issPageValues.ORDER_CONFIRMATION
}
]
},
diff --git a/src/store/index.js b/src/store/index.js
index c06022e0..3818d6ac 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -12,6 +12,7 @@ import issPageValues from '@/router/router-constants/issPage-values';
import damageLocationsSelected from '@/constants/damage-locations-selected';
import coverageStatuses from '@/constants/coverage-statuses';
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from '@/constants/schedule-constants';
+import { deepClone } from '@/helpers/object-helper';
import { convertDateStringToDate, getDateDifferenceInDays, militaryToTwelveHourTime } from '@/helpers/date-helper';
import { paymentMethods } from '@/constants/payment-method-constants';
import webStorageConstants from '@/constants/web-storage-constants';
@@ -371,7 +372,8 @@ export const useMainStore = defineStore({
}),
experimentSettings: (state) => state.applicationUser.experiments
.map((x) => x.settings)
- .reduce((r, c) => Object.assign(r, c), {}) ?? {}
+ .reduce((r, c) => Object.assign(r, c), {}) ?? {},
+ submittedOrder: () => JSON.parse(window.sessionStorage.getItem('submittedOrder'))
},
actions:
{
@@ -900,10 +902,8 @@ export const useMainStore = defineStore({
},
async getWipers() {
const { carId } = this.order.vehicle;
- // WARNING
- // TODO: this is temp test code until serviceLocation is complete.
- // const serviceZipCode = this.order.serviceLocation.zipCode;
- const serviceZipCode = '44902';
+ const serviceZipCode = this.order.serviceLocation.zipCode;
+
return globalMethods
.callHttpClient({
method: endpoints.GetWipers.method,
@@ -1047,7 +1047,68 @@ export const useMainStore = defineStore({
endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&${lineItems}&${glassPieces}`
});
},
+ mapTaxedLineItemsToStoreFormat(availableLineItems, storeLineItems) {
+ // clone the lineItems array because what we're passing in is referencing the store directly
+ const lineItems = deepClone(storeLineItems);
+ // eslint-disable-next-line no-restricted-syntax, prefer-const
+ for (let [category, lineItemsInCategory] of Object.entries(lineItems)) {
+ lineItemsInCategory = lineItemsInCategory ?? [];
+
+ if (category === 'supportingItems') {
+ // if the category is supporting items we need to filter out the items that aren't repair chips
+ const nonRepairChipSupportItemsLineItems = lineItemsInCategory.filter((lineItem) => lineItem.partNumber !== 'WSREPAIR');
+
+ /*
+ Because repair chips all have the same part number but different prices based on the quantity,
+ we have to sort the store line items and available line items by descending labor amount in order to
+ map the tax correctly to each repair chip
+ */
+ // get the supporting items that ARE repair chips and sort them by descending labor amount
+ let repairChipLineItems = lineItemsInCategory.filter((lineItem) => lineItem.partNumber === 'WSREPAIR');
+ repairChipLineItems = repairChipLineItems.sort((a, b) => parseFloat(b.laborAmount) - parseFloat(a.laborAmount));
+
+ // get the supporting items from the available line items (taxed) that ARE repair chips and sort them by descending labor amount
+ let availableRepairChipLineItems = availableLineItems.filter((lineItem) => lineItem.partNumber === 'WSREPAIR');
+ availableRepairChipLineItems = availableRepairChipLineItems.sort((a, b) => parseFloat(b.laborAmount) - parseFloat(a.laborAmount));
+
+ // go through each one of those mapping the taxes to the correct chip
+ for (let i = 0; i < availableRepairChipLineItems.length; i++) {
+ repairChipLineItems[i].salesTax = availableRepairChipLineItems[i].salesTax;
+ }
+
+ // then we map the non-repair chip items based on part number
+ for (
+ let lineItemIndex = 0;
+ lineItemIndex < nonRepairChipSupportItemsLineItems.length;
+ lineItemIndex++
+ ) {
+ const availableLineItem = availableLineItems.find((ali) =>
+ ali.partNumber === nonRepairChipSupportItemsLineItems[lineItemIndex].partNumber);
+ if (availableLineItem) {
+ nonRepairChipSupportItemsLineItems[lineItemIndex].salesTax =
+ availableLineItem.salesTax;
+ }
+ }
+
+ // finally we splice the two arrays back into one
+ lineItemsInCategory = nonRepairChipSupportItemsLineItems.concat(repairChipLineItems);
+ } else {
+ for (
+ let lineItemIndex = 0;
+ lineItemIndex < lineItemsInCategory.length;
+ lineItemIndex++
+ ) {
+ const availableLineItem = availableLineItems.find((ali) => ali.partNumber === lineItemsInCategory[lineItemIndex].partNumber);
+ if (availableLineItem) {
+ lineItemsInCategory[lineItemIndex].salesTax = availableLineItem.salesTax;
+ }
+ }
+ }
+ }
+
+ return lineItems;
+ },
lookupVehicleByVin(vin) {
return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByVin.method,
@@ -1791,6 +1852,66 @@ export const useMainStore = defineStore({
// context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
return addPricesToLineItems(availableLineItems, response.data.lineItems);
},
+ // Tax order actions
+ async taxOrderItemsAndSaveServerData(pricedLineItems) {
+ const { order } = this;
+ const { serviceLocation } = order;
+ const billToAccountNumber = this.issConfig.parentAccountNumber.toString(); // payment
+ const { providerNumber } = serviceLocation.provider;
+ const { appointmentType } = serviceLocation;
+ const serviceLocationCity = serviceLocation.city;
+ 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'
+ );
+
+ let queryString = '';
+ if (appointmentType === 'Mobile') {
+ queryString =
+ `ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}`
+ + `&BillToAccountNumber=${billToAccountNumber}`
+ + `&ProviderNumber=${providerNumber}`
+ + `&AppointmentType=${appointmentType}`
+ + `&ServiceLocation.City=${serviceLocationCity}`
+ + `&ServiceLocation.State=${serviceLocationState}`
+ + `&ServiceLocation.ZipCode=${serviceLocationZipCode}`
+ + `&${pricedLineItemsFormattedForRequest}`;
+ } else {
+ queryString =
+ `ParentAccountNumber=${applicationConfig.CASH_PARENT_ACCOUNT_NUMBER}`
+ + `&BillToAccountNumber=${billToAccountNumber}`
+ + `&ProviderNumber=${providerNumber}`
+ + `&AppointmentType=${appointmentType}`
+ + `&${pricedLineItemsFormattedForRequest}`;
+ }
+
+ const lineItemServerData = this.order.lineItems.serverData;
+ if (lineItemServerData) {
+ queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
+ }
+
+ const retPricedLineItems = await globalMethods.callHttpClient({
+ method: endpoints.TaxOrderItems.method,
+ endpoint: `${endpoints.TaxOrderItems.url}?${queryString}`
+ }).then((response) => {
+ this.order.lineItems.serverData = response.data.serverData;
+ return addTaxesToPricedLineItems(pricedLineItems, response.data.taxedLineItems);
+ });
+
+ return retPricedLineItems;
+ },
saveProviderPreferenceData(data) {
this.updatePageData({ page: issPageValues.PROVIDER_PREFERENCE, data });
},
@@ -2156,6 +2277,7 @@ export const useMainStore = defineStore({
this.resetDamageState();
this.resetInsurance();
this.resetBailout();
+ this.resetSubmittedOrder();
},
savePaymentMethodChoice(paymentMethod) {
const isPayInAdvance = paymentMethod !== paymentMethods.PAY_AT_TIME_OF_SERVICE;
@@ -2180,6 +2302,7 @@ export const useMainStore = defineStore({
}
const submittedOrder = this.order;
const { experiments } = this.applicationUser;
+ const { issConfig } = this;
// set to local storage
window.sessionStorage.setItem(webStorageConstants.SUBMITTED_ORDER, JSON.stringify(submittedOrder));
@@ -2187,6 +2310,8 @@ export const useMainStore = defineStore({
// clear vuex
this.resetState();
+ // restore issConfig
+ this.issConfig = issConfig;
// restore user's experiments
this.applicationUser.experiments = experiments;
},
@@ -2295,6 +2420,21 @@ function addPricesToLineItems(lineItems, pricingLineItems) {
return lineItems;
}
+function addTaxesToPricedLineItems(pricedLineItems, taxingLineItems = []) {
+ pricedLineItems.forEach((pricedLineItem) => {
+ const lineItemIndex = taxingLineItems.findIndex((taxingLineItem) => taxingLineItem.partNumber === pricedLineItem.partNumber);
+
+ if (pricedLineItem.childParts) {
+ addTaxesToPricedLineItems(pricedLineItem.childParts, taxingLineItems);
+ }
+
+ const taxedLineItem = taxingLineItems.splice(lineItemIndex, 1)[0];
+ pricedLineItem.salesTax = taxedLineItem?.salesTax ?? 0;
+ });
+
+ return pricedLineItems;
+}
+
function getLineItemQueryStringForPricing(lineItems) {
return lineItems.map((lineItem, index) => {
let queryStringSnippet = `&LineItems[${index}].partNumber=${lineItem.partNumber}`;