fix merge conflicts

This commit is contained in:
Katie Kroell 2024-03-08 15:12:03 -05:00
commit 7970013a17
19 changed files with 1841 additions and 1043 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

@ -0,0 +1,12 @@
function getPriceOfLineItem(lineItem) {
return (lineItem?.kitPrice ?? 0)
+ (lineItem?.laborAmount ?? 0)
+ (lineItem?.sellingPrice ?? 0);
}
export default function getPriceOfLineItems(lineItems) {
return lineItems.reduce(
(accumulator, lineItem) => accumulator + getPriceOfLineItem(lineItem),
0
);
}

View file

@ -0,0 +1,56 @@
import getPriceOfLineItems from "@/helpers/price-calculator.js";
describe('getPriceOfLineItems', () => {
test('Returns zero when no line items', () => {
// Arrange
const lineItems = [];
// Act
const result = getPriceOfLineItems(lineItems);
// 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);
// 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);
// Assert
expect(result).toBe(expected);
});
});

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

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`coverageStatement.vue-working returns the initial data 1`] = `
Object {
"baseServiceLineItems": Array [],
"currencyFormatter": NumberFormat {},
"deductibleText": "Your deductible is",
"isNoComp": false,
"isRepair": true,
"loadingText": Array [
"Connecting to your insurance company",
"Nearly there",
"Finishing up",
],
"policyLookupSuccessful": true,
"rules": Object {
"selectionRequired": "option-required",
},
"selectedProvider": "",
"supportingItems": null,
"widget": Object {
"explanatoryText": "ExplanatoryTextWidget",
"nextStep": "NextStepsWidget",
"serviceProviderQuestion": "ServiceProviderQuestion",
"subheader": "SiteSubHeaderWidget",
"verifiedItacAlert": "VerifiedITACAlert",
},
}
`;

File diff suppressed because it is too large Load diff

View file

