Merge pull request #579 from Safelite/feature/richardson/SSR-1100

AfterPay Updates
This commit is contained in:
brich1212safe 2024-03-08 10:45:24 -05:00 committed by GitHub
commit 17375aadde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 578 additions and 186 deletions

View file

@ -116,6 +116,10 @@ const endpoints = Object.freeze({
url: `${ACCOUNT_BASE_URL}/`,
method: 'GET'
},
TaxOrderItems: {
url: `${PRICE_BASE_URL}/taxed-order-items`,
method: 'GET'
},
LogExperimentExposureIfAssigned: {
url: `${EXPERIMENTS_BASE_URL}/log-exposure`,
method: 'POST'

View file

@ -23,7 +23,10 @@ export default {
return this.genericVehicleImage;
}
const { imageUrl } = this.mainStore.order.vehicle;
const imageUrl = (this.mainStore.hasSubmittedOrder())
? this.mainStore.submittedOrder.vehicle.imageUrl
: this.mainStore.order.vehicle.imageUrl;
if (!imageUrl || imageUrl === 'NULL') {
return this.getUnmatchedVehicleIcon();
}

View file

@ -15,7 +15,7 @@ jest.mock('@/helpers/cms-content-helper', () => ({
fetchCmsContentForPage: jest.fn(),
processIfStatements: jest.fn()
}));
const wordingText = 'wording Text {custom:address}';
const wordingText = 'wording text {custom:address}';
const mockMixin = {
methods: {
@ -67,6 +67,55 @@ const initialStore = {
}
};
const sessionStorage = {
schedule: {
date: '2024-03-01',
startTime: '09:00',
endTime: '10:00',
jobMinMinutes: 60,
jobMaxMinutes: 90
},
serviceLocation: {
address: '123 Test Way',
address2: '#1',
city: 'Mesa',
state: 'AZ',
zipCode: '12345',
appointmentType: 'Inshop',
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
}
}
};
const sessionStorageMock = (() => {
let sessionStore = {};
return {
getItem(key) {
return sessionStore[key] || null;
},
setItem(key, value) {
sessionStore[key] = value.toString();
},
removeItem(key) {
delete sessionStore[key];
},
clear() {
sessionStore = {};
}
};
})();
Object.defineProperty(window, 'sessionStorage', {
value: sessionStorageMock
});
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({
router: {
@ -107,9 +156,16 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu
}
describe('OrderConfirmation.vue', () => {
beforeEach(() => {
window.sessionStorage.clear();
});
afterEach(() => {
window.sessionStorage.removeItem('submittedOrder');
});
describe('Rendering', () => {
test('Should render Site Header', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const { wrapper } = getMountedComponent(initialStore);
// Act
@ -120,6 +176,7 @@ describe('OrderConfirmation.vue', () => {
});
test('Should render Vehicle Banner', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const { wrapper } = getMountedComponent(initialStore);
// Act
@ -130,6 +187,7 @@ describe('OrderConfirmation.vue', () => {
});
test('If Advanced flow, should display Site Footer', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const testStore = {
order: {
schedule: {
@ -154,6 +212,7 @@ describe('OrderConfirmation.vue', () => {
});
test('If Essential flow, should not display Site Footer', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const { wrapper } = getMountedComponent(initialStore);
// Act
@ -166,6 +225,7 @@ describe('OrderConfirmation.vue', () => {
describe('Navigation', () => {
test('If Advanced flow, forward button action navigates to carrier URL', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const carrierReturnUrl = 'testURL';
const testStore = {
order: {
@ -193,6 +253,7 @@ describe('OrderConfirmation.vue', () => {
describe('Computed properties', () => {
test('appointmentDateFormatted should return date in expected format', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const { wrapper } = getMountedComponent(initialStore);
// Act
@ -203,19 +264,18 @@ describe('OrderConfirmation.vue', () => {
});
test('appointmentTimeFormatted should return Mobile time in expected format', () => {
// Arrange
const testStore = {
order: {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
appointmentType: 'Mobile'
}
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
appointmentType: 'Mobile'
}
};
const { wrapper } = getMountedComponent(testStore);
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentTimeFormatted;
@ -225,19 +285,26 @@ describe('OrderConfirmation.vue', () => {
});
test('appointmentTimeFormatted should return Drop Off time in expected format', () => {
// Arrange
const testStore = {
order: {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
appointmentType: 'Dropoff'
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
appointmentType: 'Dropoff',
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
}
}
};
const { wrapper } = getMountedComponent(testStore);
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentTimeFormatted;
@ -247,6 +314,7 @@ describe('OrderConfirmation.vue', () => {
});
test('appointmentTimeFormatted should return In Shop time in expected format', () => {
// Arrange
window.sessionStorage.setItem('submittedOrder', JSON.stringify(sessionStorage));
const { wrapper } = getMountedComponent(initialStore);
// Act
@ -257,111 +325,162 @@ describe('OrderConfirmation.vue', () => {
});
test('appointmentWordingText should return Mobile text in expected format', () => {
// Arrange
const testStore = {
order: {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
address: '123 Test Way',
address2: '#1',
city: 'Mesa',
state: 'AZ',
zipCode: '12345',
appointmentType: 'Mobile'
}
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
address: '123 Test Way',
address2: '#1',
city: 'Mesa',
state: 'AZ',
zipCode: '12345',
appointmentType: 'Mobile'
}
};
const { wrapper } = getMountedComponent(testStore);
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentWordingText;
// Assert
expect(testValue).toEqual('wording Text <br/>123 Test Way, #1,<br/> Mesa, AZ 12345<br/>');
expect(testValue).toEqual('wording text 123 Test Way, #1,<br/> Mesa, AZ 12345');
});
test('appointmentWordingText should return Drop Off text in expected format', () => {
// Arrange
const testStore = {
order: {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
appointmentType: 'Dropoff'
}
appointmentType: 'Dropoff'
}
};
const { wrapper } = getMountedComponent(testStore);
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentWordingText;
// Assert
expect(testValue).toEqual('wording Text <br/>123 Safelite Street,<br/> Mesa, AZ 12345<br/>');
expect(testValue).toEqual('wording text 123 Safelite Street,<br/> Mesa, AZ 12345');
});
test('appointmentWordingText should return In Shop text in expected format', () => {
// Arrange
const { wrapper } = getMountedComponent(initialStore);
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
appointmentType: 'Inshop'
}
};
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentWordingText;
// Assert
expect(testValue).toEqual('wording Text <br/>123 Safelite Street,<br/> Mesa, AZ 12345<br/>');
expect(testValue).toEqual('wording text 123 Safelite Street,<br/> Mesa, AZ 12345');
});
test('serviceLocationFullAddress should return text in expected format', () => {
// Arrange
const { wrapper } = getMountedComponent(initialStore);
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
address: '123 Test Way',
address2: '#1',
city: 'Mesa',
state: 'AZ',
zipCode: '12345',
appointmentType: 'Mobile'
}
};
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.serviceLocationFullAddress;
// Assert
expect(testValue).toEqual('<br/>123 Test Way, #1,<br/> Mesa, AZ 12345<br/>');
expect(testValue).toEqual('123 Test Way, #1,<br/> Mesa, AZ 12345');
});
test('providerFullAddress should return text in expected format', () => {
// Arrange
const { wrapper } = getMountedComponent(initialStore);
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
appointmentType: 'Inshop'
}
};
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.providerFullAddress;
// Assert
expect(testValue).toEqual('<br/>123 Safelite Street,<br/> Mesa, AZ 12345<br/>');
expect(testValue).toEqual('123 Safelite Street,<br/> Mesa, AZ 12345');
});
test('appointmentWordingText2 should return Mobile text in expected format', () => {
// Arrange
const testStore = {
order: {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
address: '123 Test Way',
address2: '#1',
city: 'Mesa',
state: 'AZ',
zipCode: '12345',
appointmentType: 'Mobile'
}
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
address: '123 Test Way',
address2: '#1',
city: 'Mesa',
state: 'AZ',
zipCode: '12345',
appointmentType: 'Mobile'
}
};
const { wrapper } = getMountedComponent(testStore);
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentWordingText2;
@ -371,27 +490,26 @@ describe('OrderConfirmation.vue', () => {
});
test('appointmentWordingText2 should return Drop Off and text in expected format', () => {
// Arrange
const testStore = {
order: {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
appointmentType: 'Dropoff'
}
appointmentType: 'Dropoff'
}
};
const { wrapper } = getMountedComponent(testStore);
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentWordingText2;
@ -402,7 +520,26 @@ describe('OrderConfirmation.vue', () => {
});
test('appointmentWordingText2 should return In Shop text in expected format', () => {
// Arrange
const { wrapper } = getMountedComponent(initialStore);
const testSessionStorage = {
schedule: {
date: '2019-01-01',
startTime: '09:00',
endTime: '10:00'
},
serviceLocation: {
provider: {
address: {
streetAddress: '123 Safelite Street',
city: 'Mesa',
state: 'AZ',
zipCode: '12345'
}
},
appointmentType: 'Inshop'
}
};
window.sessionStorage.setItem('submittedOrder', JSON.stringify(testSessionStorage));
const { wrapper } = getMountedComponent();
// Act
const testValue = wrapper.vm.appointmentWordingText2;

View file

@ -58,7 +58,9 @@ import settleAllPromises from '@/helpers/layout-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store';
import { get12HourTimeFormat, get12HourTimeMobileFormat, convertDateStringToDate,
import { get12HourTimeFormat,
get12HourTimeMobileFormat,
convertDateStringToDate,
getDisplayTextForDurationLength } from '@/helpers/date-helper.js';
import { toTitleCase } from '@/helpers/text-helper.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
@ -66,14 +68,15 @@ import { AppointmentTypeStrings } from '@/constants/schedule-constants';
export default {
name: 'order-confirmation',
components: {
// eslint-disable-next-line vue/no-reserved-component-names
Form,
siteHeader,
vehicleBanner,
siteFooter,
// eslint-disable-next-line vue/no-reserved-component-names
Form
siteFooter
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
useMainStore().createSubmittedOrder();
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
@ -90,7 +93,8 @@ export default {
},
setup() {
const mainStore = useMainStore();
return { mainStore };
const { submittedOrder } = mainStore;
return { mainStore, submittedOrder };
},
computed: {
carrierName() {
@ -106,16 +110,16 @@ export default {
return this.getCmsContent('OrderConfirmationContent', 'Image');
},
appointmentType() {
return this.mainStore.order.serviceLocation.appointmentType;
return this.submittedOrder.serviceLocation.appointmentType;
},
appointmentDate() {
return this.mainStore.order.schedule.date;
return this.submittedOrder.schedule.date;
},
appointmentStartTime() {
return this.mainStore.order.schedule.startTime;
return this.submittedOrder.schedule.startTime;
},
appointmentEndTime() {
return this.mainStore.order.schedule.endTime;
return this.submittedOrder.schedule.endTime;
},
appointmentDateFormatted() {
// This conversion ensures we don't get get GMT induced date changes
@ -144,39 +148,39 @@ export default {
return this.getBodyText2FromCms('DropOffAndInShopWordingWidget');
},
serviceLocationAddress() {
return this.mainStore.order.serviceLocation.address;
return this.submittedOrder.serviceLocation.address;
},
serviceLocationAddress2() {
return this.mainStore.order.serviceLocation.address2;
return this.submittedOrder.serviceLocation.address2;
},
serviceLocationCity() {
return this.mainStore.order.serviceLocation.city;
return this.submittedOrder.serviceLocation.city;
},
serviceLocationState() {
return this.mainStore.order.serviceLocation.state;
return this.submittedOrder.serviceLocation.state;
},
serviceLocationZipCode() {
return this.mainStore.order.serviceLocation.zipCode;
return this.submittedOrder.serviceLocation.zipCode;
},
serviceLocationFullAddress() {
// eslint-disable-next-line max-len
return `<br/>${this.serviceLocationAddress}, ${this.serviceLocationAddress2 ? `${this.serviceLocationAddress2},` : ''}<br/> ${this.serviceLocationCity}, ${this.serviceLocationState} ${this.serviceLocationZipCode}<br/>`;
return `${this.serviceLocationAddress}, ${this.serviceLocationAddress2 ? `${this.serviceLocationAddress2},` : ''}<br/> ${this.serviceLocationCity}, ${this.serviceLocationState} ${this.serviceLocationZipCode}`;
},
providerAddress() {
return toTitleCase(this.mainStore.order.serviceLocation.provider.address.streetAddress);
return toTitleCase(this.submittedOrder.serviceLocation.provider.address.streetAddress);
},
providerCity() {
return toTitleCase(this.mainStore.order.serviceLocation.provider.address.city);
return toTitleCase(this.submittedOrder.serviceLocation.provider.address.city);
},
providerState() {
return this.mainStore.order.serviceLocation.provider.address.state;
return this.submittedOrder.serviceLocation.provider.address.state;
},
providerZipCode() {
return this.mainStore.order.serviceLocation.provider.address.zipCode;
return this.submittedOrder.serviceLocation.provider.address.zipCode;
},
providerFullAddress() {
// eslint-disable-next-line max-len
return `<br/>${this.providerAddress},<br/> ${this.providerCity}, ${this.providerState} ${this.providerZipCode}<br/>`;
return `${this.providerAddress},<br/> ${this.providerCity}, ${this.providerState} ${this.providerZipCode}`;
},
appointmentWordingText() {
switch (this.appointmentType) {
@ -217,18 +221,19 @@ export default {
}
},
mobileAppointment() {
return this.mainStore.isMobileAppointment;
return this.submittedOrder.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE
|| this.submittedOrder.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
},
inShopAppointment() {
return this.mainStore.isInShopAppointment;
return this.submittedOrder.serviceLocation.appointmentType === AppointmentTypeStrings.IN_SHOP;
},
dropOffAppointment() {
return this.mainStore.isDropOffAppointment;
return this.submittedOrder.serviceLocation.appointmentType === AppointmentTypeStrings.DROP_OFF;
},
inShopAppointmentDuration() {
const inshopDurationTime = getDisplayTextForDurationLength(
this.mainStore.order.schedule.jobMinMinutes,
this.mainStore.order.schedule.jobMaxMinutes
this.submittedOrder.schedule.jobMinMinutes,
this.submittedOrder.schedule.jobMaxMinutes
);
return inshopDurationTime;
}
@ -310,6 +315,9 @@ $page-side-padding: 1.5rem;
}
.appointment-text {
:deep(p) {
margin: 0;
}
:deep(strong) {
font-weight: $font-weight-bold;
color: $black;

View file

@ -33,7 +33,6 @@
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
:isStackedVertically="true"
@backClicked="navigateBack"
@ForwardClicked="forwardButtonAction" />

View file

@ -264,44 +264,6 @@ describe('payment-page.vue', () => {
});
});
describe('beforeRouteEnter', () => {
test('Properly initializes fields and kicks off hop', async () => {
// Arrange
const vmMock = {
setCmsContent: jest.fn(),
$refs: {
cart: {
cartItems: []
}
},
getPiaLineItems: jest.fn(),
fetchSignatureInfo: jest.fn(),
setIFrameListener: jest.fn(),
$nextTick: (f) => {
f();
}
};
const nextF = (f) => {
f(vmMock);
};
// Act
await payment.beforeRouteEnter.call(
vmMock,
{ query: { issPage: 'payment-page' } },
undefined,
nextF
);
// Assert
expect(vmMock.setCmsContent).toBeCalled();
expect(vmMock.fetchSignatureInfo).toBeCalled();
expect(vmMock.setIFrameListener).toBeCalled();
});
});
describe('payment type mapping', () => {
describe('getPaymentType', () => {
test('Maps AFTERPAY -> hopPaymentMethods.AFTERPAY', () => {

View file

@ -1,9 +1,5 @@
<template>
<Form
ref="theForm"
v-slot="{ meta }"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
<Form>
<div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget" />
@ -21,12 +17,12 @@
</div>
<siteFooter
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="true"
:isForwardButtonHidden="true"
@ForwardClicked="forwardButtonAction"
@backClicked="backButtonAction" />
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="true"
:isForwardButtonHidden="true"
@ForwardClicked="forwardButtonAction"
@backClicked="backButtonAction" />
</div>
</div>
@ -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;

View file

@ -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' });

View file

@ -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({

View file

@ -654,7 +654,7 @@ const routingTable = () => [
},
{
scenario: navigationScenarios.PAY_IN_ADVANCE_SUCCESS,
destinationIssPageValue: issPageValues.CONFIRMATION
destinationIssPageValue: issPageValues.ORDER_CONFIRMATION
}
]
},

View file

@ -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}`;