@ -35,26 +35,26 @@
<div
v-if="verifiedDeductible"
class="d-flex justify-content-center cost">
{{ formattedDeductible }}
{{ deductibleForDisplay }}
</div>
<div
v-if="displayQuote"
v-if="isQuoteDisplayed"
class="d-flex justify-content-center cost mb-0">
{{ formattedServicePrice }}
{{ servicePriceForDisplay }}
</div>
<div
v-if="verifiedITAC"
class="d-flex justify-content-center mb-4 deductible-text">
{{ deductibleText }}&nbsp;
<span class="text-success fw-bold">{{ formattedDeductible }}</span>
<span class="text-success fw-bold">{{ deductibleForDisplay }}</span>
</div>
<alert
v-if="verifiedITAC"
ref="verifiedITACAlert"
class="mb-5"
cmsWidgetName="VerifiedITACAlert"
:manualHeadline="verifiedITACAlertHeader"
:manualCopy="verifiedITACAlertBody"
:manualHeadline="verifiedItacAlertHeader"
:manualCopy="verifiedItacAlertBody"
alertClass="alert-success"
:isDismissible="false">
</alert>
@ -67,18 +67,18 @@
v-html="nextStepsBody">
</div>
<buttonQuestion
v-if="displayQuote"
v-if="isQuoteDisplayed"
v-model="selectedProvider"
cmsWidgetName="ServiceProviderQuestion"
:questionText="questionText"
:answers="answersFromCms"
:questionText="serviceProviderQuestionText"
:answers="serviceProviderQuestionAnswers"
groupName="ServiceProviderQuestionOption"
buttonTypeString="listButton"
isRequired
:validationRules="rules.selectionRequired">
</buttonQuestion>
<text-block
v-if="displayQuote"
v-if="isQuoteDisplayed"
cmsWidgetName="DisclaimerWidget"
typeStyle="caption" />
</div>
@ -88,7 +88,7 @@
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@backClicked="navigateBackByVehicleQuestions"
@forwardClicked="forwardButtonAction" />
@forwardClicked="navigateForward" />
</div>
</div>
</div>
@ -126,10 +126,16 @@ import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin.js';
import globalRules from '@/constants/global-rules.js';
import baseFormMixin from '@/mixins/base-form-mixin.js';
import navigationScenarios from '@/router/router-constants/navigation-scenarios.js';
import routerParams from '@/router/router-constants/router-params';
import issPageValues from '@/router/router-constants/issPage-values';
import bailoutCode from '@/constants/bailoutCode';
import bailoutMessage from '@/constants/bailoutMessage';
import widgetFields from '@/constants/cms-widget-fields.js';
import getPriceOfLineItems from '@/helpers/price-calculator.js';
const SAFELITE_PROVIDER = 'Safelite';
function setBailout(currentRoute, message) {
useMainStore().setBailout(currentRoute, message);
}
export default {
name: 'coverage-statement',
@ -164,8 +170,8 @@ export default {
];
const resultMap = await settleAllPromises(promiseResultMap);
const clonedGlassParts = useMainStore().order.lineItems.glassParts
? JSON.parse(JSON.stringify(useMainStore().order.lineItems.glassParts))
const clonedGlassParts = useMainStore().lineItems.glassParts
? JSON.parse(JSON.stringify(useMainStore().lineItems.glassParts))
: [];
const availableLineItems = [
...(resultMap.supportingItems ?? []),
@ -174,11 +180,14 @@ export default {
let hasBailedOut = false;
let pricingResults = [];
if (useMainStore().order.policy.policyLookupSuccessful && useMainStore().order.vehicle.policyVehicleId >= 0) {
if (useMainStore().policy.policyLookupSuccessful && useMainStore().vehicle.policyVehicleId >= 0) {
await useMainStore().getFinalDeductible();
pricingResults = await useMainStore().getPriceOrderItems(availableLineItems)
.catch((err) => {
useMainStore().setBailout(to, bailoutMessage.pricingResponseError(availableLineItems.map((li) => li.partNumber), { code: err.code, message: err.message, data: err.data }));
setBailout(to, bailoutMessage.pricingResponseError(
availableLineItems.map((li) => li.partNumber),
{ code: err.code, message: err.message, data: err.data }
));
hasBailedOut = true;
next(`/?issPage=${issPageValues.BAILOUT_PAGE}`);
});
@ -190,22 +199,27 @@ export default {
vm.setCmsContent(resultMap.cmsContent);
vm.setSupportingItems(resultMap.supportingItems);
// eslint-disable-next-line no-param-reassign
vm.availableLineItems = pricingResults;
vm.setBaseServiceLineItems(pricingResults);
vm.$refs.loadingModal.showModal();
vm.initializeComponent(availableLineItems);
vm.initializeComponent();
if (!vm.unverified) {
useMainStore().disableKeyFields();
}
});
}
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
const { isRepair } = useMainStore().damage;
const { policyLookupSuccessful, noCoverage } = useMainStore().policy;
return {
availableLineItems: [],
currencyFormatter: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}),
isRepair,
policyLookupSuccessful,
isNoComp: noCoverage ?? false,
baseServiceLineItems: [],
selectedProvider: '',
deductibleText: 'Your deductible is',
// TODO update when design team gives appropriate text
@ -217,84 +231,90 @@ export default {
rules: {
selectionRequired: globalRules.OPTION_REQUIRED
},
supportingItems: null
supportingItems: null,
widget: {
subheader: 'SiteSubHeaderWidget',
verifiedItacAlert: 'VerifiedITACAlert',
explanatoryText: 'ExplanatoryTextWidget',
nextStep: 'NextStepsWidget',
serviceProviderQuestion: 'ServiceProviderQuestion'
}
};
},
computed: {
verifiedITACAlertHeader() {
return this.getCmsContent(
'VerifiedITACAlert',
'HeadlineText'
coverageStatementSubHeader() {
return this.getTextFromCmsWithCustomIfStatements(
this.widget.subheader,
widgetFields.SUB_HEADER_WIDGET.SUB_HEADER_TEXT
);
},
verifiedITACAlertBody() {
verifiedItacAlertHeader() {
return this.getCmsContent(
'VerifiedITACAlert',
'BodyText'
)?.replaceAll('{custom:costSavings}', this.costSavings);
this.widget.verifiedItacAlert,
widgetFields.ALERT_WIDGET.HEADLINE_TEXT
);
},
coverageStatementSubHeader() {
return this.getSubheaderTextFromCms('SiteSubHeaderWidget');
verifiedItacAlertBody() {
return this.getCmsContent(
this.widget.verifiedItacAlert,
widgetFields.ALERT_WIDGET.BODY_TEXT
)?.replaceAll('{custom:costSavings}', this.itacCostSavingsForDisplay);
},
secondaryText() {
return this.getSecondaryTextFromCms('SiteSubHeaderWidget');
return this.getTextFromCmsWithCustomIfStatements(
this.widget.subheader,
widgetFields.SUB_HEADER_WIDGET.SECONDARY_TEXT
);
},
explanatoryText() {
return this.getExplantoryTextFromCms('ExplanatoryTextWidget');
return this.getTextFromCmsWithCustomIfStatements(
this.widget.explanatoryText,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
);
},
nextStepsHeader() {
return this.getHeaderTextFromCms('NextStepsWidget');
return this.getTextFromCmsWithCustomIfStatements(
this.widget.nextStep,
widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT
);
},
nextStepsBody() {
return this.getBodyTextFromCms('NextStepsWidget')?.replaceAll('{custom:damage}', this.damageText);
},
continueWithSchedulingBodyText() {
return this.getCmsContent('continueWithSchedulingCopy', 'BodyText');
},
unverifiedADASNextStepsBodyText() {
return this.getCmsContent('UnverifiedADASNextStepsWidget', 'BodyText')?.replaceAll('{custom:damage}', this.damageText);
},
unverifiedNonADASNextStepsBodyText() {
return this.getCmsContent('UnverifiedNonADASNextStepsWidget', 'BodyText')?.replaceAll('{custom:damage}', this.damageText);
},
unverifiedNonADASRepairBodyText() {
return this.getCmsContent('UnverifiedNonADASRepairWidget', 'BodyText');
return this.getTextFromCmsWithCustomIfStatements(
this.widget.nextStep,
widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT
)?.replaceAll('{custom:damage}', this.damageText);
},
damageText() {
const damageString = getDamageString();
return damageString === 'match' ? '' : damageString;
},
vehicleDeductible() {
const deductible = useMainStore().order.currentDeductible;
return deductible;
deductibleValue() {
return useMainStore().order.currentDeductible;
},
formattedDeductible() {
return this.getDeductibleString(this.vehicleDeductible);
},
isDeductibleZero() {
return this.vehicleDeductible === 0;
},
policyLookupSuccessful() {
return useMainStore().order.policy.policyLookupSuccessful;
deductibleForDisplay() {
return this.getFormattedAmount(this.deductibleValue);
},
registerClaimSuccessful() {
return useMainStore().payment.insuranceCoverage.isVerified;
},
verifiedNoComp() {
return this.policyLookupSuccessful ? useMainStore().order.policy.noCoverage : false;
return this.policyLookupSuccessful && this.isNoComp;
},
verifiedITAC() {
return this.policyLookupSuccessful
&& !this.verifiedNoComp
&& this.vehicleDeductible > this.totalServicePrice;
&& !this.isNoComp
&& this.deductibleValue > this.totalServicePrice;
},
coveredAndServicePriceAboveOrEqualDeductible() {
return !this.verifiedNoComp && this.totalServicePrice >= this.vehicleDeductible;
return !this.verifiedNoComp && this.totalServicePrice >= this.deductibleValue;
},
verifiedDeductible() {
return useMainStore().isClaimRegistrationRequired
? this.registerClaimSuccessful && this.coveredAndServicePriceAboveOrEqualDeductible && this.vehicleDeductible !== null
: this.policyLookupSuccessful && this.coveredAndServicePriceAboveOrEqualDeductible;
? this.registerClaimSuccessful
&& this.coveredAndServicePriceAboveOrEqualDeductible
&& this.deductibleValue !== null
: this.policyLookupSuccessful
&& this.coveredAndServicePriceAboveOrEqualDeductible;
},
unverified() {
return !this.verifiedDeductible && !this.verifiedITAC && !this.verifiedNoComp;
@ -303,41 +323,46 @@ export default {
const parts = useMainStore().order.lineItems.glassParts;
return parts !== null && !!parts.find((part) => part.requiresRecalibration);
},
isRepair() {
return useMainStore().order.damage.isRepair;
},
totalServicePrice() {
let total = 0;
this.availableLineItems.forEach((lineItem) => {
total += this.getTotalLineItemPrice(lineItem);
});
return total;
return getPriceOfLineItems(this.baseServiceLineItems);
},
formattedServicePrice() {
return this.getServicePriceString(this.totalServicePrice);
servicePriceForDisplay() {
return this.getFormattedAmount(this.totalServicePrice);
},
costSavings() {
const savings = this.getITACCostSavings(this.vehicleDeductible, this.totalServicePrice);
const formattedSavings = parseFloat(savings).toFixed(2);
return `$${formattedSavings}`;
itacCostSavings() {
return this.deductibleValue - this.totalServicePrice;
},
questionText() {
return this.getCmsContent('ServiceProviderQuestion', 'QuestionText');
itacCostSavingsForDisplay() {
return this.getFormattedAmount(this.itacCostSavings);
},
answersFromCms() {
return this.getCmsContent('ServiceProviderQuestion', 'Answers');
serviceProviderQuestionText() {
return this.getCmsContent(
this.widget.serviceProviderQuestion,
widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT
);
},
displayQuote() {
serviceProviderQuestionAnswers() {
return this.getCmsContent(
this.widget.serviceProviderQuestion,
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
);
},
isQuoteDisplayed() {
return this.verifiedITAC || this.verifiedNoComp;
},
shouldRegisterClaim() {
return this.policyLookupSuccessful
&& useMainStore().vehicle.policyVehicleId != null
&& useMainStore().vehicle.policyVehicleId >= 0
&& useMainStore().isClaimRegistrationRequired
&& !useMainStore().isClaimAlreadyRegistered
&& (this.coveredAndServicePriceAboveOrEqualDeductible || this.verifiedITAC);
}
},
watch: {
selectedProvider() {
if (this.selectedProvider === 'Safelite') {
this.$refs.siteFooter.updateButtonText('Continue with Safelite');
} else {
this.$refs.siteFooter.updateButtonText('Continue');
}
const buttonText = this.selectedProvider === SAFELITE_PROVIDER ? 'Continue with Safelite' : 'Safelite';
this.$refs.siteFooter.updateButtonText(buttonText);
},
nextStepsBody(newValue, oldValue) {
if (newValue !== oldValue) {
@ -353,78 +378,43 @@ export default {
arePagePrerequisitesValid() {
return !!useMainStore().vehicle.carId;
},
getFormattedAmount(amount) {
return this.currencyFormatter.format(amount);
},
async initializeComponent() {
useMainStore().updatePolicyITACFlag(this.verifiedITAC);
if (this.policyLookupSuccessful
&& useMainStore().order.vehicle.policyVehicleId >= 0
&& useMainStore().isClaimRegistrationRequired
&& !useMainStore().isClaimAlreadyRegistered
&& (this.coveredAndServicePriceAboveOrEqualDeductible || this.verifiedITAC)) {
if (this.shouldRegisterClaim) {
await useMainStore().registerClaim()?.catch(() => {});
}
this.$refs.loadingModal.hideModal();
},
async forwardButtonAction() {
return this.navigateForward();
},
async navigateForward() {
if (this.unverified || this.verifiedDeductible) {
useMainStore().updateSupportingItems(this.supportingItems);
this.$router.navigate(
navigationScenarios.CLICKED_FORWARD,
this.$route,
{},
{ [routerParams.SAVE_SESSION_SYNCHRONOUS]: true }
);
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD);
} else if (this.verifiedITAC || this.verifiedNoComp) {
useMainStore().updateIsSafeliteProvider(this.selectedProvider === 'Safelite');
if (this.selectedProvider === 'Safelite') {
useMainStore().updateIsSafeliteProvider(this.selectedProvider === SAFELITE_PROVIDER);
if (this.selectedProvider === SAFELITE_PROVIDER) {
useMainStore().updateSupportingItems(this.supportingItems);
this.$router.navigate(
navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
this.$route,
{},
{ [routerParams.SAVE_SESSION_SYNCHRONOUS]: true }
);
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE);
} else {
this.mainStore.setBailout(this.$router.currentRoute, bailoutMessage.RequestCallback());
this.$router.navigate(
navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP,
this.$route,
{},
{ [routerParams.SAVE_SESSION_SYNCHRONOUS]: true }
);
this.setBailoutWithMessage(bailoutMessage.RequestCallback());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP);
}
} else {
this.mainStore.setBailout(this.$router.currentRoute, bailoutMessage.coverageStatementInvalidState());
this.$router.navigate(
navigationScenarios.CLICKED_FORWARD_WITH_INVALID_STATE,
this.$route,
{},
{ [routerParams.SAVE_SESSION_SYNCHRONOUS]: true }
);
this.setBailoutWithMessage(bailoutMessage.coverageStatementInvalidState());
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_INVALID_STATE);
}
},
processIfStatements,
getHeaderTextFromCms(cmsWidgetName) {
const header = this.getCmsContent(cmsWidgetName, 'HeaderText');
return this.processIfStatements(header, 'custom', this.getCustomValueFromString);
setBailoutWithMessage(message) {
setBailout(this.$router.currentRoute, message);
},
getSubheaderTextFromCms(cmsWidgetName) {
const subHeader = this.getCmsContent(cmsWidgetName, 'SubHeaderText');
return this.processIfStatements(subHeader, 'custom', this.getCustomValueFromString);
navigateWithScenario(scenario) {
this.$router.navigate(scenario, this.$route);
},
getBodyTextFromCms(cmsWidgetName) {
const bodyText = this.getCmsContent(cmsWidgetName, 'BodyText');
return this.processIfStatements(bodyText, 'custom', this.getCustomValueFromString);
},
getSecondaryTextFromCms(cmsWidgetName) {
const secondaryText = this.getCmsContent(cmsWidgetName, 'SecondaryText');
return this.processIfStatements(secondaryText, 'custom', this.getCustomValueFromString);
},
getExplantoryTextFromCms(cmsWidgetName) {
const explanatoryText = this.getCmsContent(cmsWidgetName, 'BodyText');
return this.processIfStatements(explanatoryText, 'custom', this.getCustomValueFromString);
getTextFromCmsWithCustomIfStatements(widgetName, widgetField) {
const rawText = this.getCmsContent(widgetName, widgetField);
return processIfStatements(rawText, 'custom', this.getCustomValueFromString);
},
getCustomValueFromString(str) {
switch (str) {
@ -443,29 +433,18 @@ export default {
case 'nonADASRepair':
return this.isRepair;
case 'deductibleOverZero':
return this.verifiedDeductible && !this.isDeductibleZero; // TODO what if deductible is negative?
return this.verifiedDeductible && this.deductibleValue !== 0; // TODO what if deductible is negative?
case 'isDeductibleZero':
return this.verifiedDeductible && this.isDeductibleZero;
return this.verifiedDeductible && this.deductibleValue === 0;
default:
return null;
}
},
getTotalLineItemPrice(lineItem) {
return lineItem.kitPrice + lineItem.laborAmount + lineItem.sellingPrice;
},
getDeductibleString(deductible) {
const formattedDeductibleFloat = parseFloat(deductible).toFixed(2);
return `$${formattedDeductibleFloat}`;
},
getServicePriceString(price) {
const formattedPriceFloat = parseFloat(price).toFixed(2);
return `$${formattedPriceFloat}`;
},
getITACCostSavings(vehicleDeductible, totalServicePrice) {
return vehicleDeductible - totalServicePrice;
},
setSupportingItems(newSupportingItems) {
this.supportingItems = newSupportingItems;
},
setBaseServiceLineItems(lineItems) {
this.baseServiceLineItems = lineItems;
}
}
};
@ -475,7 +454,7 @@ export default {
.cost {
color: $green;
font-size: 2rem;
font-weight: 300;
font-weight: $font-weight-light;
line-height: 2.75rem;
}

View file

@ -136,7 +136,8 @@ describe('duplicateCheck.vue', () => {
expect(wrapper.vm.duplicateOrders[0]).toStrictEqual({
Text: duplicateOrderText,
Name: referralNumber,
SubText: expectedSubtext
SubText: expectedSubtext,
value: useMainStore().applicationUser.duplicateOrders[0]
});
});
test('duplicateOrder in store with null vehicle make => returns order with only date subtext', () => {
@ -173,7 +174,8 @@ describe('duplicateCheck.vue', () => {
expect(wrapper.vm.duplicateOrders[0]).toStrictEqual({
Text: duplicateOrderText,
Name: referralNumber,
SubText: expectedSubtext
SubText: expectedSubtext,
value: useMainStore().applicationUser.duplicateOrders[0]
});
});
test('duplicateOrder in store with null vehicle model => returns order with only date subtext', () => {
@ -210,7 +212,8 @@ describe('duplicateCheck.vue', () => {
expect(wrapper.vm.duplicateOrders[0]).toStrictEqual({
Text: duplicateOrderText,
Name: referralNumber,
SubText: expectedSubtext
SubText: expectedSubtext,
value: useMainStore().applicationUser.duplicateOrders[0]
});
});
test('duplicateOrder in store with all vehicle info => returns order with year, make, model and date in subtext', () => {
@ -248,7 +251,8 @@ describe('duplicateCheck.vue', () => {
expect(wrapper.vm.duplicateOrders[0]).toStrictEqual({
Text: duplicateOrderText,
Name: referralNumber,
SubText: `${expectedVehicle}, ${expectedDate}`
SubText: `${expectedVehicle}, ${expectedDate}`,
value: useMainStore().applicationUser.duplicateOrders[0]
});
});
});
@ -352,7 +356,7 @@ describe('duplicateCheck.vue', () => {
test('Selected duplicate => load session called', async () => {
// Arrange
const newOrderSelectionName = getRandomString(6, 6);
const selectedAnswer = getRandomString(6, 6);
const selectedAnswer = {};
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }
@ -405,7 +409,7 @@ describe('duplicateCheck.vue', () => {
test('Load session throws error => still navigate forward', async () => {
// Arrange
const newOrderSelectionName = getRandomString(6, 6);
const selectedAnswer = getRandomString(6, 6);
const selectedAnswer = {};
const mountOptions = getMountOptions({
router: { navigate: jest.fn() }

View file

@ -75,7 +75,7 @@ export default {
},
data() {
return {
selectedAnswer: '',
selectedAnswer: null,
widget: {
siteHeader: 'SiteHeaderWidget',
siteSubHeader: 'SiteSubHeaderWidget',
@ -113,7 +113,8 @@ export default {
return {
Text: duplicateOrderText,
Name: o.referralNumber,
SubText: toTitleCase(subtext)
SubText: toTitleCase(subtext),
value: o
};
}) ?? [];
},
@ -126,13 +127,16 @@ export default {
* @summary Steps to perform when forward button clicked.
*/
async forwardButtonAction() {
if (this.selectedAnswer !== this.getNewOrderSelectionName) {
await useMainStore().loadSession()
.then(() => {}, () => {})
.finally(() => { this.navigateForward(); });
} else {
this.navigateForward();
if (this.selectedAnswer !== null && typeof this.selectedAnswer === 'object') {
await useMainStore().loadSession(this.selectedAnswer)
.catch(() => {})
.finally(() => {
this.navigateForward();
});
return;
}
this.navigateForward();
},
navigateForward() {
if (!this.mainStore.order.policy.policyLookupSuccessful) {

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

@ -25,7 +25,7 @@
<p>{{ appointmentDateFormatted }}</p>
<p>{{ appointmentTimeFormatted }}</p>
</div>
<!-- <addToCalendar
<addToCalendar
mobileWidgetName="addToCalendar_Mobile"
inShopWidgetName="addToCalendar_InShop"
dropOffWidgetName="addToCalendar_DropOff"
@ -37,7 +37,7 @@
:appointmentType="appointmentType"
:scheduleDate="appointmentDate"
:scheduleStartTime="appointmentStartTime"
:scheduleEndTime="appointmentEndTime" /> -->
:scheduleEndTime="appointmentEndTime" />
<div
class="appointment-text text-center lh-base"
v-html="appointmentWordingText">
@ -72,7 +72,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';
@ -80,15 +82,16 @@ 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,
addToCalendar,
// eslint-disable-next-line vue/no-reserved-component-names
Form
addToCalendar
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
useMainStore().createSubmittedOrder();
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
@ -105,7 +108,8 @@ export default {
},
setup() {
const mainStore = useMainStore();
return { mainStore };
const { submittedOrder } = mainStore;
return { mainStore, submittedOrder };
},
computed: {
carrierName() {
@ -121,16 +125,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
@ -159,39 +163,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) {
@ -232,18 +236,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;
}
@ -289,7 +294,7 @@ export default {
}
};
</script>
<!-- <style lang="scss" scoped>
<style lang="scss" scoped>
$page-side-padding: 1.5rem;
.page-container-grouped-styles {
@ -325,9 +330,12 @@ $page-side-padding: 1.5rem;
}
.appointment-text {
:deep(p) {
margin: 0;
}
:deep(strong) {
font-weight: $font-weight-bold;
color: $black;
}
}
</style> -->
</style>

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

@ -11,7 +11,8 @@ import applicationConfig from '@/constants/application-config';
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 { AppointmentTypeStrings } 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,
@ -1224,20 +1285,17 @@ export const useMainStore = defineStore({
});
},
async loadSession() {
async loadSession(duplicate) {
const { applicationUser, order, issConfig } = this;
// TODO how to get savedSessionId for a duplicate referral?
try {
const response = await globalMethods.callHttpClient({
method: endpoints.LoadSession.method,
endpoint: endpoints.LoadSession.url,
payload: {
savedSessionId: applicationUser.savedSessionId?.toString(),
referralNumber: order.referralNumber?.toString(),
referralDate: order.referralDate?.toString(),
referralNumber: duplicate.referralNumber,
referralDate: duplicate.responseDate,
parentAccountNumber: issConfig.parentAccountNumber,
referralCorrelationId: order.referralCorrelationId
referralCorrelationId: duplicate.referralCorrelationId
}
});
const { data } = response;
@ -1246,10 +1304,6 @@ export const useMainStore = defineStore({
return response;
}
applicationUser.crmCustomerId = data.applicationUser?.crmCustomerId;
applicationUser.experiments = data.applicationUser?.experiments ?? [];
applicationUser.savedSessionId = data.applicationUser?.savedSessionId;
if (order.policy.policyLookupSuccessful) {
order.customer.emailAddress = data.customer?.emailAddress;
order.customer.firstName = data.customer?.firstName;
@ -1791,6 +1845,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 +2270,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 +2295,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 +2303,8 @@ export const useMainStore = defineStore({
// clear vuex
this.resetState();
// restore issConfig
this.issConfig = issConfig;
// restore user's experiments
this.applicationUser.experiments = experiments;
},
@ -2295,6 +2413,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}`;

View file

@ -961,10 +961,7 @@ describe('Store', () => {
describe('loadSession method', () => {
describe('successful method call', () => {
const applicationUser = {
crmCustomerId: getRandomString(6, 6),
experiments: getRandomString(6, 6),
pageData: getRandomString(6, 6),
savedSessionId: getRandomString(6, 6)
experiments: getRandomString(6, 6)
};
const vehicle = {
year: getRandomString(6, 6),
@ -1006,9 +1003,13 @@ describe('Store', () => {
it('calls load session api endpoint', async () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({ data: {} }));
const duplicate = {
referralNumber: getRandomString(6, 6),
referralCorrelationId: getRandomGuid()
};
// Act
store.loadSession();
store.loadSession(duplicate);
// Asserts
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({
@ -1020,34 +1021,31 @@ describe('Store', () => {
// Arrange
const response = { data: { ReferralNumber: getRandomString(6, 6) } };
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response));
const duplicate = {
referralNumber: getRandomString(6, 6),
referralCorrelationId: getRandomGuid()
};
// Act
const result = store.loadSession();
const result = store.loadSession(duplicate);
// Asserts
await expect(result).resolves.toBe(response.data);
});
it('sets expected application user data', async () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(fullApiResponse));
// Act
await store.loadSession();
// Asserts
expect(store.applicationUser.experiments).toEqual(applicationUser.experiments);
expect(store.applicationUser.savedSessionId).toBe(applicationUser.savedSessionId);
expect(store.applicationUser.crmCustomerId).toBe(applicationUser.crmCustomerId);
});
it('sets expected vehicle data', async () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(fullApiResponse));
const duplicate = {
referralNumber: getRandomString(6, 6),
referralCorrelationId: getRandomGuid()
};
store.order.policy.policyLookupSuccessful = true;
store.policy.vehicles = [{ vin: vehicle.vin }];
// Act
await store.loadSession();
await store.loadSession(duplicate);
// Asserts
expect(store.vehicle.year).toBe(vehicle.year);
@ -1061,11 +1059,16 @@ describe('Store', () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(fullApiResponse));
const duplicate = {
referralNumber: getRandomString(6, 6),
referralCorrelationId: getRandomGuid()
};
store.order.policy.policyLookupSuccessful = true;
store.policy.vehicles = [{ vin: vehicle.vin }];
// Act
await store.loadSession();
await store.loadSession(duplicate);
// Asserts
expect(store.order.customer.address.streetAddress).toBe(customer.address.streetAddress);
@ -1081,10 +1084,14 @@ describe('Store', () => {
it('sets expected remaining order data', async () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(fullApiResponse));
const originalWorkOrderNumber = store.order.workOrderNumber;
const duplicate = {
referralNumber: getRandomString(6, 6),
referralCorrelationId: getRandomGuid()
};
// Act
await store.loadSession();
await store.loadSession(duplicate);
// Asserts
expect(store.order.referralNumber).toBe(fullApiResponse.data.referralNumber);
@ -1092,7 +1099,6 @@ describe('Store', () => {
expect(store.order.referralCorrelationId).toBe(fullApiResponse.data.referralCorrelationId);
expect(store.order.referralSequenceNumber).toBe(fullApiResponse.data.referralSequenceNumber);
expect(store.order.eon).toBe(fullApiResponse.data.eon);
expect(store.order.workOrderNumber).toBe(originalWorkOrderNumber);
});
});
it('api call throws exception', async () => {
@ -1100,8 +1106,13 @@ describe('Store', () => {
const error = 'load session error';
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error));
const duplicate = {
referralNumber: getRandomString(6, 6),
referralCorrelationId: getRandomGuid()
};
// Act
await store.loadSession().catch((e) => {
await store.loadSession(duplicate).catch((e) => {
expect(e).toEqual(error);
});