Merge branch 'develop' into dependabot/npm_and_yarn/multi-75e6bc5210
This commit is contained in:
commit
093c1f67e8
51 changed files with 3400 additions and 662 deletions
9
jsconfig.json
Normal file
9
jsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ SKIP_CONTENT_SITE="false"
|
|||
# Experiments Flag
|
||||
IS_MOBILEFIRST="false"
|
||||
IS_ADYENPAYMENTS="false"
|
||||
IS_MULTILOCATIONPOPUP="false"
|
||||
|
||||
# Base URLs by environment
|
||||
# qa
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ SKIP_CONTENT_SITE="false"
|
|||
# Experiments Flag
|
||||
IS_MOBILEFIRST="false"
|
||||
IS_ADYENPAYMENTS="false"
|
||||
IS_MULTILOCATIONPOPUP="false"
|
||||
|
||||
# Base URLs by environment
|
||||
# qa
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@ export function getDefaultExperimentsData(): IExperiments {
|
|||
return {
|
||||
isAdyenPayments: !!process.env.IS_ADYENPAYMENTS && process.env.IS_ADYENPAYMENTS !== "" ? process.env.IS_ADYENPAYMENTS === "true" : false,
|
||||
isMobileFirst: !!process.env.IS_MOBILEFIRST && process.env.IS_MOBILEFIRST !== "" ? process.env.IS_MOBILEFIRST === "true" : false,
|
||||
isMultiLocationPopup: !!process.env.IS_MULTILOCATIONPOPUP && process.env.IS_MULTILOCATIONPOPUP !== "" ? process.env.IS_MULTILOCATIONPOPUP === "true" : false,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export interface IExperiments {
|
||||
isMobileFirst: boolean,
|
||||
isAdyenPayments: boolean
|
||||
isAdyenPayments: boolean,
|
||||
isMultiLocationPopup: boolean
|
||||
}
|
||||
|
|
@ -257,8 +257,12 @@ export class BasePage {
|
|||
: "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true";
|
||||
|
||||
experimentsURLExtension += experiments?.isAdyenPayments
|
||||
? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)"
|
||||
: ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)";
|
||||
? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)=true"
|
||||
: ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)=true";
|
||||
|
||||
experimentsURLExtension += experiments?.isMultiLocationPopup
|
||||
? ",MultiLocationPopup=MultiLocationPopup_V1=MultiLocationPopup_TEST=true"
|
||||
: ",MultiLocationPopup=MultiLocationPopup_V1=MultiLocationPopup_CONTROL=true";
|
||||
} else {
|
||||
console.log("Url extension without query string");
|
||||
}
|
||||
|
|
|
|||
81
playwright-tests/pages/PaymentAdyenPage.ts
Normal file
81
playwright-tests/pages/PaymentAdyenPage.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { type Locator, type Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { IPaymentDetails } from 'safelite-playwright-core';
|
||||
import { ITestData } from 'framework/TestData';
|
||||
import { PaypalPage } from './PaypalPage';
|
||||
|
||||
export class PaymentAdyenPage extends BasePage {
|
||||
readonly page: Page;
|
||||
readonly creditOrDebitCardButton: Locator;
|
||||
readonly nameOnCardTextField: Locator;
|
||||
readonly cardNumberTextField: Locator;
|
||||
readonly expiryDateTextField: Locator;
|
||||
readonly cvvTextField: Locator;
|
||||
readonly countryDropDown: Locator;
|
||||
readonly billingAddressTextField: Locator;
|
||||
readonly cityTextField: Locator;
|
||||
readonly stateDropDown: Locator;
|
||||
readonly billingZipTextField: Locator;
|
||||
readonly submitPaymentButton: Locator;
|
||||
readonly payPalButton: Locator;
|
||||
readonly navigateToPaypalButton: Locator;
|
||||
readonly afterPayButton: Locator;
|
||||
readonly navigateToAfterPayButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.page = page;
|
||||
this.creditOrDebitCardButton = page.locator('.adyen-checkout__payment-method--credit button');
|
||||
this.cardNumberTextField = page.frameLocator('iframe[title="Iframe for card number"]').locator('input[id*="adyen-checkout-encryptedCardNumber"]');
|
||||
this.expiryDateTextField = page.frameLocator('iframe[title="Iframe for expiry date"]').locator('input[id*="adyen-checkout-encryptedExpiryDate"]');
|
||||
this.cvvTextField = page.frameLocator('iframe[title="Iframe for security code"]').locator('input[id*="adyen-checkout-encryptedSecurityCode"]');
|
||||
this.nameOnCardTextField = page .locator('input[id*="adyen-checkout-holderName"]');
|
||||
this.countryDropDown = page.locator('input[id*="adyen-checkout-country"]');
|
||||
this.billingAddressTextField = page.locator('input[id*="adyen-checkout-street"]');
|
||||
this.cityTextField = page.locator('input[id*="adyen-checkout-city"]');
|
||||
this.stateDropDown = page.locator('input[id*="adyen-checkout-stateOrProvince"]');
|
||||
this.billingZipTextField = page.locator('input[id*="adyen-checkout-postalCode"]');
|
||||
this.submitPaymentButton = page.locator('button.adyen-checkout__button');
|
||||
this.payPalButton = page.locator('button[id*="button-paypal"]');
|
||||
this.navigateToPaypalButton = page.frameLocator('iframe[title="PayPal-paypal"]:first-of-type').locator('div[role="link"][class*="paypal-button"]');
|
||||
this.afterPayButton = page.locator('button[id*="button-redirect"]');
|
||||
this.navigateToAfterPayButton = page.locator('div.adyen-checkout__payment-method--afterpaytouch_US button.adyen-checkout__button');
|
||||
|
||||
}
|
||||
|
||||
async populateAdyenCreditCardDetails(testData: Partial<ITestData>){
|
||||
const { paymentDetails, customerDetails } = testData;
|
||||
|
||||
paymentDetails!.cardNumber = '5100 0600 0000 0002';
|
||||
paymentDetails!.expirationMonth = "12 - December";
|
||||
paymentDetails!.expirationYear = "2029";
|
||||
paymentDetails!.cvv = "737";
|
||||
|
||||
await this.creditOrDebitCardButton.click();
|
||||
await this.cardNumberTextField.fill(paymentDetails!.cardNumber || '');
|
||||
|
||||
const expirationDate = `${(paymentDetails!.expirationMonth!.split(' ')[0] || '').padStart(2, '0')} + ${paymentDetails!.expirationYear!.toString().slice(-2)}`;
|
||||
await this.expiryDateTextField.pressSequentially(expirationDate);
|
||||
|
||||
await this.cvvTextField.fill(paymentDetails!.cvv!);
|
||||
await this.nameOnCardTextField.fill(customerDetails!.firstName! + ' ' + customerDetails!.lastName!);
|
||||
await this.billingAddressTextField.fill(paymentDetails!.billingAddress!.street);
|
||||
await this.countryDropDown.pressSequentially("United States");
|
||||
await this.countryDropDown.click();
|
||||
await this.page.getByRole('option', { name: 'United States', exact: true }).click();
|
||||
|
||||
await this.cityTextField.fill(paymentDetails!.billingAddress!.city);
|
||||
await this.stateDropDown.pressSequentially(paymentDetails!.billingAddress!.state);
|
||||
await this.stateDropDown.click();
|
||||
await this.page.locator(".adyen-checkout__field--stateOrProvince li").nth(0).click();
|
||||
await this.billingZipTextField.fill(paymentDetails!.billingAddress!.postalCode);
|
||||
await this.submitPaymentButton.click();
|
||||
}
|
||||
|
||||
async navigateToAdyenPaypalCheckout(): Promise<PaypalPage> {
|
||||
await this.payPalButton.click();
|
||||
const paypalPage = this.page.waitForEvent('popup');
|
||||
await this.navigateToPaypalButton.click();
|
||||
return new PaypalPage(await paypalPage);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import { AfterpayPage } from './AfterpayPage';
|
|||
import { PaypalPage } from './PaypalPage';
|
||||
import { ITestData } from 'framework/TestData';
|
||||
import { step } from 'framework/localTypes/Step';
|
||||
import { IExperiments } from 'framework/localTypes/IExperiments';
|
||||
import { PaymentAdyenPage } from './PaymentAdyenPage';
|
||||
|
||||
export class PaymentMethodPage extends BasePage {
|
||||
readonly page: Page;
|
||||
|
|
@ -25,7 +27,9 @@ export class PaymentMethodPage extends BasePage {
|
|||
readonly submitButton: Locator;
|
||||
readonly recalibrationCheckbox: Locator;
|
||||
readonly paymentPage: PaymentPage;
|
||||
readonly paymentAdyenPage: PaymentAdyenPage;
|
||||
readonly paypalPage: PaypalPage;
|
||||
readonly afterpayPage: AfterpayPage
|
||||
readonly afterPayBreakoutSection: Locator;
|
||||
readonly afterPayToggle: Locator;
|
||||
|
||||
|
|
@ -59,7 +63,9 @@ export class PaymentMethodPage extends BasePage {
|
|||
this.recalibrationCheckbox = this.page.locator('label:has(>input[name=\'recalAckOptIn\'])');
|
||||
// this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1);
|
||||
this.paymentPage = new PaymentPage(page);
|
||||
this.paymentAdyenPage = new PaymentAdyenPage(page);
|
||||
this.paypalPage = new PaypalPage(page);
|
||||
this.afterpayPage = new AfterpayPage(page);
|
||||
|
||||
// Payment details validation locators
|
||||
this.reviewTable = this.page.locator('div.review-table');
|
||||
|
|
@ -191,35 +197,57 @@ export class PaymentMethodPage extends BasePage {
|
|||
return text;
|
||||
}
|
||||
|
||||
async executePayment(paymentDetails: IPaymentDetails, isRecalVehicle: boolean) {
|
||||
async executePayment(testData: Partial<ITestData>) {
|
||||
const browserContext = this.page.context();
|
||||
const { paymentDetails, isRecalVehicle, experiments } = testData;
|
||||
|
||||
switch (paymentDetails.paymentType) {
|
||||
switch (paymentDetails!.paymentType) {
|
||||
case PaymentType.Credit:
|
||||
await this.selectCreditCard();
|
||||
await this.nextPage();
|
||||
await this.paymentPage.populateCreditCardDetails(paymentDetails);
|
||||
experiments!.isAdyenPayments
|
||||
? await this.paymentAdyenPage.populateAdyenCreditCardDetails(testData)
|
||||
: await this.paymentPage.populateCreditCardDetails(paymentDetails!);
|
||||
break;
|
||||
case PaymentType.AfterPay:
|
||||
await this.payInFourButton.click();
|
||||
await this.continueButton.click();
|
||||
|
||||
// Capture popup
|
||||
const afterpayPopup = await browserContext.waitForEvent('page');
|
||||
const afterpayPage = new AfterpayPage(afterpayPopup);
|
||||
if (experiments?.isAdyenPayments) {
|
||||
await this.paymentAdyenPage.afterPayButton.click();
|
||||
await this.paymentAdyenPage.navigateToAfterPayButton.click();
|
||||
await this.afterpayPage.executeAfterpayPayment(paymentDetails!);
|
||||
} else {
|
||||
// Capture popup
|
||||
const afterpayPopup = await browserContext.waitForEvent('page');
|
||||
const afterpayPage = new AfterpayPage(afterpayPopup);
|
||||
|
||||
// Execute payment
|
||||
await afterpayPage.executeAfterpayPayment(paymentDetails!);
|
||||
}
|
||||
|
||||
// Execute payment
|
||||
await afterpayPage.executeAfterpayPayment(paymentDetails);
|
||||
break;
|
||||
case PaymentType.Paypal:
|
||||
await this.selectPaypal();
|
||||
// TODO: Click paypal button
|
||||
await this.nextPage();
|
||||
await this.paymentPage.navigateToPaypalCheckout();
|
||||
await this.paypalPage.completePaypalPurchase(paymentDetails);
|
||||
const paypalPage = experiments?.isAdyenPayments
|
||||
? await this.paymentAdyenPage.navigateToAdyenPaypalCheckout()
|
||||
: await this.paymentPage.navigateToPaypalCheckout();
|
||||
|
||||
/*if (experiments?.isAdyenPayments) {
|
||||
// Capture popup
|
||||
const payPalPopup = await this.page.waitForEvent('popup');
|
||||
const paypalPage = new PaypalPage(payPalPopup);
|
||||
paypalPage.completePaypalPurchase(testData);
|
||||
} else {
|
||||
await this.paypalPage.completePaypalPurchase(testData);
|
||||
}*/
|
||||
await paypalPage.completePaypalPurchase(testData);
|
||||
|
||||
break;
|
||||
case PaymentType.PayAtService:
|
||||
await this.selectPayAtService(isRecalVehicle);
|
||||
await this.selectPayAtService(isRecalVehicle!);
|
||||
await this.nextPage();
|
||||
break;
|
||||
case PaymentType.PayWithInsurance:
|
||||
|
|
@ -527,7 +555,7 @@ export class PaymentMethodPage extends BasePage {
|
|||
|
||||
@step("PaymentMethodPage >> Select Payment Method: ")
|
||||
async handlePaymentMethodPage(testData: Partial<ITestData>) {
|
||||
const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData;
|
||||
const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM, experiments } = testData;
|
||||
|
||||
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
|
||||
await this.validatePaymentDetailsPage(testData);
|
||||
|
|
@ -542,7 +570,8 @@ export class PaymentMethodPage extends BasePage {
|
|||
await this.verifyVAPS();
|
||||
}
|
||||
if (paymentDetails?.paymentType) {
|
||||
await this.executePayment(paymentDetails!, isRecalVehicle!);
|
||||
// await this.executePayment(paymentDetails!, isRecalVehicle!, experiments!);
|
||||
await this.executePayment(testData);
|
||||
} else {
|
||||
await this.nextPage();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { type Locator, type Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { IPaymentDetails } from 'safelite-playwright-core';
|
||||
import { PaypalPage } from './PaypalPage';
|
||||
|
||||
export class PaymentPage extends BasePage {
|
||||
readonly page: Page;
|
||||
|
|
@ -43,8 +44,8 @@ export class PaymentPage extends BasePage {
|
|||
await this.submitPaymentButton.click();
|
||||
}
|
||||
|
||||
async navigateToPaypalCheckout() {
|
||||
async navigateToPaypalCheckout(): Promise<PaypalPage> {
|
||||
await this.payPalButton.click();
|
||||
return new PaypalPage(this.page);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, type Locator, type Page } from '@playwright/test';
|
||||
import { BasePage } from './BasePage';
|
||||
import { IPaymentDetails } from 'safelite-playwright-core';
|
||||
import { ITestData } from 'framework/TestData';
|
||||
|
||||
export class PaypalPage extends BasePage {
|
||||
readonly page: Page;
|
||||
|
|
@ -30,24 +30,28 @@ export class PaypalPage extends BasePage {
|
|||
this.tryAnotherWayButton = page.getByRole('button', { name: 'Try another way' });
|
||||
}
|
||||
|
||||
async completePaypalPurchase(paymentDetails: IPaymentDetails){
|
||||
async completePaypalPurchase(testData: Partial<ITestData>){
|
||||
|
||||
const {paymentDetails, experiments } = testData;
|
||||
// Wait for the PayPal login page to load
|
||||
await this.page.waitForFunction(() => window.location.href.includes('sandbox.paypal.com'), null, { timeout: 10000 });
|
||||
|
||||
// handle flow with and without the "Log in with a password instead" link
|
||||
if (await this.loginWithPasswordButton.isVisible()) {
|
||||
await this.loginWithPasswordButton.click();
|
||||
await this.passwordTextBox.fill(paymentDetails.password!);
|
||||
await this.passwordTextBox.fill(paymentDetails!.password!);
|
||||
await this.paypalLoginButton.click();
|
||||
await this.completePurchaseButton.click();
|
||||
} else {
|
||||
await this.usernameTextBox.waitFor({ state: 'visible' });
|
||||
await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true });
|
||||
await this.usernameTextBox.fill(paymentDetails.username!);
|
||||
await this.usernameTextBox.fill(paymentDetails!.username!);
|
||||
await this.nextButton.click();
|
||||
await this.tryAnotherWayButton.click();
|
||||
await this.usePasswordInsteadButton.click();
|
||||
await this.passwordTextBox.fill(paymentDetails.password!);
|
||||
if (!experiments!.isAdyenPayments){
|
||||
await this.tryAnotherWayButton.click();
|
||||
await this.usePasswordInsteadButton.click();
|
||||
}
|
||||
await this.passwordTextBox.fill(paymentDetails!.password!);
|
||||
await this.paypalLoginButton.click();
|
||||
await this.payWithRadioButton.click();
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
|
|
|||
3
src/assets/img/icons/alert-circle-yellow.svg
Normal file
3
src/assets/img/icons/alert-circle-yellow.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
|
||||
<path fill="#E86421" fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 474 B |
5
src/assets/img/icons/info-circle-blue.svg
Normal file
5
src/assets/img/icons/info-circle-blue.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0C6.41775 0 4.87104 0.469192 3.55544 1.34824C2.23985 2.22729 1.21447 3.47672 0.608967 4.93853C0.00346626 6.40034 -0.15496 8.00887 0.153721 9.56072C0.462403 11.1126 1.22433 12.538 2.34315 13.6569C3.46197 14.7757 4.88743 15.5376 6.43928 15.8463C7.99113 16.155 9.59966 15.9965 11.0615 15.391C12.5233 14.7855 13.7727 13.7602 14.6518 12.4446C15.5308 11.129 16 9.58225 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0V0ZM8.4672 11.4C8.4672 11.5485 8.4082 11.691 8.30318 11.796C8.19816 11.901 8.05572 11.96 7.9072 11.96C7.75868 11.96 7.61624 11.901 7.51122 11.796C7.4062 11.691 7.3472 11.5485 7.3472 11.4V7.08C7.3472 6.93148 7.4062 6.78904 7.51122 6.68402C7.61624 6.579 7.75868 6.52 7.9072 6.52C8.05572 6.52 8.19816 6.579 8.30318 6.68402C8.4082 6.78904 8.4672 6.93148 8.4672 7.08V11.4ZM7.9072 5.4C7.79645 5.4 7.68818 5.36716 7.59608 5.30562C7.50399 5.24409 7.43222 5.15663 7.38983 5.0543C7.34745 4.95198 7.33636 4.83938 7.35796 4.73075C7.37957 4.62212 7.43291 4.52234 7.51122 4.44402C7.58954 4.3657 7.68932 4.31237 7.79795 4.29076C7.90658 4.26915 8.01918 4.28024 8.12151 4.32263C8.22383 4.36501 8.31129 4.43679 8.37283 4.52888C8.43436 4.62097 8.4672 4.72924 8.4672 4.84C8.46741 4.9136 8.45307 4.98651 8.42501 5.05455C8.39694 5.12259 8.3557 5.18441 8.30365 5.23645C8.25161 5.28849 8.18979 5.32973 8.12176 5.3578C8.05372 5.38587 7.9808 5.40021 7.9072 5.4Z" fill="#0070D1"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -15,6 +15,8 @@ const GaCategories = {
|
|||
APPOINTMENT: "Appointment",
|
||||
SERVICE_LOCATION: "service-location",
|
||||
CONFIRMATION_CLICKED: "confirmation clicked",
|
||||
INSHOP_CONFIRMATION_CLICKED: "inshop confirmation clicked",
|
||||
MOBILE_CONFIRMATION_CLICKED: "mobile confirmation clicked",
|
||||
RECAL_DISCLAIMER: "Recalibration disclaimer",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const experimentUniverses = {
|
|||
IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
|
||||
AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay",
|
||||
MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment",
|
||||
MULTI_LOCATION_POPUP: "MultiLocationPopup",
|
||||
};
|
||||
|
||||
const experimentSettings = {
|
||||
|
|
@ -40,7 +41,13 @@ const experimentSettings = {
|
|||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR",
|
||||
SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR",
|
||||
OFFER_OEM: "OfferOem",
|
||||
SHOW_MULTI_LOCATION_APPT: "ShowMultiLocationAppt",
|
||||
SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION: "Show_NoAvailableDaysMultiLocation",
|
||||
SHOW_NO_PM_DAYS_MULTI_LOCATION: "Show_NoPMDaysMultiLocation",
|
||||
SHOW_PM_DAYS_MULTI_LOCATION: "Show_PMDaysMultiLocation",
|
||||
USE_ADYEN_PAYMENT: "UseAdyenPayment",
|
||||
SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry",
|
||||
SERVICE_PACKAGE_ORDER: "ServicePackageOrder",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ export default {
|
|||
let classes;
|
||||
switch (this.buttonTypeString) {
|
||||
case "timeSlotModalListButton":
|
||||
case "multiLocationRadioButton":
|
||||
case "listButton":
|
||||
classes = "w-100";
|
||||
break;
|
||||
|
|
@ -265,6 +266,8 @@ export default {
|
|||
classes += " radio-button-container";
|
||||
} else if (this.buttonTypeString == "servicePackageRadio") {
|
||||
classes = "package-wrapper col";
|
||||
} else if (this.buttonTypeString == "multiLocationRadioButton") {
|
||||
classes = "multi-location-wrapper col";
|
||||
}
|
||||
|
||||
if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ import {
|
|||
convertDateToDateString,
|
||||
convertDateStringToDate,
|
||||
getTodayDateString,
|
||||
getInitialViewWeeks,
|
||||
getMonthEnd,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { useField, ErrorMessage } from "vee-validate";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
|
|
@ -291,140 +293,6 @@ export default {
|
|||
}
|
||||
this.$emit("date-selected", date);
|
||||
},
|
||||
getWeekStartDate(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
// Subtract the day of the week from date to get the date of Sunday
|
||||
const sunday = new Date(date);
|
||||
sunday.setDate(sunday.getDate() - dayOfWeek);
|
||||
return convertDateToDateString(sunday);
|
||||
},
|
||||
getWeekEndDate(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
||||
// Clone the given date and add the remaining days until Saturday
|
||||
const saturday = new Date(date);
|
||||
saturday.setDate(date.getDate() + daysUntilSaturday);
|
||||
return convertDateToDateString(saturday);
|
||||
},
|
||||
getNextWeekSunday(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
||||
// Clone the given date and add the remaining days until Sunday
|
||||
const nextSunday = new Date(date);
|
||||
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
||||
return convertDateToDateString(nextSunday);
|
||||
},
|
||||
getMonthEnd(dateStr) {
|
||||
// convert string to date, do date calc, then return a string back
|
||||
const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0);
|
||||
return convertDateToDateString(date);
|
||||
},
|
||||
getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDate) {
|
||||
// TODO: this only is for future direction; need to create logic for past direction
|
||||
const weeks = [];
|
||||
let weekStartDate = this.getWeekStartDate(todayString);
|
||||
let weekEndDate = this.getWeekEndDate(todayString);
|
||||
|
||||
if (preSelectedDate) {
|
||||
let preSelectedDateMonthEnd = this.getMonthEnd(
|
||||
preSelectedDate.replace("-mobile", "")
|
||||
);
|
||||
let monthEndsByThisWeek = false;
|
||||
let i = 0;
|
||||
while (!monthEndsByThisWeek && i < 52) {
|
||||
if (i > 0) {
|
||||
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
||||
weekEndDate = this.getWeekEndDate(weekStartDate);
|
||||
|
||||
if (
|
||||
(preSelectedDateMonthEnd > weekStartDate &&
|
||||
preSelectedDateMonthEnd < weekEndDate) ||
|
||||
preSelectedDateMonthEnd === weekStartDate ||
|
||||
preSelectedDateMonthEnd === weekEndDate
|
||||
) {
|
||||
weekEndDate = preSelectedDateMonthEnd;
|
||||
monthEndsByThisWeek = true;
|
||||
} else if (preSelectedDateMonthEnd < weekEndDate) {
|
||||
// Additional case: if month ends before this week (but not necessarily during it),
|
||||
// still cut the loop here, but don't truncate the week.
|
||||
|
||||
monthEndsByThisWeek = true;
|
||||
}
|
||||
}
|
||||
weeks.push({
|
||||
weekNum: i + 1,
|
||||
weekStartDate: weekStartDate,
|
||||
weekEndDate: weekEndDate,
|
||||
});
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < initialViewRowsToShow; i++) {
|
||||
if (i > 0) {
|
||||
weekStartDate = this.getNextWeekSunday(weekEndDate);
|
||||
weekEndDate = this.getWeekEndDate(weekStartDate);
|
||||
}
|
||||
weeks.push({
|
||||
weekNum: i + 1,
|
||||
weekStartDate: weekStartDate,
|
||||
weekEndDate: weekEndDate,
|
||||
});
|
||||
}
|
||||
// If any of these weeks is split between two months, then make them 2 separate "weeks"
|
||||
// (a week split between two months is considered 2 weeks per business requirements)
|
||||
const hasSplitWeek = (week) => {
|
||||
return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1]
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
||||
|
||||
if (splitWeekIndex > -1) {
|
||||
const week1 = [];
|
||||
const week2 = [];
|
||||
let switchToWeek2 = false;
|
||||
|
||||
for (let j = 0; j < 7; j++) {
|
||||
const newDate = convertDateStringToDate(
|
||||
weeks[splitWeekIndex].weekStartDate
|
||||
);
|
||||
newDate.setDate(newDate.getDate() + j);
|
||||
if (newDate.getDate() === 1) switchToWeek2 = true;
|
||||
if (switchToWeek2) {
|
||||
week2.push(convertDateToDateString(newDate));
|
||||
} else {
|
||||
week1.push(convertDateToDateString(newDate));
|
||||
}
|
||||
}
|
||||
|
||||
const week1EndDate = week1[week1.length - 1];
|
||||
const week2StartDate = week2[0];
|
||||
|
||||
if (week1EndDate < todayString) {
|
||||
// replace week 1 with week 2
|
||||
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
||||
} else {
|
||||
const newWeek = {
|
||||
weekNum: weeks[splitWeekIndex].weekNum,
|
||||
weekStartDate: week2StartDate,
|
||||
weekEndDate: weeks[splitWeekIndex].weekEndDate,
|
||||
};
|
||||
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
||||
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
||||
weeks.pop();
|
||||
weeks.forEach((item, index) => {
|
||||
if (index > splitWeekIndex) {
|
||||
item.weekNum = item.weekNum + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return weeks;
|
||||
},
|
||||
async loadInitialData(config) {
|
||||
/*
|
||||
** NOTE: this _could_ be called by a parent before fully loaded, so FYI component data or computeds might not be available
|
||||
|
|
@ -444,10 +312,10 @@ export default {
|
|||
if (config.selectableDatesSetting === "past") calendarViewDirection = "past";
|
||||
if (config.selectableDatesSetting === "custom") calendarViewDirection = "future";
|
||||
|
||||
const currentMonthEnd = this.getMonthEnd(todayDateString);
|
||||
const currentMonthEnd = getMonthEnd(todayDateString);
|
||||
// TODO - set up currentMonthStart if direction is PAST:
|
||||
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
|
||||
const initialViewWeeks = this.getInitialViewWeeks(
|
||||
const initialViewWeeks = getInitialViewWeeks(
|
||||
todayDateString,
|
||||
config.initialViewRowsToShow,
|
||||
config.preSelectedDate
|
||||
|
|
|
|||
|
|
@ -31,23 +31,15 @@
|
|||
<div class="donation-slider my-4">
|
||||
<input
|
||||
name="amount"
|
||||
id="one"
|
||||
id="zero"
|
||||
type="radio"
|
||||
:value="donationValues[0]"
|
||||
checked />
|
||||
<label for="one" class="label">$1</label>
|
||||
<input
|
||||
name="amount"
|
||||
id="three"
|
||||
type="radio"
|
||||
:value="donationValues[1]" />
|
||||
<label for="three" class="label">$3</label>
|
||||
<input
|
||||
name="amount"
|
||||
id="five"
|
||||
type="radio"
|
||||
:value="donationValues[2]" />
|
||||
<label for="five" class="label">$5</label>
|
||||
<label for="zero" class="label">${{ donationValues[0] }}</label>
|
||||
<input name="amount" id="one" type="radio" :value="donationValues[1]" />
|
||||
<label for="one" class="label">${{ donationValues[1] }}</label>
|
||||
<input name="amount" id="two" type="radio" :value="donationValues[2]" />
|
||||
<label for="two" class="label">${{ donationValues[2] }}</label>
|
||||
<div class="indicator-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -264,17 +256,17 @@ export default {
|
|||
background: $blue;
|
||||
}
|
||||
input {
|
||||
&#one:checked {
|
||||
&#zero:checked {
|
||||
~ .indicator-bar {
|
||||
transform: translateX(2rem);
|
||||
}
|
||||
}
|
||||
&#three:checked {
|
||||
&#one:checked {
|
||||
~ .indicator-bar {
|
||||
transform: translateX(calc(100% + 2rem));
|
||||
}
|
||||
}
|
||||
&#five:checked {
|
||||
&#two:checked {
|
||||
~ .indicator-bar {
|
||||
transform: translateX(calc(200% + 2rem));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,10 @@ export default {
|
|||
serviceLocationZipCode:
|
||||
store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: pricedLineItemsToTax,
|
||||
serviceLocationAddressLine1:
|
||||
store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2:
|
||||
store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
|
|||
|
|
@ -343,3 +343,14 @@ export function splitCMSCopyOnParagraphTag(copy) {
|
|||
export function splitCMSCopyOnBR(copy) {
|
||||
return copy.split("<br>");
|
||||
}
|
||||
|
||||
// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not.
|
||||
export function formatToUSDollar(amount) {
|
||||
const isInteger = amount % 1 === 0;
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: isInteger ? 0 : 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export async function loadSessionIfPresent(isConceptInsurance, pageNameToLog) {
|
|||
// Do nothing if there is no cookie or session to use for loading.
|
||||
if (
|
||||
funnelCookie == null ||
|
||||
funnelCookie.SavedSessionId == null ||
|
||||
funnelCookie.ReferralCorrelationId == null ||
|
||||
!funnelCookie.ReferralNumber
|
||||
) {
|
||||
|
|
@ -128,7 +127,7 @@ async function loadSession(
|
|||
const response = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.LOAD_SESSION,
|
||||
{
|
||||
savedSessionId: savedSessionId?.toString(),
|
||||
savedSessionId: savedSessionId != null ? savedSessionId.toString() : null,
|
||||
referralNumber,
|
||||
referralDate,
|
||||
referralCorrelationId,
|
||||
|
|
|
|||
281
src/helpers/page-prerequisites-helper.js
Normal file
281
src/helpers/page-prerequisites-helper.js
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
import { debugLog } from "@/helpers/debug-log-helper.js";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import { coverageStatus } from "@/constants/insurance";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
|
||||
/**
|
||||
* Logs the start of the page prerequisites check.
|
||||
* Should be used on pages before the page prerequisites check is executed.
|
||||
* @param {string} source - The source of the page prerequisites check.
|
||||
* @param {boolean} preReqResult - The result of the page prerequisites check.
|
||||
*/
|
||||
export function logPagePrereqsStart(source, preReqResult) {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog(`--- ${source} pagePrereqs start ---`, null, !preReqResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the end of the page prerequisites check.
|
||||
* Should be used on pages after the page prerequisites check is executed.
|
||||
* @param {string} source - The source of the page prerequisites check.
|
||||
* @param {boolean} preReqResult - The result of the page prerequisites check.
|
||||
*/
|
||||
export function logPagePrereqsEnd(source, preReqResult) {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog(`--- ${source} pagePrereqs end ---`, null, !preReqResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the page prerequisites logs.
|
||||
* @param {string} source - The source of the page prerequisites check. This is the page name most places.
|
||||
* @param {boolean} preReqResult - The result of the page prerequisites check.
|
||||
* @param {Array} logQueue - The queue of log functions.
|
||||
*/
|
||||
export function flushPagePrereqsLogs(source, preReqResult, logQueue) {
|
||||
logPagePrereqsStart(source, preReqResult);
|
||||
logQueue.forEach((fn) => fn());
|
||||
logPagePrereqsEnd(source, preReqResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a log function to the log queue.
|
||||
* @param {Array} logQueue - The queue of log functions.
|
||||
* @param {Function} logFn - The log function to be executed.
|
||||
*/
|
||||
function queueLogging(logQueue, logFn) {
|
||||
if (logQueue) {
|
||||
logQueue.push(logFn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service zip code information is valid for the order.
|
||||
* Should be used on pages after service zip code collection is complete.
|
||||
* Returns true if the zip code and zip code CTU are present on serviceLocation.
|
||||
*/
|
||||
export function hasServiceZipInfo(order, logQueue = null) {
|
||||
const serviceLocation = order.serviceLocation;
|
||||
const result = !!(serviceLocation.zipCode && serviceLocation.zipCodeCtu);
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasServiceZipInfo:", result, !result);
|
||||
debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !result);
|
||||
debugLog("serviceLocation.zipCodeCtu:", serviceLocation.zipCodeCtu, !result);
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the service location information is valid for the appointment.
|
||||
* Returns true if the required info is present for the chosen appointment type.
|
||||
* For MOBILE: address, city, state, and zipCode must be present on serviceLocation.
|
||||
* For INSHOP/DROPOFF: all provider address fields must be present.
|
||||
*/
|
||||
export function hasServiceLocationInfo(order, logQueue = null) {
|
||||
const serviceLocation = order.serviceLocation;
|
||||
const mobileReqs = !!(
|
||||
serviceLocation.address &&
|
||||
serviceLocation.city &&
|
||||
serviceLocation.state &&
|
||||
serviceLocation.zipCode
|
||||
);
|
||||
|
||||
const providerLocation = serviceLocation.provider.address;
|
||||
const dropOffInshopReqs = !!(
|
||||
providerLocation.streetAddress &&
|
||||
providerLocation.city &&
|
||||
providerLocation.state &&
|
||||
providerLocation.zipCode
|
||||
);
|
||||
|
||||
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
const result = (isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasServiceLocationInfo mobileReqs:", mobileReqs, !result);
|
||||
debugLog("serviceLocation.address:", serviceLocation.address, !result);
|
||||
debugLog("serviceLocation.city:", serviceLocation.city, !result);
|
||||
debugLog("serviceLocation.state:", serviceLocation.state, !result);
|
||||
debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !result);
|
||||
debugLog("dropOffInshopReqs:", dropOffInshopReqs, !result);
|
||||
debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !result);
|
||||
debugLog("providerLocation.city:", providerLocation.city, !result);
|
||||
debugLog("providerLocation.state:", providerLocation.state, !result);
|
||||
debugLog("providerLocation.zipCode:", providerLocation.zipCode, !result);
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the insurance information is valid for the order. To be used after all insurance info collection is complete.
|
||||
* Returns true if cash selected or if insurance is selected and some insurance fields are present.
|
||||
*/
|
||||
export function hasInsuranceInfo(order, logQueue = null) {
|
||||
const isInsurance = order.payment?.isInsurance;
|
||||
const insuranceCoverageStatus = order.payment?.insuranceCoverage?.coverageStatus;
|
||||
const currentDeductible = order.policy?.currentDeductible;
|
||||
const parentAccountNumber = order.payment?.parentAccountNumber;
|
||||
const policyNumber = order.policy?.policyNumber;
|
||||
|
||||
let result = false;
|
||||
if (isInsurance !== null) {
|
||||
if (isInsurance === false) {
|
||||
result = true; // Cash selected - no need for policy number or parent account
|
||||
} else {
|
||||
const parentAccountNotCash =
|
||||
parentAccountNumber !== applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
|
||||
const hasPolicyNumber = !!policyNumber;
|
||||
|
||||
if (!parentAccountNotCash || !hasPolicyNumber) {
|
||||
result = false;
|
||||
} else if (
|
||||
insuranceCoverageStatus === coverageStatus.VERIFIED &&
|
||||
!(currentDeductible === 0 || currentDeductible > 0)
|
||||
) {
|
||||
result = false; // if coverageStatus is verified there must be a valid deductible also
|
||||
} else {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasInsuranceInfo:", result, !result);
|
||||
if (isInsurance === true) {
|
||||
debugLog("hasInsuranceInfo parentAccountNumber:", parentAccountNumber, !result);
|
||||
debugLog("hasInsuranceInfo policyNumber:", policyNumber, !result);
|
||||
}
|
||||
if (insuranceCoverageStatus === coverageStatus.VERIFIED) {
|
||||
debugLog("hasInsuranceInfo coverageStatus:", insuranceCoverageStatus, !result);
|
||||
debugLog("hasInsuranceInfo currentDeductible:", currentDeductible, !result);
|
||||
}
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the scheduling information is valid for the order.
|
||||
* Should be used on pages after scheduling info collection is complete.
|
||||
* Returns true if the required info is present for the chosen appointment type.
|
||||
* For MOBILE: date, startTime, endTime, jobMaxMinutes, and jobMinMinutes must be present on schedule.
|
||||
* For INSHOP/DROPOFF: all provider scheduling fields must be present.
|
||||
*/
|
||||
export function hasSchedulingInfo(order, logQueue = null) {
|
||||
const schedule = order.schedule;
|
||||
const result = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasSchedulingInfo:", result, !result);
|
||||
debugLog("schedule.date:", schedule.date, !result);
|
||||
debugLog("schedule.startTime:", schedule.startTime, !result);
|
||||
debugLog("schedule.endTime:", schedule.endTime, !result);
|
||||
debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !result);
|
||||
debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !result);
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the customer information is valid for the order.
|
||||
* Should be used on pages after customer info collection is complete.
|
||||
* Returns true if the firstName, lastName, phoneNumber, and emailAddress are present on customer.
|
||||
*/
|
||||
export function hasCustomerInfo(order, logQueue = null) {
|
||||
const customer = order.customer;
|
||||
const result = !!(
|
||||
customer.firstName &&
|
||||
customer.lastName &&
|
||||
customer.phoneNumber &&
|
||||
customer.emailAddress
|
||||
);
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasCustomerInfo:", result, !result);
|
||||
debugLog("customer.firstName:", customer.firstName, !result);
|
||||
debugLog("customer.lastName:", customer.lastName, !result);
|
||||
debugLog("customer.phoneNumber:", customer.phoneNumber, !result);
|
||||
debugLog("customer.emailAddress:", customer.emailAddress, !result);
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the glass parts or repair information is valid for the order.
|
||||
* Should be used on pages after glass parts or repair info collection is complete.
|
||||
* Returns true if the isRepair is true or if the glassParts array is not empty.
|
||||
*/
|
||||
export function hasGlassPartsOrRepairInfo(order, logQueue = null) {
|
||||
const isRepair = order.damage?.isRepair;
|
||||
const glassParts = order.lineItems?.glassParts;
|
||||
const hasGlassParts = glassParts != null && glassParts.length > 0;
|
||||
const result = !!isRepair || hasGlassParts;
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasGlassPartsOrRepairInfo:", result, !result);
|
||||
debugLog("damage.isRepair:", isRepair, !result);
|
||||
debugLog("lineItems.glassParts:", glassParts, !result);
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the payment method information is valid for the order.
|
||||
* Should only be used on pages after payment method info collection is complete which is only the payment collection pages.
|
||||
* Returns true if the isPia is true or if the piaType is set.
|
||||
*/
|
||||
export function hasPaymentMethodInfo(order, logQueue = null) {
|
||||
const payment = order.payment;
|
||||
const result = payment.isPia !== null && (payment.isPia || !!payment.piaType);
|
||||
|
||||
queueLogging(logQueue, () => {
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("hasPaymentMethodInfo:", result, !result);
|
||||
debugLog("payment.isPia:", payment?.isPia, !result);
|
||||
debugLog("payment.piaType:", payment?.piaType, !result);
|
||||
debugLog("", null, !result);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
716
src/helpers/page-prerequisites-helper.spec.js
Normal file
716
src/helpers/page-prerequisites-helper.spec.js
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
import * as pagePrereqsHelper from "@/helpers/page-prerequisites-helper";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import { coverageStatus } from "@/constants/insurance";
|
||||
|
||||
jest.mock("@/helpers/debug-log-helper.js", () => ({
|
||||
debugLog: jest.fn(),
|
||||
}));
|
||||
|
||||
const { debugLog } = require("@/helpers/debug-log-helper.js");
|
||||
|
||||
describe("page-prerequisites-helper.js", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("logPagePrereqsStart", () => {
|
||||
test("calls debugLog with source and forceLog when preReqResult is false", () => {
|
||||
pagePrereqsHelper.logPagePrereqsStart("payment.vue", false);
|
||||
|
||||
expect(debugLog).toHaveBeenCalledWith(
|
||||
"--- payment.vue pagePrereqs start ---",
|
||||
null,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("calls debugLog with forceLog false when preReqResult is true", () => {
|
||||
pagePrereqsHelper.logPagePrereqsStart("payment.vue", true);
|
||||
|
||||
expect(debugLog).toHaveBeenCalledWith(
|
||||
"--- payment.vue pagePrereqs start ---",
|
||||
null,
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("flushPagePrereqsLogs", () => {
|
||||
test("calls logPagePrereqsStart, executes logQueue, and logPagePrereqsEnd", () => {
|
||||
const logQueue = [jest.fn(), jest.fn()];
|
||||
|
||||
pagePrereqsHelper.flushPagePrereqsLogs("payment.vue", true, logQueue);
|
||||
|
||||
expect(debugLog).toHaveBeenCalledWith(
|
||||
"--- payment.vue pagePrereqs start ---",
|
||||
null,
|
||||
false
|
||||
);
|
||||
expect(debugLog).toHaveBeenCalledWith(
|
||||
"--- payment.vue pagePrereqs end ---",
|
||||
null,
|
||||
false
|
||||
);
|
||||
expect(logQueue[0]).toHaveBeenCalled();
|
||||
expect(logQueue[1]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("logPagePrereqsEnd", () => {
|
||||
test("calls debugLog with source and forceLog when preReqResult is false", () => {
|
||||
pagePrereqsHelper.logPagePrereqsEnd("payment-method.vue", false);
|
||||
|
||||
expect(debugLog).toHaveBeenCalledWith(
|
||||
"--- payment-method.vue pagePrereqs end ---",
|
||||
null,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("calls debugLog with forceLog false when preReqResult is true", () => {
|
||||
pagePrereqsHelper.logPagePrereqsEnd("payment-method.vue", true);
|
||||
|
||||
expect(debugLog).toHaveBeenCalledWith(
|
||||
"--- payment-method.vue pagePrereqs end ---",
|
||||
null,
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasServiceZipInfo", () => {
|
||||
test("returns true when zipCode and zipCodeCtu are set", () => {
|
||||
const order = {
|
||||
serviceLocation: { zipCode: "12345", zipCodeCtu: "12345" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceZipInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when zipCode is missing", () => {
|
||||
const order = {
|
||||
serviceLocation: { zipCode: null, zipCodeCtu: "12345" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceZipInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when zipCodeCtu is missing", () => {
|
||||
const order = {
|
||||
serviceLocation: { zipCode: "12345", zipCodeCtu: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceZipInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when both are empty strings", () => {
|
||||
const order = {
|
||||
serviceLocation: { zipCode: "", zipCodeCtu: "" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceZipInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("does not add to logQueue when logQueue is null", () => {
|
||||
const order = {
|
||||
serviceLocation: { zipCode: "12345", zipCodeCtu: "12345" },
|
||||
};
|
||||
|
||||
pagePrereqsHelper.hasServiceZipInfo(order, null);
|
||||
|
||||
expect(debugLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("adds log functions to logQueue when logQueue is provided", () => {
|
||||
const order = {
|
||||
serviceLocation: { zipCode: "12345", zipCodeCtu: "12345" },
|
||||
};
|
||||
const logQueue = [];
|
||||
|
||||
pagePrereqsHelper.hasServiceZipInfo(order, logQueue);
|
||||
|
||||
expect(logQueue).toHaveLength(1);
|
||||
logQueue[0]();
|
||||
expect(debugLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasServiceLocationInfo", () => {
|
||||
const baseOrder = {
|
||||
serviceLocation: {
|
||||
provider: {
|
||||
address: {
|
||||
streetAddress: "123 Provider St",
|
||||
city: "ProviderCity",
|
||||
state: "OH",
|
||||
zipCode: "00000",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("returns true for mobile when address, city, state, zipCode are set", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
address: "123 Main St",
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "12345",
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
provider: baseOrder.serviceLocation.provider,
|
||||
},
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceLocationInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for mobile when address is missing", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
address: null,
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "12345",
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
provider: baseOrder.serviceLocation.provider,
|
||||
},
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceLocationInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for drop-off when provider address has all fields", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
appointmentType: AppointmentTypeStrings.DROP_OFF,
|
||||
},
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceLocationInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for drop-off when provider streetAddress is missing", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
appointmentType: AppointmentTypeStrings.DROP_OFF,
|
||||
provider: {
|
||||
address: {
|
||||
streetAddress: null,
|
||||
city: "ProviderCity",
|
||||
state: "OH",
|
||||
zipCode: "00000",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceLocationInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for in-shop when provider address has all fields", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
},
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasServiceLocationInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("does not add to logQueue when logQueue is null", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
address: "123",
|
||||
city: "City",
|
||||
state: "OH",
|
||||
zipCode: "12345",
|
||||
provider: baseOrder.serviceLocation.provider,
|
||||
},
|
||||
};
|
||||
|
||||
pagePrereqsHelper.hasServiceLocationInfo(order, null);
|
||||
|
||||
expect(debugLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("adds log functions to logQueue when logQueue is provided", () => {
|
||||
const order = {
|
||||
...baseOrder,
|
||||
serviceLocation: {
|
||||
...baseOrder.serviceLocation,
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
address: "123",
|
||||
city: "City",
|
||||
state: "OH",
|
||||
zipCode: "12345",
|
||||
provider: baseOrder.serviceLocation.provider,
|
||||
},
|
||||
};
|
||||
const logQueue = [];
|
||||
|
||||
pagePrereqsHelper.hasServiceLocationInfo(order, logQueue);
|
||||
|
||||
expect(logQueue).toHaveLength(1);
|
||||
expect(typeof logQueue[0]).toBe("function");
|
||||
logQueue[0]();
|
||||
expect(debugLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasInsuranceInfo", () => {
|
||||
test("returns false when isInsurance is null", () => {
|
||||
const order = { payment: { isInsurance: null } };
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true when isInsurance is false (cash selected)", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: false,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.PENDING },
|
||||
},
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when isInsurance is true and coverage is not verified", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.PENDING },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: "POL123" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when isInsurance is true and coverage is verified with valid deductible", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: "POL123", currentDeductible: 100 },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when coverage is verified and deductible is 0", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: "POL123", currentDeductible: 0 },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when isInsurance is true but parentAccountNumber is CASH_PARENT_ACCOUNT_NUMBER", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.PENDING },
|
||||
parentAccountNumber: 167132,
|
||||
},
|
||||
policy: { policyNumber: "POL123" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when isInsurance is true but policyNumber is missing", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.PENDING },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when coverage is verified but deductible is null", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: "POL123", currentDeductible: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when coverage is verified but deductible is undefined", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: "POL123", currentDeductible: undefined },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when coverage is verified but deductible is negative", () => {
|
||||
const order = {
|
||||
payment: {
|
||||
isInsurance: true,
|
||||
insuranceCoverage: { coverageStatus: coverageStatus.VERIFIED },
|
||||
parentAccountNumber: 999999,
|
||||
},
|
||||
policy: { policyNumber: "POL123", currentDeductible: -1 },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasInsuranceInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("adds log functions to logQueue when logQueue is provided", () => {
|
||||
const order = { payment: { isInsurance: null } };
|
||||
const logQueue = [];
|
||||
|
||||
pagePrereqsHelper.hasInsuranceInfo(order, logQueue);
|
||||
|
||||
expect(logQueue).toHaveLength(1);
|
||||
logQueue[0]();
|
||||
expect(debugLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasSchedulingInfo", () => {
|
||||
const validSchedule = {
|
||||
date: "2024-01-15",
|
||||
startTime: "09:00",
|
||||
endTime: "17:00",
|
||||
jobMinMinutes: "30",
|
||||
jobMaxMinutes: "45",
|
||||
};
|
||||
|
||||
test("returns true when all schedule fields are set", () => {
|
||||
const order = { schedule: validSchedule };
|
||||
|
||||
const result = pagePrereqsHelper.hasSchedulingInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when date is missing", () => {
|
||||
const order = {
|
||||
schedule: { ...validSchedule, date: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasSchedulingInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when startTime is missing", () => {
|
||||
const order = {
|
||||
schedule: { ...validSchedule, startTime: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasSchedulingInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when jobMinMinutes is missing", () => {
|
||||
const order = {
|
||||
schedule: { ...validSchedule, jobMinMinutes: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasSchedulingInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("does not add to logQueue when logQueue is null", () => {
|
||||
const order = { schedule: validSchedule };
|
||||
|
||||
pagePrereqsHelper.hasSchedulingInfo(order, null);
|
||||
|
||||
expect(debugLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("adds log functions to logQueue when logQueue is provided", () => {
|
||||
const order = { schedule: validSchedule };
|
||||
const logQueue = [];
|
||||
|
||||
pagePrereqsHelper.hasSchedulingInfo(order, logQueue);
|
||||
|
||||
expect(logQueue).toHaveLength(1);
|
||||
logQueue[0]();
|
||||
expect(debugLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasCustomerInfo", () => {
|
||||
const validCustomer = {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
phoneNumber: "555-555-5555",
|
||||
emailAddress: "john@example.com",
|
||||
};
|
||||
|
||||
test("returns true when all customer fields are set", () => {
|
||||
const order = { customer: validCustomer };
|
||||
|
||||
const result = pagePrereqsHelper.hasCustomerInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when firstName is missing", () => {
|
||||
const order = {
|
||||
customer: { ...validCustomer, firstName: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasCustomerInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when emailAddress is missing", () => {
|
||||
const order = {
|
||||
customer: { ...validCustomer, emailAddress: "" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasCustomerInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when phoneNumber is empty", () => {
|
||||
const order = {
|
||||
customer: { ...validCustomer, phoneNumber: "" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasCustomerInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("does not add to logQueue when logQueue is null", () => {
|
||||
const order = { customer: validCustomer };
|
||||
|
||||
pagePrereqsHelper.hasCustomerInfo(order, null);
|
||||
|
||||
expect(debugLog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasGlassPartsOrRepairInfo", () => {
|
||||
test("returns true when isRepair is true", () => {
|
||||
const order = {
|
||||
damage: { isRepair: true },
|
||||
lineItems: { glassParts: [] },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when glassParts has items", () => {
|
||||
const order = {
|
||||
damage: { isRepair: false },
|
||||
lineItems: { glassParts: [{ id: "1" }] },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when isRepair is false and glassParts is empty", () => {
|
||||
const order = {
|
||||
damage: { isRepair: false },
|
||||
lineItems: { glassParts: [] },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when isRepair is false and glassParts is null", () => {
|
||||
const order = {
|
||||
damage: { isRepair: false },
|
||||
lineItems: { glassParts: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when damage and lineItems are undefined", () => {
|
||||
const order = {};
|
||||
|
||||
const result = pagePrereqsHelper.hasGlassPartsOrRepairInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("does not add to logQueue when logQueue is null", () => {
|
||||
const order = {
|
||||
damage: { isRepair: true },
|
||||
lineItems: { glassParts: [] },
|
||||
};
|
||||
|
||||
pagePrereqsHelper.hasGlassPartsOrRepairInfo(order, null);
|
||||
|
||||
expect(debugLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("adds log functions to logQueue when logQueue is provided", () => {
|
||||
const order = {
|
||||
damage: { isRepair: true },
|
||||
lineItems: { glassParts: [] },
|
||||
};
|
||||
const logQueue = [];
|
||||
|
||||
pagePrereqsHelper.hasGlassPartsOrRepairInfo(order, logQueue);
|
||||
|
||||
expect(logQueue).toHaveLength(1);
|
||||
logQueue[0]();
|
||||
expect(debugLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasPaymentMethodInfo", () => {
|
||||
test("returns true when isPia is true and piaType is set", () => {
|
||||
const order = {
|
||||
payment: { isPia: true, piaType: "Afterpay" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasPaymentMethodInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when isPia is false and piaType is set", () => {
|
||||
const order = {
|
||||
payment: { isPia: false, piaType: "PayLater" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasPaymentMethodInfo(order);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when isPia is null", () => {
|
||||
const order = {
|
||||
payment: { isPia: null, piaType: "Afterpay" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasPaymentMethodInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when isPia is false and piaType is null", () => {
|
||||
const order = {
|
||||
payment: { isPia: false, piaType: null },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasPaymentMethodInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when isPia is false and piaType is empty string", () => {
|
||||
const order = {
|
||||
payment: { isPia: false, piaType: "" },
|
||||
};
|
||||
|
||||
const result = pagePrereqsHelper.hasPaymentMethodInfo(order);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("does not add to logQueue when logQueue is null", () => {
|
||||
const order = { payment: { isPia: true, piaType: "Afterpay" } };
|
||||
|
||||
pagePrereqsHelper.hasPaymentMethodInfo(order, null);
|
||||
|
||||
expect(debugLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("adds log functions to logQueue when logQueue is provided", () => {
|
||||
const order = { payment: { isPia: true, piaType: "Afterpay" } };
|
||||
const logQueue = [];
|
||||
|
||||
pagePrereqsHelper.hasPaymentMethodInfo(order, logQueue);
|
||||
|
||||
expect(logQueue).toHaveLength(1);
|
||||
logQueue[0]();
|
||||
expect(debugLog).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/helpers/vehicle-helper.js
Normal file
11
src/helpers/vehicle-helper.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import store from "@/store";
|
||||
|
||||
export function hasVehicleInfo() {
|
||||
return (
|
||||
store.getters.order?.vehicle.year &&
|
||||
store.getters.order?.vehicle?.make &&
|
||||
store.getters.order?.vehicle?.model &&
|
||||
store.getters.order?.vehicle?.style &&
|
||||
store.getters.order?.vehicle?.carId
|
||||
);
|
||||
}
|
||||
|
|
@ -470,7 +470,8 @@ export default {
|
|||
}
|
||||
},
|
||||
donationValues() {
|
||||
return [1, 3, 5];
|
||||
var donationAmounts = this.getCmsContent("DonationAmountsWidget", "Text");
|
||||
return donationAmounts.split(",").map(Number);
|
||||
},
|
||||
lineItemsWithoutDonation() {
|
||||
const lineItemsClone = deepClone(this.lineItems);
|
||||
|
|
@ -531,10 +532,11 @@ export default {
|
|||
window.location.assign(location.protocol + "//" + location.host);
|
||||
},
|
||||
async addDonationToOrder() {
|
||||
const selectedItemNumber = this.donationValues.indexOf(Number(this.donationAmount));
|
||||
const donationResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.SUBMIT_DONATION_PART_TO_SV2,
|
||||
{
|
||||
donationAmount: parseInt(this.donationAmount),
|
||||
selectedItemNumber: selectedItemNumber,
|
||||
submittedOrder: this.submittedOrder,
|
||||
},
|
||||
false
|
||||
|
|
|
|||
|
|
@ -89,18 +89,53 @@ describe("mobile-details.vue", () => {
|
|||
// Assert
|
||||
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
|
||||
});
|
||||
test("arePagePrerequisitesValid should be true ", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
describe("arePagePrerequisitesValid", () => {
|
||||
test("returns true when all prerequisites are valid", () => {
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
//Act
|
||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
test("returns falsy when service zip info is missing", () => {
|
||||
store.getters.order.serviceLocation.zipCode = null;
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
//Assert
|
||||
expect(arePagePrerequisitesValid).toBe(true);
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test("returns false when appointment type is not mobile", () => {
|
||||
store.getters.order.serviceLocation.appointmentType = "Inshop";
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when scheduling info is missing", () => {
|
||||
store.getters.order.schedule.date = null;
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("if the back button is clicked, navigate back", async () => {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ import { settleAllPromises } from "@/helpers/layout-helper";
|
|||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
import { flushPagePrereqsLogs, hasSchedulingInfo } from "@/helpers/page-prerequisites-helper";
|
||||
export default {
|
||||
name: "MobileDetails",
|
||||
data() {
|
||||
|
|
@ -110,6 +111,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
const logQueue = [];
|
||||
const order = store.getters.order;
|
||||
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const serviceLocationPreReqs =
|
||||
serviceLocation.zipCode &&
|
||||
|
|
@ -118,16 +122,12 @@ export default {
|
|||
serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
const scheduleInfoPreReqs = hasSchedulingInfo(order, logQueue);
|
||||
|
||||
const preReqResult = serviceLocationPreReqs && scheduleInfoPreReqs;
|
||||
|
||||
flushPagePrereqsLogs("mobile-details.vue", preReqResult, logQueue);
|
||||
|
||||
const preReqResult = serviceLocationPreReqs && scheduleReqs;
|
||||
return preReqResult;
|
||||
},
|
||||
getServiceAddressFromStore() {
|
||||
|
|
|
|||
214
src/layouts/payment-adyen/payment-adyen.spec.js
Normal file
214
src/layouts/payment-adyen/payment-adyen.spec.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import paymentAdyen from "@/layouts/payment-adyen/payment-adyen.vue";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper";
|
||||
import store from "@/store";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
|
||||
jest.mock("@/helpers/pricing-helper.js", () => ({
|
||||
getAmountDue: jest.fn().mockReturnValue(100),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||
fetchCmsContentForPage: () => Promise.resolve("content"),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/layout-helper", () => ({
|
||||
settleAllPromises: jest.fn().mockResolvedValue({ cmsContent: "content" }),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/loading-modal-helper", () => ({
|
||||
showFmgLoadingModal: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
|
||||
submitWorkOrder: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/debug-log-helper.js", () => ({
|
||||
debugLog: jest.fn(),
|
||||
}));
|
||||
|
||||
// Component that skips Adyen initialization in mounted (only needed for arePagePrerequisitesValid tests)
|
||||
const PaymentAdyenTestComponent = {
|
||||
...paymentAdyen,
|
||||
mounted() {
|
||||
// Stub - skip initializeAdyen to avoid Adyen/API setup
|
||||
},
|
||||
};
|
||||
|
||||
const createValidOrder = () => ({
|
||||
vehicle: {
|
||||
year: "2020",
|
||||
make: "acura",
|
||||
model: "mdx",
|
||||
carId: "dummyCarId",
|
||||
},
|
||||
serviceLocation: {
|
||||
address: "123 Main St",
|
||||
address2: "",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43235",
|
||||
zipCodeCtu: "43235",
|
||||
appointmentType: AppointmentTypeStrings.IN_SHOP,
|
||||
provider: {
|
||||
address: {
|
||||
streetAddress: "456 Provider St",
|
||||
city: "Columbus",
|
||||
state: "OH",
|
||||
zipCode: "43235",
|
||||
zipCodeCtu: "43235",
|
||||
},
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
emailAddress: "john.doe@example.com",
|
||||
phoneNumber: "555-555-5555",
|
||||
},
|
||||
payment: {
|
||||
isInsurance: false,
|
||||
isPia: true,
|
||||
piaType: "CREDIT_CARD",
|
||||
insuranceCoverage: {},
|
||||
},
|
||||
policy: {
|
||||
currentDeductible: 0,
|
||||
policyNumber: null,
|
||||
},
|
||||
schedule: {
|
||||
date: "2024-01-15",
|
||||
startTime: "09:00",
|
||||
endTime: "10:00",
|
||||
jobMinMinutes: "30",
|
||||
jobMaxMinutes: "45",
|
||||
},
|
||||
workOrderNumber: "WO-123456",
|
||||
referralCorrelationId: "ref-123",
|
||||
referralSequenceNumber: 1,
|
||||
damage: { isRepair: false },
|
||||
lineItems: {
|
||||
glassParts: [{ id: "part1", partType: "WINDSHIELD" }],
|
||||
},
|
||||
});
|
||||
|
||||
describe("payment-adyen.vue", () => {
|
||||
beforeEach(() => {
|
||||
store.getters = {
|
||||
order: createValidOrder(),
|
||||
damage: {},
|
||||
lineItems: { glassParts: [] },
|
||||
payment: {},
|
||||
policy: {},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("arePagePrerequisitesValid", () => {
|
||||
test("returns true when all prerequisites are valid", () => {
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when service location info is missing", () => {
|
||||
store.getters.order.serviceLocation.address = null;
|
||||
store.getters.order.serviceLocation.provider.address.streetAddress = null;
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when insurance info is invalid", () => {
|
||||
store.getters.order.payment.isInsurance = null;
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when scheduling info is missing", () => {
|
||||
store.getters.order.schedule.date = null;
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when customer info is missing", () => {
|
||||
store.getters.order.customer.firstName = null;
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when payment method info is invalid", () => {
|
||||
store.getters.order.payment.isPia = null;
|
||||
store.getters.order.payment.piaType = null;
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when glass parts or repair info is missing", () => {
|
||||
store.getters.order.damage.isRepair = false;
|
||||
store.getters.order.lineItems.glassParts = [];
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for mobile appointment type when address fields are present", () => {
|
||||
store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
store.getters.order.serviceLocation.address = "123 Mobile St";
|
||||
store.getters.order.serviceLocation.city = "Columbus";
|
||||
store.getters.order.serviceLocation.state = "OH";
|
||||
store.getters.order.serviceLocation.zipCode = "43235";
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ customMountOptions } = {}) {
|
||||
const mountOptions = getMountOptions({
|
||||
...customMountOptions,
|
||||
store,
|
||||
route: { name: "payment-adyen" },
|
||||
router: {
|
||||
navigateWithSaving: jest.fn(),
|
||||
navigateWithoutSaving: jest.fn(),
|
||||
navigateWithPageData: jest.fn(),
|
||||
},
|
||||
});
|
||||
mountOptions.global.mocks["navigationScenarios"] = {};
|
||||
mountOptions.mixins = [
|
||||
{
|
||||
methods: {
|
||||
getCmsContent: jest.fn(),
|
||||
setCmsContent: jest.fn(),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return shallowMount(PaymentAdyenTestComponent, mountOptions);
|
||||
}
|
||||
|
|
@ -83,7 +83,15 @@ import cart from "@/fmg-components/cart/cart";
|
|||
import { coverageStatus } from "@/constants/insurance";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import store from "@/store";
|
||||
import { debugLog } from "@/helpers/debug-log-helper.js";
|
||||
import {
|
||||
flushPagePrereqsLogs,
|
||||
hasServiceLocationInfo,
|
||||
hasInsuranceInfo,
|
||||
hasSchedulingInfo,
|
||||
hasCustomerInfo,
|
||||
hasPaymentMethodInfo,
|
||||
hasGlassPartsOrRepairInfo,
|
||||
} from "@/helpers/page-prerequisites-helper.js";
|
||||
|
||||
export default {
|
||||
name: "payment-adyen",
|
||||
|
|
@ -136,110 +144,19 @@ export default {
|
|||
},
|
||||
|
||||
arePagePrerequisitesValid() {
|
||||
// Service Location
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const mobileReqs = !!(
|
||||
serviceLocation.address &&
|
||||
serviceLocation.city &&
|
||||
serviceLocation.state &&
|
||||
serviceLocation.zipCode
|
||||
);
|
||||
|
||||
const providerLocation = serviceLocation.provider.address;
|
||||
const dropOffInshopReqs = !!(
|
||||
providerLocation.streetAddress &&
|
||||
providerLocation.city &&
|
||||
providerLocation.state &&
|
||||
providerLocation.zipCode
|
||||
);
|
||||
|
||||
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
|
||||
const serviceLocationReqs =
|
||||
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
|
||||
|
||||
// Insurance
|
||||
const isInsuranceSet = store.getters.order.payment.isInsurance !== null;
|
||||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
|
||||
// Customer
|
||||
const customer = store.getters.order.customer;
|
||||
const customerReqs = !!(
|
||||
customer.firstName &&
|
||||
customer.lastName &&
|
||||
customer.phoneNumber &&
|
||||
customer.emailAddress
|
||||
);
|
||||
|
||||
const paymentMethodReqs =
|
||||
store.getters.order.payment.isPia !== null &&
|
||||
(store.getters.order.payment.isPia || !!store.getters.order.payment.piaType);
|
||||
|
||||
const preReqResult =
|
||||
serviceLocationReqs &&
|
||||
isInsuranceSet &&
|
||||
scheduleReqs &&
|
||||
customerReqs &&
|
||||
paymentMethodReqs;
|
||||
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("--- payment-adyen.vue pagePrereqs start ---", null, !preReqResult);
|
||||
debugLog("mobileReqs:", mobileReqs, !preReqResult);
|
||||
debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult);
|
||||
debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult);
|
||||
debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult);
|
||||
debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult);
|
||||
debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult);
|
||||
debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult);
|
||||
debugLog("providerLocation.city:", providerLocation.city, !preReqResult);
|
||||
debugLog("providerLocation.state:", providerLocation.state, !preReqResult);
|
||||
debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("scheduleReqs:", scheduleReqs, !preReqResult);
|
||||
debugLog("schedule.date:", schedule.date, !preReqResult);
|
||||
debugLog("schedule.startTime:", schedule.startTime, !preReqResult);
|
||||
debugLog("schedule.endTime:", schedule.endTime, !preReqResult);
|
||||
debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult);
|
||||
debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("customerReqs:", customerReqs, !preReqResult);
|
||||
debugLog("customer.firstName:", customer.firstName, !preReqResult);
|
||||
debugLog("customer.lastName:", customer.lastName, !preReqResult);
|
||||
debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult);
|
||||
debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult);
|
||||
debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult);
|
||||
debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult);
|
||||
|
||||
debugLog("--- payment-adyen.vue pagePrereqs end ---", null, !preReqResult);
|
||||
}
|
||||
|
||||
return preReqResult;
|
||||
const order = store.getters.order;
|
||||
const logQueue = [];
|
||||
const results = [
|
||||
hasServiceLocationInfo(order, logQueue),
|
||||
hasInsuranceInfo(order, logQueue),
|
||||
hasSchedulingInfo(order, logQueue),
|
||||
hasCustomerInfo(order, logQueue),
|
||||
hasPaymentMethodInfo(order, logQueue),
|
||||
hasGlassPartsOrRepairInfo(order, logQueue),
|
||||
];
|
||||
const result = results.every(Boolean);
|
||||
flushPagePrereqsLogs("payment-adyen.vue", result, logQueue);
|
||||
return result;
|
||||
},
|
||||
|
||||
async initializeAdyen() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import store from "@/store";
|
|||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import globalMethods from "@/global-methods";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
|
||||
globalMethods.callHttpClient = jest.fn();
|
||||
|
||||
|
|
@ -29,8 +30,64 @@ jest.mock("@/helpers/pricing-helper.js", () => ({
|
|||
getAmountDue: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/debug-log-helper.js", () => ({
|
||||
debugLog: jest.fn(),
|
||||
}));
|
||||
|
||||
let piaDisabledFlag = false;
|
||||
|
||||
const createValidOrderForPaymentMethod = () => ({
|
||||
payment: {
|
||||
isPia: false,
|
||||
isInsurance: false,
|
||||
insuranceCoverage: { isVerified: true },
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: [{ id: "part1", partType: "WINDSHIELD" }],
|
||||
supportingItems: [],
|
||||
vaps: [],
|
||||
promos: [],
|
||||
},
|
||||
policy: {
|
||||
currentDeductible: 123,
|
||||
isNoComp: false,
|
||||
isItac: false,
|
||||
},
|
||||
serviceLocation: {
|
||||
appointmentType: AppointmentTypeStrings.MOBILE,
|
||||
address: "123 Test Ave",
|
||||
address2: "",
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "00000",
|
||||
zipCodeCtu: "00000",
|
||||
provider: {
|
||||
address: {
|
||||
streetAddress: "456 Provider St",
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "00000",
|
||||
zipCodeCtu: "00000",
|
||||
},
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
emailAddress: "john@example.com",
|
||||
phoneNumber: "555-555-5555",
|
||||
},
|
||||
schedule: {
|
||||
date: "2024-01-15",
|
||||
startTime: "09:00",
|
||||
endTime: "10:00",
|
||||
jobMinMinutes: "30",
|
||||
jobMaxMinutes: "45",
|
||||
},
|
||||
vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" },
|
||||
damage: { isRepair: false },
|
||||
});
|
||||
|
||||
describe("payment-method.vue", () => {
|
||||
describe("navigation", () => {
|
||||
test("if the back button is clicked, navigate back", async () => {
|
||||
|
|
@ -54,6 +111,63 @@ describe("payment-method.vue", () => {
|
|||
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("arePagePrerequisitesValid", () => {
|
||||
test("returns true when all prerequisites are valid", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when service location info is missing", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
store.getters.order.serviceLocation.address = null;
|
||||
store.getters.order.serviceLocation.provider.address.streetAddress = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when insurance info is invalid", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
store.getters.order.payment.isInsurance = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when scheduling info is missing", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
store.getters.order.schedule.date = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when customer info is missing", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
store.getters.order.customer.firstName = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when glass parts or repair info is missing", () => {
|
||||
const { wrapper } = setupMocks();
|
||||
store.getters.order.damage.isRepair = false;
|
||||
store.getters.order.lineItems.glassParts = [];
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks() {
|
||||
|
|
@ -65,34 +179,9 @@ function setupMocks() {
|
|||
vaps: [],
|
||||
promos: [],
|
||||
},
|
||||
order: {
|
||||
payment: {
|
||||
isPia: false,
|
||||
insuranceCoverage: {
|
||||
isVerified: true,
|
||||
},
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: [],
|
||||
supportingItems: [],
|
||||
vaps: [],
|
||||
promos: [],
|
||||
},
|
||||
policy: {
|
||||
currentDeductible: 123,
|
||||
isNoComp: false,
|
||||
isItac: false,
|
||||
},
|
||||
serviceLocation: {
|
||||
appointmentType: "Mobile",
|
||||
address: "123 Test Ave",
|
||||
city: "Anytown",
|
||||
state: "OH",
|
||||
zipCode: "00000",
|
||||
provider: { providerNumber: "0000567" },
|
||||
},
|
||||
vehicle: { year: 2020, make: "Toyota", model: "Camry", carId: "12345" },
|
||||
damage: { isRepair: false },
|
||||
order: createValidOrderForPaymentMethod(),
|
||||
applicationUser: {
|
||||
experiments: [],
|
||||
},
|
||||
policy: {
|
||||
currentDeductible: 123,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,17 @@
|
|||
v-bind:isDismissible="false" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<alert
|
||||
ref="UnverifiedInsuranceAlert"
|
||||
class="mt-5 mb-5"
|
||||
cmsWidgetName="UnverifiedInsuranceAlertWidget"
|
||||
v-if="shouldDisplayUnverifiedInsuranceAlert"
|
||||
:showInfoIcon="true"
|
||||
:showHorizontalRow="true"
|
||||
alertClass="alert-info" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isRecalibrationOnOrder && shouldHideRecalibration"
|
||||
class="questions-about-service mt-5">
|
||||
|
|
@ -167,7 +178,14 @@ import { containsRecalParts } from "@/helpers/recal-helper";
|
|||
import { getBoolFromString } from "@/helpers/boolean-helper";
|
||||
import { getAmountDue, addPricesToLineItems } from "@/helpers/pricing-helper.js";
|
||||
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
import {
|
||||
flushPagePrereqsLogs,
|
||||
hasServiceLocationInfo,
|
||||
hasInsuranceInfo,
|
||||
hasSchedulingInfo,
|
||||
hasCustomerInfo,
|
||||
hasGlassPartsOrRepairInfo,
|
||||
} from "@/helpers/page-prerequisites-helper.js";
|
||||
import { ErrorMessage } from "vee-validate";
|
||||
import { Field } from "vee-validate";
|
||||
import experimentMixin from "../../mixins/experiment-mixin";
|
||||
|
|
@ -311,6 +329,8 @@ export default {
|
|||
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: lineItemsForCart,
|
||||
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -368,91 +388,19 @@ export default {
|
|||
this.$router.navigateWithoutSaving(scenarioName, this.pageName);
|
||||
},
|
||||
arePagePrerequisitesValid() {
|
||||
// Service Location
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const mobileReqs = !!(
|
||||
serviceLocation.address &&
|
||||
serviceLocation.city &&
|
||||
serviceLocation.state &&
|
||||
serviceLocation.zipCode
|
||||
);
|
||||
|
||||
const providerLocation = serviceLocation.provider.address;
|
||||
const dropOffInshopReqs = !!(
|
||||
providerLocation.streetAddress &&
|
||||
providerLocation.city &&
|
||||
providerLocation.state &&
|
||||
providerLocation.zipCode
|
||||
);
|
||||
|
||||
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
|
||||
const serviceLocationReqs =
|
||||
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
|
||||
|
||||
// Insurance
|
||||
const isInsurance = store.getters.order.payment.isInsurance;
|
||||
const insuranceCoverageStatus = store.getters.payment.insuranceCoverage.coverageStatus;
|
||||
const currentDeductible = store.getters.policy.currentDeductible;
|
||||
const isInsuranceSet = () => {
|
||||
if (isInsurance !== null) {
|
||||
if (
|
||||
insuranceCoverageStatus === coverageStatus.VERIFIED &&
|
||||
!(currentDeductible === 0 || currentDeductible > 0)
|
||||
) {
|
||||
return false; // if coverageStatus is verified there must be a valid deductible also
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
|
||||
// Customer
|
||||
const customer = store.getters.order.customer;
|
||||
const customerReqs = !!(
|
||||
customer.firstName &&
|
||||
customer.lastName &&
|
||||
customer.phoneNumber &&
|
||||
customer.emailAddress
|
||||
);
|
||||
|
||||
const preReqResult =
|
||||
serviceLocationReqs && isInsuranceSet() && scheduleReqs && customerReqs;
|
||||
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("--- payment-method.vue pagePrereqs start ---", null, !preReqResult);
|
||||
debugLog("serviceLocationRequs::isMobile:", isMobile, !preReqResult);
|
||||
debugLog("serviceLocationReqs::mobileReqs:", mobileReqs, !preReqResult);
|
||||
debugLog("serviceLocationReqs::dropOffInshopReqs:", dropOffInshopReqs, !preReqResult);
|
||||
debugLog("serviceLocationReqs:: result", serviceLocationReqs, !preReqResult);
|
||||
|
||||
debugLog("isInsuranceSet::", isInsuranceSet(), !preReqResult);
|
||||
|
||||
debugLog("scheduleReqs::schedule.date:", schedule.date, !preReqResult);
|
||||
debugLog("scheduleReqs::schedule.startTime:", schedule.startTime, !preReqResult);
|
||||
debugLog("scheduleReqs::schedule.endTime:", schedule.endTime, !preReqResult);
|
||||
debugLog("scheduleReqs::schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult);
|
||||
debugLog("scheduleReqs::schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult);
|
||||
|
||||
debugLog("customerReqs::customer.firstName:", customer.firstName, !preReqResult);
|
||||
debugLog("customerReqs::customer.lastName:", customer.lastName, !preReqResult);
|
||||
debugLog("customerReqs::customer.phoneNumber:", customer.phoneNumber, !preReqResult);
|
||||
debugLog("customerReqs::customer.emailAddress:", customer.emailAddress, !preReqResult);
|
||||
debugLog("--- payment-method.vue pagePrereqs end ---", null, !preReqResult);
|
||||
}
|
||||
|
||||
return preReqResult;
|
||||
const order = store.getters.order;
|
||||
const logQueue = [];
|
||||
const results = [
|
||||
hasServiceLocationInfo(order, logQueue),
|
||||
hasInsuranceInfo(order, logQueue),
|
||||
hasSchedulingInfo(order, logQueue),
|
||||
hasCustomerInfo(order, logQueue),
|
||||
hasGlassPartsOrRepairInfo(order, logQueue),
|
||||
];
|
||||
//false if any check is false but calls all checks for logging purposes
|
||||
const result = results.every(Boolean);
|
||||
flushPagePrereqsLogs("payment-method.vue", result, logQueue);
|
||||
return result;
|
||||
},
|
||||
getPaymentMethodFromStore() {
|
||||
const piaType = store.getters.order.payment.piaType;
|
||||
|
|
@ -515,6 +463,10 @@ export default {
|
|||
serviceLocationState: this.$store.getters.order.serviceLocation.state,
|
||||
serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: this.lineItems,
|
||||
serviceLocationAddressLine1:
|
||||
this.$store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2:
|
||||
this.$store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -639,6 +591,8 @@ export default {
|
|||
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||
serviceLocationZipCode: store.getters.order.serviceLocation.zipCode,
|
||||
pricedLineItems: this.lineItems,
|
||||
serviceLocationAddressLine1: store.getters.order.serviceLocation?.address,
|
||||
serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2,
|
||||
},
|
||||
"payment-method",
|
||||
false
|
||||
|
|
@ -865,6 +819,13 @@ export default {
|
|||
shouldDisplayPiaAlert() {
|
||||
return getBoolFromString(this.$route?.query?.piaError);
|
||||
},
|
||||
shouldDisplayUnverifiedInsuranceAlert() {
|
||||
const shouldDisplayUnverifiedDeduct = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_UNVERIFIED_DEDUCT_ENTRY,
|
||||
"true"
|
||||
);
|
||||
return shouldDisplayUnverifiedDeduct;
|
||||
},
|
||||
// Necessary to make the watcher of lineItems work
|
||||
// JavaScript does not keep a record of the old value, only a reference to it's location in the memory.
|
||||
// This creates a separate location for oldValue/newValue so they can be compared
|
||||
|
|
@ -1033,7 +994,15 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert.alert-info.widget-name-UnverifiedInsuranceAlertWidget {
|
||||
:deep(.alert-heading) {
|
||||
color: $black;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(span.label),
|
||||
:deep(span) {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
|
|
|
|||
|
|
@ -258,6 +258,52 @@ describe("payment.vue", () => {
|
|||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("Returns false when service location info is missing", () => {
|
||||
const wrapper = setupMocks({});
|
||||
store.getters.order.serviceLocation.provider.address.streetAddress = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("Returns false when insurance info is invalid", () => {
|
||||
const wrapper = setupMocks({});
|
||||
store.getters.order.payment.isInsurance = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("Returns false when scheduling info is missing", () => {
|
||||
const wrapper = setupMocks({});
|
||||
store.getters.order.schedule.date = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("Returns false when customer info is missing", () => {
|
||||
const wrapper = setupMocks({});
|
||||
store.getters.order.customer.firstName = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("Returns false when glass parts or repair info is missing", () => {
|
||||
const wrapper = setupMocks({});
|
||||
store.getters.order.damage.isRepair = false;
|
||||
store.getters.order.lineItems.glassParts = [];
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe("Payment method cases", () => {
|
||||
test("Returns false if pia options are not valid", () => {
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -235,7 +235,6 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
|||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { paymentMethods } from "@/constants/payment-method-constants";
|
||||
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
||||
|
|
@ -243,6 +242,15 @@ import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.
|
|||
import { coverageStatus } from "@/constants/insurance";
|
||||
import { getDisplayAmountDue, getAmountDue } from "@/helpers/pricing-helper.js";
|
||||
import { debugLog } from "@/helpers/debug-log-helper.js";
|
||||
import {
|
||||
flushPagePrereqsLogs,
|
||||
hasServiceLocationInfo,
|
||||
hasInsuranceInfo,
|
||||
hasSchedulingInfo,
|
||||
hasCustomerInfo,
|
||||
hasPaymentMethodInfo,
|
||||
hasGlassPartsOrRepairInfo,
|
||||
} from "@/helpers/page-prerequisites-helper.js";
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import paymentMethodListButton from "@/layouts/payment-method/payment-method-question/payment-method-list-button/payment-method-list-button";
|
||||
|
||||
|
|
@ -435,110 +443,19 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
// Service Location
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
const mobileReqs = !!(
|
||||
serviceLocation.address &&
|
||||
serviceLocation.city &&
|
||||
serviceLocation.state &&
|
||||
serviceLocation.zipCode
|
||||
);
|
||||
|
||||
const providerLocation = serviceLocation.provider.address;
|
||||
const dropOffInshopReqs = !!(
|
||||
providerLocation.streetAddress &&
|
||||
providerLocation.city &&
|
||||
providerLocation.state &&
|
||||
providerLocation.zipCode
|
||||
);
|
||||
|
||||
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||
|
||||
const serviceLocationReqs =
|
||||
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
|
||||
|
||||
// Insurance
|
||||
const isInsuranceSet = store.getters.order.payment.isInsurance !== null;
|
||||
|
||||
// Schedule
|
||||
const schedule = store.getters.order.schedule;
|
||||
const scheduleReqs = !!(
|
||||
schedule.date &&
|
||||
schedule.startTime &&
|
||||
schedule.endTime &&
|
||||
schedule.jobMaxMinutes &&
|
||||
schedule.jobMinMinutes
|
||||
);
|
||||
|
||||
// Customer
|
||||
const customer = store.getters.order.customer;
|
||||
const customerReqs = !!(
|
||||
customer.firstName &&
|
||||
customer.lastName &&
|
||||
customer.phoneNumber &&
|
||||
customer.emailAddress
|
||||
);
|
||||
|
||||
const paymentMethodReqs =
|
||||
store.getters.order.payment.isPia !== null &&
|
||||
(store.getters.order.payment.isPia || !!store.getters.order.payment.piaType);
|
||||
|
||||
const preReqResult =
|
||||
serviceLocationReqs &&
|
||||
isInsuranceSet &&
|
||||
scheduleReqs &&
|
||||
customerReqs &&
|
||||
paymentMethodReqs;
|
||||
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("--- payment.vue pagePrereqs start ---", null, !preReqResult);
|
||||
debugLog("mobileReqs:", mobileReqs, !preReqResult);
|
||||
debugLog("serviceLocation.address:", serviceLocation.address, !preReqResult);
|
||||
debugLog("serviceLocation.city:", serviceLocation.city, !preReqResult);
|
||||
debugLog("serviceLocation.state:", serviceLocation.state, !preReqResult);
|
||||
debugLog("serviceLocation.zipCode:", serviceLocation.zipCode, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("dropOffInshopReqs:", dropOffInshopReqs, !preReqResult);
|
||||
debugLog("serviceLocationReqs:", serviceLocationReqs, !preReqResult);
|
||||
debugLog("providerLocation.streetAddress:", providerLocation.streetAddress, !preReqResult);
|
||||
debugLog("providerLocation.city:", providerLocation.city, !preReqResult);
|
||||
debugLog("providerLocation.state:", providerLocation.state, !preReqResult);
|
||||
debugLog("providerLocation.zipCode:", providerLocation.zipCode, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("isInsuranceSet:", isInsuranceSet, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("scheduleReqs:", scheduleReqs, !preReqResult);
|
||||
debugLog("schedule.date:", schedule.date, !preReqResult);
|
||||
debugLog("schedule.startTime:", schedule.startTime, !preReqResult);
|
||||
debugLog("schedule.endTime:", schedule.endTime, !preReqResult);
|
||||
debugLog("schedule.jobMaxMinutes:", schedule.jobMaxMinutes, !preReqResult);
|
||||
debugLog("schedule.jobMinMinutes:", schedule.jobMinMinutes, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("customerReqs:", customerReqs, !preReqResult);
|
||||
debugLog("customer.firstName:", customer.firstName, !preReqResult);
|
||||
debugLog("customer.lastName:", customer.lastName, !preReqResult);
|
||||
debugLog("customer.phoneNumber:", customer.phoneNumber, !preReqResult);
|
||||
debugLog("customer.emailAddress:", customer.emailAddress, !preReqResult);
|
||||
|
||||
debugLog("", null, !preReqResult);
|
||||
|
||||
debugLog("paymentMethodReqs:", paymentMethodReqs, !preReqResult);
|
||||
debugLog("store.getters.order.payment.isPia:", store.getters.order?.payment?.isPia, !preReqResult);
|
||||
debugLog("store.getters.order.payment.piaType:", store.getters.order?.payment?.piaType, !preReqResult);
|
||||
|
||||
debugLog("--- payment.vue pagePrereqs end ---", null, !preReqResult);
|
||||
}
|
||||
|
||||
return preReqResult;
|
||||
const order = store.getters.order;
|
||||
const logQueue = [];
|
||||
const results = [
|
||||
hasServiceLocationInfo(order, logQueue),
|
||||
hasInsuranceInfo(order, logQueue),
|
||||
hasSchedulingInfo(order, logQueue),
|
||||
hasCustomerInfo(order, logQueue),
|
||||
hasPaymentMethodInfo(order, logQueue),
|
||||
hasGlassPartsOrRepairInfo(order, logQueue),
|
||||
];
|
||||
const result = results.every(Boolean);
|
||||
flushPagePrereqsLogs("payment.vue", result, logQueue);
|
||||
return result;
|
||||
},
|
||||
getAnswersNullSafe(widgetName) {
|
||||
const rawData = this.getCmsContent(widgetName, "Answers");
|
||||
|
|
|
|||
|
|
@ -295,6 +295,47 @@ describe("quote.vue", () => {
|
|||
|
||||
expect(arePagePrerequisitesValid).toBe(false);
|
||||
});
|
||||
|
||||
test("should fail arePagePrerequisitesValid when service zip info is missing", () => {
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: { glassParts: ["item"] },
|
||||
serviceLocation: {
|
||||
zipCode: null,
|
||||
zipCodeCtu: "value",
|
||||
},
|
||||
damage: { isRepair: false },
|
||||
payment: { insuranceCoverage: { isVerified: false } },
|
||||
referralNumber: "1234567",
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should fail arePagePrerequisitesValid when insurance coverage is verified", () => {
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: { glassParts: ["item"] },
|
||||
serviceLocation: {
|
||||
zipCode: "12345",
|
||||
zipCodeCtu: "value",
|
||||
},
|
||||
damage: { isRepair: false },
|
||||
payment: { insuranceCoverage: { isVerified: true } },
|
||||
referralNumber: "1234567",
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => {
|
||||
//Arrange
|
||||
store.getters = {
|
||||
|
|
|
|||
|
|
@ -183,6 +183,11 @@ import { routeData } from "@/router/constants/routes";
|
|||
import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stash";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
import {
|
||||
flushPagePrereqsLogs,
|
||||
hasServiceZipInfo,
|
||||
hasGlassPartsOrRepairInfo,
|
||||
} from "@/helpers/page-prerequisites-helper.js";
|
||||
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||
import { quotePageDiscountTable } from "../../constants/quote-page-discounts";
|
||||
|
||||
|
|
@ -728,28 +733,27 @@ export default {
|
|||
this.$refs[modalName].openModal();
|
||||
},
|
||||
arePagePrerequisitesValid() {
|
||||
const payment = store.getters.order.payment;
|
||||
const order = store.getters.order;
|
||||
const logQueue = [];
|
||||
const isVerifiedOk =
|
||||
order.payment?.insuranceCoverage?.isVerified == null ||
|
||||
order.payment?.insuranceCoverage?.isVerified === false;
|
||||
|
||||
const preReqResult =
|
||||
store.getters.order.serviceLocation.zipCode &&
|
||||
store.getters.order.serviceLocation.zipCodeCtu &&
|
||||
(store.getters.order.damage.isRepair ||
|
||||
(store.getters.order.lineItems?.glassParts != null &&
|
||||
store.getters.order.lineItems.glassParts.length > 0)) &&
|
||||
(payment.insuranceCoverage?.isVerified == null ||
|
||||
payment.insuranceCoverage?.isVerified === false);
|
||||
const results = [
|
||||
hasServiceZipInfo(order, logQueue),
|
||||
hasGlassPartsOrRepairInfo(order, logQueue),
|
||||
];
|
||||
const preReqResult = results.every(Boolean) && isVerifiedOk;
|
||||
|
||||
// prettier-ignore
|
||||
{
|
||||
debugLog("--- quote.vue pagePrereqs start ---", null, !preReqResult);
|
||||
debugLog("store.getters.order.serviceLocation.zipCode:", store.getters.order.serviceLocation?.zipCode, !preReqResult);
|
||||
debugLog("store.getters.order.serviceLocation.zipCodeCtu:", store.getters.order.serviceLocation?.zipCodeCtu, !preReqResult);
|
||||
debugLog("store.getters.order.damage.isRepair:", store.getters.order.damage?.isRepair, !preReqResult);
|
||||
debugLog("store.getters.order.lineItems?.glassParts:", store.getters.order.lineItems?.glassParts, !preReqResult);
|
||||
debugLog("store.getters.order.payemt.insuranceCoverage?.isVerified:", store.getters.order.payment?.insuranceCoverage?.isVerified, !preReqResult);
|
||||
debugLog("--- quote.vue pagePrereqs end ---", null, !preReqResult);
|
||||
}
|
||||
logQueue.push(() => {
|
||||
debugLog(
|
||||
"payment.insuranceCoverage?.isVerified:",
|
||||
order.payment?.insuranceCoverage?.isVerified,
|
||||
!preReqResult
|
||||
);
|
||||
});
|
||||
|
||||
flushPagePrereqsLogs("quote.vue", preReqResult, logQueue);
|
||||
return preReqResult;
|
||||
},
|
||||
vapsItemsSelectedAction(vapsItemsSelected) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
|
|||
return true;
|
||||
}
|
||||
},
|
||||
hasSetting(settingName) {
|
||||
if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
hasSettingEqualTo(settingName, settingValue) {
|
||||
return false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -163,6 +163,16 @@ export default {
|
|||
}));
|
||||
|
||||
this.$emit("currentNumberOfPackages", modifiedAnswers.length);
|
||||
|
||||
if (this.experimentPackageOrder()) {
|
||||
const orderedKeys = this.experimentPackageOrder()
|
||||
.split(",")
|
||||
.map((key) => key.trim());
|
||||
modifiedAnswers.sort(
|
||||
(a, b) => orderedKeys.indexOf(a.value) - orderedKeys.indexOf(b.value)
|
||||
);
|
||||
}
|
||||
|
||||
return modifiedAnswers;
|
||||
},
|
||||
isDiscountOnOrder() {
|
||||
|
|
@ -235,6 +245,11 @@ export default {
|
|||
)
|
||||
);
|
||||
},
|
||||
experimentPackageOrder() {
|
||||
return experimentMixin.methods.hasSetting(experimentSettings.SERVICE_PACKAGE_ORDER)
|
||||
? experimentMixin.methods.getSettingValue(experimentSettings.SERVICE_PACKAGE_ORDER)
|
||||
: false;
|
||||
},
|
||||
getButtonTypeObject() {
|
||||
if (this.isAfterpayBreakoutDisplay()) {
|
||||
return this.servicePackageRadioAfterpayExperiment;
|
||||
|
|
|
|||
|
|
@ -79,3 +79,137 @@ export function getTodayDate() {
|
|||
export function getTodayDateString() {
|
||||
return convertDateToDateString(getTodayDate());
|
||||
}
|
||||
|
||||
export function getMonthEnd(dateStr) {
|
||||
// convert string to date, do date calc, then return a string back
|
||||
const date = new Date(dateStr.split("-")[0], parseInt(dateStr.split("-")[1]), 0);
|
||||
return convertDateToDateString(date);
|
||||
}
|
||||
|
||||
export function getWeekStartDate(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
// Subtract the day of the week from date to get the date of Sunday
|
||||
const sunday = new Date(date);
|
||||
sunday.setDate(sunday.getDate() - dayOfWeek);
|
||||
return convertDateToDateString(sunday);
|
||||
}
|
||||
|
||||
export function getWeekEndDate(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
|
||||
// Clone the given date and add the remaining days until Saturday
|
||||
const saturday = new Date(date);
|
||||
saturday.setDate(date.getDate() + daysUntilSaturday);
|
||||
return convertDateToDateString(saturday);
|
||||
}
|
||||
|
||||
export function getNextWeekSunday(dateString) {
|
||||
const date = convertDateStringToDate(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
|
||||
// Clone the given date and add the remaining days until Sunday
|
||||
const nextSunday = new Date(date);
|
||||
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
|
||||
return convertDateToDateString(nextSunday);
|
||||
}
|
||||
|
||||
export function getInitialViewWeeks(todayString, initialViewRowsToShow, preSelectedDate) {
|
||||
const weeks = [];
|
||||
let weekStartDate = getWeekStartDate(todayString);
|
||||
let weekEndDate = getWeekEndDate(todayString);
|
||||
|
||||
if (preSelectedDate) {
|
||||
let preSelectedDateMonthEnd = getMonthEnd(preSelectedDate.replace("-mobile", ""));
|
||||
let monthEndsByThisWeek = false;
|
||||
let i = 0;
|
||||
while (!monthEndsByThisWeek && i < 52) {
|
||||
if (i > 0) {
|
||||
weekStartDate = getNextWeekSunday(weekEndDate);
|
||||
weekEndDate = getWeekEndDate(weekStartDate);
|
||||
|
||||
if (
|
||||
(preSelectedDateMonthEnd > weekStartDate &&
|
||||
preSelectedDateMonthEnd < weekEndDate) ||
|
||||
preSelectedDateMonthEnd === weekStartDate ||
|
||||
preSelectedDateMonthEnd === weekEndDate
|
||||
) {
|
||||
weekEndDate = preSelectedDateMonthEnd;
|
||||
monthEndsByThisWeek = true;
|
||||
} else if (preSelectedDateMonthEnd < weekEndDate) {
|
||||
// Additional case: if month ends before this week (but not necessarily during it),
|
||||
// still cut the loop here, but don't truncate the week.
|
||||
|
||||
monthEndsByThisWeek = true;
|
||||
}
|
||||
}
|
||||
weeks.push({
|
||||
weekNum: i + 1,
|
||||
weekStartDate: weekStartDate,
|
||||
weekEndDate: weekEndDate,
|
||||
});
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < initialViewRowsToShow; i++) {
|
||||
if (i > 0) {
|
||||
weekStartDate = getNextWeekSunday(weekEndDate);
|
||||
weekEndDate = getWeekEndDate(weekStartDate);
|
||||
}
|
||||
weeks.push({
|
||||
weekNum: i + 1,
|
||||
weekStartDate: weekStartDate,
|
||||
weekEndDate: weekEndDate,
|
||||
});
|
||||
}
|
||||
// If any of these weeks is split between two months, then make them 2 separate "weeks"
|
||||
// (a week split between two months is considered 2 weeks per business requirements)
|
||||
const hasSplitWeek = (week) => {
|
||||
return week.weekStartDate.split("-")[1] !== week.weekEndDate.split("-")[1]
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
|
||||
|
||||
if (splitWeekIndex > -1) {
|
||||
const week1 = [];
|
||||
const week2 = [];
|
||||
let switchToWeek2 = false;
|
||||
|
||||
for (let j = 0; j < 7; j++) {
|
||||
const newDate = convertDateStringToDate(weeks[splitWeekIndex].weekStartDate);
|
||||
newDate.setDate(newDate.getDate() + j);
|
||||
if (newDate.getDate() === 1) switchToWeek2 = true;
|
||||
if (switchToWeek2) {
|
||||
week2.push(convertDateToDateString(newDate));
|
||||
} else {
|
||||
week1.push(convertDateToDateString(newDate));
|
||||
}
|
||||
}
|
||||
|
||||
const week1EndDate = week1[week1.length - 1];
|
||||
const week2StartDate = week2[0];
|
||||
|
||||
if (week1EndDate < todayString) {
|
||||
// replace week 1 with week 2
|
||||
weeks[splitWeekIndex].weekStartDate = week2StartDate;
|
||||
} else {
|
||||
const newWeek = {
|
||||
weekNum: weeks[splitWeekIndex].weekNum,
|
||||
weekStartDate: week2StartDate,
|
||||
weekEndDate: weeks[splitWeekIndex].weekEndDate,
|
||||
};
|
||||
weeks[splitWeekIndex].weekEndDate = week1EndDate;
|
||||
weeks.splice(splitWeekIndex + 1, 0, newWeek);
|
||||
weeks.pop();
|
||||
weeks.forEach((item, index) => {
|
||||
if (index > splitWeekIndex) {
|
||||
item.weekNum = item.weekNum + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return weeks;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,358 @@
|
|||
<template>
|
||||
<div id="multi-location-modal-container">
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
suppressPageScroll
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalFooterText"
|
||||
:isFooterButtonPrimary="true"
|
||||
@footer-button-event="confirmAppointment"
|
||||
@isModalOpened="setIsModalOpen">
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
buttonTypeString="multiLocationRadioButton"
|
||||
:buttonTypeObject="multiLocationRadioButton"
|
||||
class="mt-5"
|
||||
:answers="availableAppointments"
|
||||
groupName="multiLocationQuestion"
|
||||
textPosition="text-center"
|
||||
v-model="selectedValue"
|
||||
isRequired
|
||||
validationRules="time-slot-required" />
|
||||
|
||||
<template v-slot:modal-header-slot>
|
||||
<span>{{ modalSubHeaderText }}</span>
|
||||
</template>
|
||||
<template v-slot:modal-footer-slot>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
id="see-more-options"
|
||||
:disabled="isLoading"
|
||||
@click="closeModal">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Supporting files
|
||||
import store from "@/store";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import { get12HourTimeFormat } from "@/helpers/date-helper";
|
||||
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
import multiLocationRadioButton from "./multi-location-radio/multi-location-radio";
|
||||
|
||||
// Validation
|
||||
import { defineRule, useField } from "vee-validate";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
||||
const NULL_APPOINTMENT = {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: null,
|
||||
maximum: null,
|
||||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
};
|
||||
|
||||
// Validation for the modal button
|
||||
defineRule("time-slot-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "multi-location-modal",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
buttonWidgetName: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
mobileAppointmentOption: NULL_APPOINTMENT,
|
||||
inshopAppointmentOption: NULL_APPOINTMENT,
|
||||
confirmedAppointment: false,
|
||||
multiLocationRadioButton: multiLocationRadioButton,
|
||||
selectedValue: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
get12HourTimeFormat,
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.pushEvent();
|
||||
this.confirmedAppointment = false;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
confirmAppointment() {
|
||||
if (!this.selectedAppointment.timeSlot.id) return;
|
||||
|
||||
const selectedTimeSlot = {
|
||||
timeSlot: this.selectedAppointment.timeSlot,
|
||||
routeCode: this.selectedAppointment.timeSlot.id,
|
||||
date: this.selectedAppointment.date,
|
||||
provider: this.selectedAppointment.provider,
|
||||
};
|
||||
this.confirmedAppointment = true;
|
||||
this.$emit("confirm-appointment", {
|
||||
selectedTimeSlot: selectedTimeSlot,
|
||||
appointmentType: this.selectedValue,
|
||||
});
|
||||
this.modal.closeModal();
|
||||
},
|
||||
setPromotedMobileAppointment(appointment) {
|
||||
this.mobileAppointmentOption = appointment;
|
||||
},
|
||||
setPromotedInshopAppointment(appointment) {
|
||||
this.inshopAppointmentOption = appointment;
|
||||
},
|
||||
getIsModalOpen() {
|
||||
return this.isModalOpen;
|
||||
},
|
||||
setIsModalOpen(isOpen) {
|
||||
this.isModalOpen = isOpen;
|
||||
},
|
||||
selectedSlotEventValue() {
|
||||
const daysUntilAppointment =
|
||||
(new Date(this.selectedAppointment.date) - new Date()) / (1000 * 60 * 60 * 24);
|
||||
const isAM = this.selectedAppointment.timeSlot.startTime < "12:00:00";
|
||||
return (
|
||||
"AM: " +
|
||||
isAM +
|
||||
" | " +
|
||||
"PM: " +
|
||||
!isAM +
|
||||
" | " +
|
||||
"Days: " +
|
||||
Math.ceil(daysUntilAppointment)
|
||||
);
|
||||
},
|
||||
pushEvent() {
|
||||
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
|
||||
if (
|
||||
this.selectedValue?.toLowerCase() === "mobile" &&
|
||||
this.mobileAppointmentOption?.timeSlot
|
||||
) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.MOBILE_CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.mobileAppointmentOption.timeSlot.startTime +
|
||||
"-" +
|
||||
this.mobileAppointmentOption.timeSlot.endTime +
|
||||
" " +
|
||||
this.mobileAppointmentOption.date,
|
||||
true,
|
||||
null,
|
||||
this.selectedSlotEventValue()
|
||||
);
|
||||
} else if (
|
||||
this.selectedValue?.toLowerCase() === "inshop" &&
|
||||
this.inshopAppointmentOption?.timeSlot
|
||||
) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.INSHOP_CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.inshopAppointmentOption.timeSlot.startTime +
|
||||
" " +
|
||||
this.inshopAppointmentOption.date,
|
||||
true,
|
||||
null,
|
||||
this.selectedSlotEventValue()
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedAppointment() {
|
||||
if (this.selectedValue?.toLowerCase() === "mobile") {
|
||||
return this.mobileAppointmentOption;
|
||||
} else if (this.selectedValue?.toLowerCase() === "inshop") {
|
||||
return this.inshopAppointmentOption;
|
||||
} else {
|
||||
return NULL_APPOINTMENT;
|
||||
}
|
||||
},
|
||||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
modalSubHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
buttonText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText2");
|
||||
},
|
||||
mobileButtonLabel() {
|
||||
return this.getCmsContent(this.buttonWidgetName, "HeaderText");
|
||||
},
|
||||
inshopButtonLabel() {
|
||||
return this.getCmsContent(this.buttonWidgetName, "SubheaderText");
|
||||
},
|
||||
mobileButtonDate() {
|
||||
if (!this.mobileAppointmentOption.date) return "";
|
||||
return new Date(this.mobileAppointmentOption.date).toLocaleDateString("en-US", {
|
||||
timeZone: "UTC",
|
||||
weekday: "short",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
inshopButtonDate() {
|
||||
if (!this.inshopAppointmentOption.date) return "";
|
||||
return new Date(this.inshopAppointmentOption.date).toLocaleDateString("en-US", {
|
||||
timeZone: "UTC",
|
||||
weekday: "short",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
},
|
||||
mobileButtonTime() {
|
||||
const timeslot =
|
||||
this.get12HourTimeFormat(this.mobileAppointmentOption.timeSlot?.startTime) +
|
||||
" - " +
|
||||
this.get12HourTimeFormat(this.mobileAppointmentOption.timeSlot?.endTime);
|
||||
return timeslot;
|
||||
},
|
||||
inshopButtonTime() {
|
||||
const timeslot = this.get12HourTimeFormat(
|
||||
this.inshopAppointmentOption.timeSlot?.startTime
|
||||
);
|
||||
return timeslot;
|
||||
},
|
||||
mobileLocationLabel() {
|
||||
return this.getCmsContent(this.buttonWidgetName, "BodyText");
|
||||
},
|
||||
inshopLocationLabel() {
|
||||
return this.inshopAppointmentOption?.addressCopy;
|
||||
},
|
||||
mobileServiceDuration() {
|
||||
const durationTime = getDisplayTextForDurationLength(
|
||||
this.mobileAppointmentOption.estimatedServiceMinutes.minimum,
|
||||
this.mobileAppointmentOption.estimatedServiceMinutes.maximum
|
||||
);
|
||||
const footerText = this.getCmsContent(this.buttonWidgetName, "FooterText").replaceAll(
|
||||
"{custom:serviceLength}",
|
||||
durationTime
|
||||
);
|
||||
return footerText;
|
||||
},
|
||||
inshopServiceDuration() {
|
||||
const durationTime = getDisplayTextForDurationLength(
|
||||
this.inshopAppointmentOption.estimatedServiceMinutes.minimum,
|
||||
this.inshopAppointmentOption.estimatedServiceMinutes.maximum
|
||||
);
|
||||
const footerText = this.getCmsContent(this.buttonWidgetName, "FooterText").replaceAll(
|
||||
"{custom:serviceLength}",
|
||||
durationTime
|
||||
);
|
||||
return footerText;
|
||||
},
|
||||
availableAppointments() {
|
||||
return [
|
||||
{
|
||||
value: "mobile",
|
||||
buttonLabel: this.mobileButtonDate,
|
||||
buttonLabelSubCopy: this.mobileButtonTime,
|
||||
buttonBodyCopy: this.mobileLocationLabel,
|
||||
buttonAuxillaryCopy: this.mobileButtonLabel,
|
||||
buttonFooterCopy: this.mobileServiceDuration,
|
||||
},
|
||||
{
|
||||
value: "inshop",
|
||||
buttonLabel: this.inshopButtonDate,
|
||||
buttonLabelSubCopy: this.inshopButtonTime,
|
||||
buttonBodyCopy: this.inshopLocationLabel,
|
||||
buttonAuxillaryCopy: this.inshopButtonLabel,
|
||||
buttonFooterCopy: this.inshopServiceDuration,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
components: { modal, buttonQuestion },
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#multi-location-modal-container {
|
||||
.modal-component {
|
||||
@include media-breakpoint-up(md) {
|
||||
.modal-dialog {
|
||||
left: 0;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
width: 22.063rem;
|
||||
transform: translate(0, 0);
|
||||
|
||||
.modal-content {
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-dialog {
|
||||
.modal-content {
|
||||
.modal-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
margin-top: 4rem;
|
||||
|
||||
& > span {
|
||||
color: $red;
|
||||
font-weight: $font-weight-600;
|
||||
text-transform: uppercase;
|
||||
font-family: $font-family-sans-serif-semibold;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: $font-size-20;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.button-question {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-body-inner {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 1rem;
|
||||
background-color: #f4f4f4;
|
||||
font-size: $font-size-14;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-footer {
|
||||
flex-flow: wrap-reverse;
|
||||
|
||||
#see-more-options {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
margin-top: 1.5rem;
|
||||
font-weight: $font-weight-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import multiLocationRadio from "./multi-location-radio";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
describe("multi-location-radio.vue", () => {
|
||||
it("Should include buttonLabel in html", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const outputHtml = wrapper.html();
|
||||
|
||||
// Assert
|
||||
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]));
|
||||
});
|
||||
|
||||
it("Should include buttonLabelSubCopy in html", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const outputHtml = wrapper.html();
|
||||
|
||||
// Assert
|
||||
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"]));
|
||||
});
|
||||
it("Should include buttonFooterCopy in html", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const outputHtml = wrapper.html();
|
||||
|
||||
// Assert
|
||||
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"]));
|
||||
});
|
||||
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a <ul><li>...</li>(x5)</ul> buttonBodyCopy", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
// Act
|
||||
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
|
||||
|
||||
// Assert
|
||||
expect(results.length).toBe(5);
|
||||
});
|
||||
it("Should get strings from getArrayOfListItemsFromRawCmsCopy without <ul> or <li> tags when provided with a <ul><li>...</li></ul> buttonBodyCopy", async () => {
|
||||
// Arrange
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: mockProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
|
||||
|
||||
// Assert
|
||||
const fileredResults = results.filter((result) => {
|
||||
return (
|
||||
result.includes("<ul>") ||
|
||||
result.includes("</ul>") ||
|
||||
result.includes("<li>") ||
|
||||
result.includes("</li>")
|
||||
);
|
||||
});
|
||||
expect(fileredResults.length).toBe(0);
|
||||
});
|
||||
it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when buttonBodyCopy has 6 total <li>...</li>, but one is empty ", async () => {
|
||||
// Arrange
|
||||
const moddedProps = mockProps;
|
||||
moddedProps["buttonBodyCopy"] =
|
||||
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li><li></li></ul>";
|
||||
let { wrapper } = setupMocks({
|
||||
mountOptionsMockData: {
|
||||
propsData: moddedProps,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy);
|
||||
|
||||
// Assert
|
||||
expect(results.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
const mockProps = {
|
||||
groupName: "group-name",
|
||||
buttonLabel: "buttonLabel test copy",
|
||||
buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy",
|
||||
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
|
||||
buttonBodyCopy:
|
||||
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>",
|
||||
buttonFooterCopy: "buttonFooterCopy test copy",
|
||||
buttonAuxillaryCopy: "buttonAuxillaryCopy test copy",
|
||||
};
|
||||
|
||||
function setupMocks({ mountOptionsMockData = {} }) {
|
||||
const wrapper = mount(multiLocationRadio, {
|
||||
...mountOptionsMockData,
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
});
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
<template>
|
||||
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
|
||||
<div class="radio-label" for="testradio">
|
||||
<div class="location-specs">
|
||||
<div class="row">
|
||||
<div class="col md-6">
|
||||
<p class="m-0">
|
||||
<span v-html="this.buttonLabel"></span>
|
||||
</p>
|
||||
<span class="service-tag" v-html="this.buttonAuxillaryCopy"></span>
|
||||
<p
|
||||
class="sub-label m-0"
|
||||
v-if="this.buttonLabelSubCopy"
|
||||
v-html="this.buttonLabelSubCopy"></p>
|
||||
</div>
|
||||
<div class="col md-6 location-info">
|
||||
<div
|
||||
class="location-info-label"
|
||||
v-if="this.buttonBodyCopy && this.value === 'mobile'"
|
||||
v-html="this.buttonBodyCopy"></div>
|
||||
<div
|
||||
class="location-info-label"
|
||||
v-if="this.buttonBodyCopy && this.value === 'inshop'">
|
||||
<textLink
|
||||
linkType="newWindowLink"
|
||||
:text="this.buttonBodyCopy"
|
||||
:href="inshopHref"
|
||||
target="_blank" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col md-6 time-info" v-html="this.buttonFooterCopy"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
import {
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getRouterLinkRouteFromCopy,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
export default {
|
||||
name: "multiLocationRadioButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
textLink,
|
||||
},
|
||||
computed: {
|
||||
arrayOfListItemsFromBodyText() {
|
||||
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
|
||||
},
|
||||
shouldDisplayStrikeThroughPrice() {
|
||||
const displayPrice = this.buttonAuxillaryCopy?.substring(
|
||||
this.buttonAuxillaryCopy.indexOf("$")
|
||||
);
|
||||
return displayPrice != this.additionalButtonData?.strikeThroughPrice;
|
||||
},
|
||||
isAppleBrowser() {
|
||||
return baseMixin.methods.isAppleBrowser();
|
||||
},
|
||||
inshopHref() {
|
||||
if (this.isAppleBrowser) {
|
||||
return "https://maps.apple.com/?q=" + encodeURIComponent(this.buttonBodyCopy) + '"';
|
||||
}
|
||||
return (
|
||||
"https://www.google.com/maps/search/?api=1&query=" +
|
||||
encodeURIComponent(this.buttonBodyCopy) +
|
||||
'"'
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getRouterLinkRouteFromCopy,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
stripUlTagFromCopy(copy) {
|
||||
const regex = /(?:<ul(?:.*?)>)|(?:<\/ul>)/g;
|
||||
return copy?.replace(regex, "");
|
||||
},
|
||||
getArrayOfListItemsFromRawCmsCopy(copy) {
|
||||
const withoutUlTags = this.stripUlTagFromCopy(copy);
|
||||
return withoutUlTags
|
||||
?.split(/(?:<li(?:.*?)>)|(?:<\/li>)/g)
|
||||
?.filter((lineItem) => lineItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.multi-location-wrapper {
|
||||
margin: 1rem 0;
|
||||
&:first-child {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
||||
+ .radio-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid $gray-300;
|
||||
box-shadow:
|
||||
0px 4px 8px -4px rgba(0, 0, 0, 0.15),
|
||||
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
min-height: 52px;
|
||||
max-height: 126px;
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
transition: max-height 0.25s ease-in;
|
||||
|
||||
&.has-subheader {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: relative;
|
||||
top: 5px;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $gray-500;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 20px;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
min-width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
+ .radio-label {
|
||||
&:before {
|
||||
border: 1px solid #8e9292;
|
||||
box-shadow:
|
||||
0px 0px 0px 4px #9fcee6,
|
||||
0px 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ .radio-label {
|
||||
.location-specs {
|
||||
.hide-when-closed {
|
||||
display: block;
|
||||
}
|
||||
.service-tag {
|
||||
background: $blue;
|
||||
color: $blue-150;
|
||||
}
|
||||
}
|
||||
}
|
||||
+ .radio-label {
|
||||
border: 1px solid $blue;
|
||||
background-color: $blue-100;
|
||||
}
|
||||
+ .radio-label {
|
||||
&:before {
|
||||
box-shadow: 0px 0px 0px 1px $blue;
|
||||
}
|
||||
}
|
||||
+ .radio-label {
|
||||
&:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
+ .radio-label {
|
||||
&:before {
|
||||
border: 2px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.location-specs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
|
||||
.col {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.service-tag {
|
||||
font-size: $font-size-12;
|
||||
font-family: $font-family-sans-serif-semibold;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: $blue-150;
|
||||
color: $blue;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
p {
|
||||
font-size: $font-size-14;
|
||||
|
||||
& > span {
|
||||
color: $black;
|
||||
font-family: $font-family-sans-serif-semibold;
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
.sub-label {
|
||||
margin-bottom: 0.5rem !important;
|
||||
border-bottom: 1px solid $gray-1000;
|
||||
}
|
||||
}
|
||||
|
||||
.location-info-label,
|
||||
.time-info {
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-12;
|
||||
}
|
||||
.location-info-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.625rem;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-right: 0.375rem;
|
||||
width: 0.75rem;
|
||||
height: 1rem;
|
||||
background-image: url(~@/assets/img/icons/location-pin.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50%;
|
||||
background-size: 0.75rem auto;
|
||||
padding: 0 0.75rem 0 0;
|
||||
@include blue-filter();
|
||||
}
|
||||
& > a.new-window-link {
|
||||
margin-bottom: 0;
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-12;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideaway {
|
||||
from {
|
||||
display: block;
|
||||
}
|
||||
to {
|
||||
transform: translateY(40px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-when-closed {
|
||||
display: none;
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -57,6 +57,40 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
|||
navigateToHeritageFunnel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/debug-log-helper.js", () => ({
|
||||
debugLog: jest.fn(),
|
||||
}));
|
||||
|
||||
const createValidOrderForSchedule = () => ({
|
||||
payment: {
|
||||
isInsurance: false,
|
||||
insuranceCoverage: { isVerified: false },
|
||||
},
|
||||
referralNumber: "",
|
||||
serviceLocation: {
|
||||
address: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "00000",
|
||||
zipCodeCtu: "000",
|
||||
appointmentType: null,
|
||||
provider: {},
|
||||
isVehicleProtected: false,
|
||||
},
|
||||
schedule: {
|
||||
date: "",
|
||||
routeCode: "",
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
jobMinMinutes: null,
|
||||
jobMaxMinutes: null,
|
||||
},
|
||||
policy: { isItac: false, isNoComp: false },
|
||||
damage: { isRepair: true },
|
||||
lineItems: { glassParts: [] },
|
||||
});
|
||||
|
||||
describe("schedule.vue", () => {
|
||||
describe("navigation", () => {
|
||||
test("backButtonAction => non-insurance: navigateWithoutSaving called", () => {
|
||||
|
|
@ -149,8 +183,67 @@ describe("schedule.vue", () => {
|
|||
expect(wrapper.vm.$router.navigateWithSaving).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("arePagePrerequisitesValid", () => {
|
||||
test("returns true when all prerequisites are valid", () => {
|
||||
const { wrapper } = setupMocksForArePagePrerequisitesValid();
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when service zip info is missing", () => {
|
||||
const { wrapper } = setupMocksForArePagePrerequisitesValid();
|
||||
store.getters.order.serviceLocation.zipCode = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when glass parts or repair info is missing", () => {
|
||||
const { wrapper } = setupMocksForArePagePrerequisitesValid();
|
||||
store.getters.order.damage.isRepair = false;
|
||||
store.getters.order.lineItems.glassParts = [];
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when insurance info is invalid", () => {
|
||||
const { wrapper } = setupMocksForArePagePrerequisitesValid();
|
||||
store.getters.order.payment.isInsurance = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when supporting items missing for cash order", () => {
|
||||
const { wrapper } = setupMocksForArePagePrerequisitesValid();
|
||||
store.getters.order.payment.isInsurance = false;
|
||||
store.getters.lineItems.supportingItems = null;
|
||||
|
||||
const result = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocksForArePagePrerequisitesValid() {
|
||||
store.getters = {
|
||||
...storeMocked.getters,
|
||||
order: createValidOrderForSchedule(),
|
||||
lineItems: {
|
||||
supportingItems: [],
|
||||
},
|
||||
};
|
||||
return setupMocks({ store });
|
||||
}
|
||||
|
||||
function setupMocks(mountOptionsMockData = {}) {
|
||||
const route = { name: "schedule" };
|
||||
const defaultMountOptions = {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@
|
|||
<mobileFirstModal
|
||||
modalWidgetName="MobileFirstModalWidget"
|
||||
ref="mobileFirstModal"
|
||||
@confirm-appointment="updateTimeSlotandNavigateForward" />
|
||||
@confirm-appointment="updateMobileFirstTimeSlotandNavigateForward" />
|
||||
<multiLocationModal
|
||||
modalWidgetName="MultiLocationModalWidget"
|
||||
buttonWidgetName="MultiLocationRadioButtonWidget"
|
||||
ref="multiLocationModal"
|
||||
@confirm-appointment="updateMultiLocationTimeSlotandNavigateForward" />
|
||||
<recalAckModal
|
||||
modalWidgetName="RecalAckModalWidget"
|
||||
agreementWidgetName="RecalAgreementQuestionWidget"
|
||||
|
|
@ -14,7 +19,9 @@
|
|||
ref="recalAckModal" />
|
||||
<div
|
||||
class="container page-container-grouped-styles"
|
||||
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
|
||||
:class="[
|
||||
isMobileFirstModalOpen || isMultiLocationModalOpen ? 'hidden-background' : '',
|
||||
]">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
<mobileFeeWaiverAlert
|
||||
|
|
@ -28,7 +35,6 @@
|
|||
cmsWidgetName="ScheduleYourServiceWidget"
|
||||
id="schedule-your-service" />
|
||||
<appointmentTypeQuestion
|
||||
v-if="hasSelectableDatesLoaded"
|
||||
v-model="appointmentTypeFromAppointmentTypeQuestion"
|
||||
v-show="isAppointmentTypeDisplayed"
|
||||
:isServiceableMobile="isServiceableMobile"
|
||||
|
|
@ -36,7 +42,6 @@
|
|||
:isDisplayed="isAppointmentTypeDisplayed"
|
||||
:mobileFeeApplies="mobileFeeApplies"
|
||||
:zipCode="zipCode"
|
||||
ref="appointmentTypeQuestion"
|
||||
groupName="appointmentTypeQuestion"
|
||||
cmsWidgetName="AppointmentTypeQuestionWidget"
|
||||
validationRules="option-required"
|
||||
|
|
@ -76,6 +81,16 @@
|
|||
cmsWidgetName="AlertNoShopsWidget"
|
||||
v-if="displayNoShopsAlert"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
ref="alertMSRNotCoveredByInsurance"
|
||||
class="mt-5 mb-5"
|
||||
v-if="isMSRFeeNotCoveredByInsurance"
|
||||
:manualHeadline="MSRFeeAlertHeadlineText"
|
||||
:manualCopy="MSRFeeAlertBodyText"
|
||||
:showWarningIcon="true"
|
||||
:hasBorder="true"
|
||||
:showHorizontalRow="true"
|
||||
alertClass="alert-warning" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -193,6 +208,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
|
|||
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
||||
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
|
||||
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
|
||||
import multiLocationModal from "@/layouts/schedule/multi-location-modal/multi-location-modal.vue";
|
||||
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
|
||||
|
||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
|
|
@ -223,7 +239,11 @@ import {
|
|||
RECAL_ACK_YES,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import {
|
||||
fetchCmsContentForPage,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
formatToUSDollar,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
|
|
@ -239,13 +259,14 @@ import { partNumberStrings } from "@/constants/part-number-strings";
|
|||
import store from "@/store";
|
||||
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
|
||||
import {
|
||||
calcDaysBetweenDates,
|
||||
convertDateStringToDate,
|
||||
sumDateString,
|
||||
isDropOffRouteCode,
|
||||
getTodayDate,
|
||||
getTodayDateString,
|
||||
getInitialViewWeeks,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper";
|
||||
|
||||
|
|
@ -255,6 +276,12 @@ import { getAmountDue, getPricingByDayPartWithPrice } from "@/helpers/pricing-he
|
|||
import { getItemsWithoutRecalParts } from "@/helpers/recal-helper";
|
||||
import { deepClone } from "@/helpers/object-helper";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
import {
|
||||
flushPagePrereqsLogs,
|
||||
hasServiceZipInfo,
|
||||
hasGlassPartsOrRepairInfo,
|
||||
hasInsuranceInfo,
|
||||
} from "@/helpers/page-prerequisites-helper.js";
|
||||
import {
|
||||
getSessionKeyValue,
|
||||
getUserIdValue,
|
||||
|
|
@ -477,11 +504,9 @@ export default {
|
|||
shopListButton: shopListButton,
|
||||
lastSelectedInshopOrDropoffProvider: null,
|
||||
selectedMobileFirstAppointment: false,
|
||||
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
),
|
||||
selectedMultiLocationAppointment: false,
|
||||
calendarLoadingStatus: "none",
|
||||
isLoadingMultiLocationPopup: true,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -612,8 +637,9 @@ export default {
|
|||
pricedMobileFeePart,
|
||||
shopProviderData.data
|
||||
);
|
||||
vm.initializeDatePicker().then(() => {
|
||||
vm.showMobileFirstModal();
|
||||
vm.initializeDatePicker().then(async () => {
|
||||
// vm.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||
await vm.showMultiLocationModal();
|
||||
vm.hideLoadingModal();
|
||||
vm.calendarLoadingStatus = "none";
|
||||
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
|
||||
|
|
@ -679,8 +705,7 @@ export default {
|
|||
isMobileStaticRecalibrationApplicable() {
|
||||
return (
|
||||
this.displayMSR &&
|
||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
|
||||
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
|
||||
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
|
||||
);
|
||||
},
|
||||
displayMSR() {
|
||||
|
|
@ -690,6 +715,28 @@ export default {
|
|||
?.toLowerCase() === "true"
|
||||
);
|
||||
},
|
||||
isMSRFeeNotCoveredByInsurance() {
|
||||
return (
|
||||
this.isMobileSelected &&
|
||||
this.isMobileStaticRecalibrationApplicable &&
|
||||
this.mobileFeeHasPrice &&
|
||||
this.isInsurance &&
|
||||
!this.mobileFeePart?.isInsurable
|
||||
);
|
||||
},
|
||||
MSRFeeAlertHeadlineText() {
|
||||
return this.getCmsContent("AlertMSRNotCoveredByInsuranceWidget", "HeadlineText");
|
||||
},
|
||||
MSRFeeAlertBodyText() {
|
||||
const cmsContentText = this.getCmsContent(
|
||||
"AlertMSRNotCoveredByInsuranceWidget",
|
||||
"BodyText"
|
||||
);
|
||||
return cmsContentText.replaceAll(
|
||||
"{custom:mobileFee}",
|
||||
formatToUSDollar(this.mobileFee)
|
||||
);
|
||||
},
|
||||
isCashItacNoComp() {
|
||||
return !this.isInsurance || this.isITAC || this.isNoComp;
|
||||
},
|
||||
|
|
@ -805,13 +852,6 @@ export default {
|
|||
};
|
||||
},
|
||||
selectedShopAnswer() {
|
||||
const toTitleCase = (str) => {
|
||||
if (!str) return "";
|
||||
return str.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
this.selectedProvider &&
|
||||
this.selectedProvider.address &&
|
||||
|
|
@ -820,18 +860,11 @@ export default {
|
|||
const provider = this.shopProviderData?.shopProviders?.find(
|
||||
(p) => p.providerNumber === this.selectedProvider?.providerNumber
|
||||
);
|
||||
const streetAddress = toTitleCase(this.selectedProvider.address.streetAddress);
|
||||
const city = toTitleCase(this.selectedProvider.address.city);
|
||||
const state = this.selectedProvider.address.state;
|
||||
const zipCode = this.selectedProvider.address.zipCode;
|
||||
const distanceInMiles = provider ? Math.round(provider.distanceInMiles * 2) / 2 : 0;
|
||||
const distanceInMiles = provider?.distanceInMiles
|
||||
? Math.round(provider.distanceInMiles * 2) / 2
|
||||
: 0;
|
||||
|
||||
return {
|
||||
city: `${city}`,
|
||||
distance: `${distanceInMiles} mi`,
|
||||
address1: `${streetAddress}`,
|
||||
address2: `${city}, ${state} ${zipCode}`,
|
||||
};
|
||||
return this.getFormattedShopAddress(provider?.address, distanceInMiles);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
|
@ -902,6 +935,15 @@ export default {
|
|||
? this.$refs.mobileFirstModal?.getIsModalOpen()
|
||||
: false;
|
||||
},
|
||||
isMultiLocationModalOpen() {
|
||||
// Only return false if we're done loading the popup
|
||||
if (this.isLoadingMultiLocationPopup) {
|
||||
return true; // Still loading, keep background hidden
|
||||
}
|
||||
return this.selectableDatesMobile.days && this.selectableDatesInshop.days
|
||||
? this.$refs.multiLocationModal?.getIsModalOpen()
|
||||
: false;
|
||||
},
|
||||
displayWaitList() {
|
||||
if (!this.appointmentType) return false;
|
||||
const firstMobileDateString =
|
||||
|
|
@ -950,86 +992,65 @@ export default {
|
|||
return false;
|
||||
},
|
||||
hasSelectableDatesLoaded() {
|
||||
if (this.isMobileSelected) {
|
||||
if (this.isServiceableMobile) {
|
||||
return this.selectableDatesMobile?.days?.length > 0;
|
||||
} else {
|
||||
return this.selectableDatesInshop?.days?.length > 0;
|
||||
}
|
||||
},
|
||||
isShowMobileFirstAppt() {
|
||||
return experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
);
|
||||
},
|
||||
isShowMultiLocationPopup() {
|
||||
return experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MULTI_LOCATION_APPT,
|
||||
"true"
|
||||
);
|
||||
},
|
||||
initialViewStartDate() {
|
||||
return getTodayDateString();
|
||||
},
|
||||
initialViewEndDate() {
|
||||
const initialViewWeeks = getInitialViewWeeks(
|
||||
this.initialViewStartDate,
|
||||
NUMBER_OF_CALENDAR_ROWS_TO_SHOW_FOR_INITIAL_VIEW,
|
||||
this.preSelectedDate
|
||||
);
|
||||
return initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
arePagePrerequisitesValid() {
|
||||
const paymentInfo = store.getters.payment.isInsurance !== null;
|
||||
const damageInfo =
|
||||
store.getters.order.damage.isRepair ||
|
||||
(store.getters.order.lineItems?.glassParts != null &&
|
||||
store.getters.order.lineItems.glassParts.length > 0);
|
||||
const parentAccountNumberInfo =
|
||||
store.getters.payment.parentAccountNumber !==
|
||||
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
|
||||
const policyNumberInfo = store.getters.order.policy.policyNumber;
|
||||
const zipCodeInfo = store.getters.order.serviceLocation.zipCode;
|
||||
const supportingItemsInfo = store.getters.lineItems.supportingItems;
|
||||
const order = store.getters.order;
|
||||
const logQueue = [];
|
||||
|
||||
const serviceZip = hasServiceZipInfo(order, logQueue);
|
||||
const glassPartsOrRepair = hasGlassPartsOrRepairInfo(order, logQueue);
|
||||
const insuranceInfo = hasInsuranceInfo(order, logQueue);
|
||||
|
||||
// insurance drops the recycle fee on replace orders so it won't be in supportingItems
|
||||
const insurancePreReqResult =
|
||||
parentAccountNumberInfo &&
|
||||
policyNumberInfo &&
|
||||
zipCodeInfo &&
|
||||
paymentInfo &&
|
||||
damageInfo;
|
||||
const cashPreReqResult =
|
||||
supportingItemsInfo && zipCodeInfo && paymentInfo && damageInfo;
|
||||
const supportingItemsInfo = store.getters.lineItems?.supportingItems;
|
||||
const supportingItemsPreReqs = order.payment?.isInsurance
|
||||
? true
|
||||
: !!supportingItemsInfo;
|
||||
|
||||
const outputDebugLog = (preReqResult) => {
|
||||
// prettier-ignore
|
||||
debugLog("--- schedule.vue pagePrereqs start ---", null, !preReqResult);
|
||||
debugLog(
|
||||
"store.getters.payment.isInsurance:",
|
||||
store.getters.payment?.isInsurance,
|
||||
!preReqResult
|
||||
);
|
||||
debugLog(
|
||||
"store.getters.order.damage.isRepair:",
|
||||
store.getters.order.damage?.isRepair,
|
||||
!preReqResult
|
||||
);
|
||||
debugLog(
|
||||
"store.getters.order.lineItems.glassParts:",
|
||||
store.getters.order.lineItems?.glassParts,
|
||||
!preReqResult
|
||||
);
|
||||
debugLog(
|
||||
"store.getters.payment.parentAccountNumber:",
|
||||
store.getters.payment.parentAccountNumber,
|
||||
!preReqResult
|
||||
);
|
||||
debugLog(
|
||||
"store.getters.order.policy.policyNumber:",
|
||||
store.getters.order.policy.policyNumber,
|
||||
!preReqResult
|
||||
);
|
||||
debugLog(
|
||||
"store.getters.order.serviceLocation.zipCode:",
|
||||
store.getters.order.serviceLocation.zipCode,
|
||||
!preReqResult
|
||||
);
|
||||
const preReqResult =
|
||||
serviceZip && insuranceInfo && glassPartsOrRepair && supportingItemsPreReqs;
|
||||
|
||||
logQueue.push(() => {
|
||||
debugLog(
|
||||
"store.getters.lineItems.supportingItems:",
|
||||
store.getters.lineItems.supportingItems,
|
||||
!preReqResult
|
||||
store.getters.lineItems?.supportingItems,
|
||||
!supportingItemsPreReqs
|
||||
);
|
||||
debugLog("--- schedule.vue pagePrereqs end ---", null, !preReqResult);
|
||||
};
|
||||
});
|
||||
|
||||
if (store.getters.payment.isInsurance) {
|
||||
outputDebugLog(insurancePreReqResult);
|
||||
return insurancePreReqResult;
|
||||
} else {
|
||||
outputDebugLog(cashPreReqResult);
|
||||
return cashPreReqResult;
|
||||
}
|
||||
flushPagePrereqsLogs("schedule.vue", preReqResult, logQueue);
|
||||
return preReqResult;
|
||||
},
|
||||
|
||||
setDataOnLoad(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) {
|
||||
|
|
@ -1241,7 +1262,8 @@ export default {
|
|||
}
|
||||
}
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||
this.showMultiLocationModal();
|
||||
this.hideLoadingModal();
|
||||
this.calendarLoadingStatus = "none";
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
|
|
@ -1275,7 +1297,7 @@ export default {
|
|||
},
|
||||
|
||||
async initializeDatePicker() {
|
||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
||||
this.isShowMultiLocationPopup && this.showLoadingModal();
|
||||
|
||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
|
||||
|
|
@ -1787,7 +1809,8 @@ export default {
|
|||
this.zipCodeCtu = newZipCode.zipCodeCtu;
|
||||
this.selectedDate = null;
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
// this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
|
||||
this.showMultiLocationModal();
|
||||
this.hideLoadingModal();
|
||||
this.calendarLoadingStatus = "none";
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
|
|
@ -1842,10 +1865,13 @@ export default {
|
|||
} else {
|
||||
this.appointmentType = null;
|
||||
}
|
||||
this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
},
|
||||
setSelectedDateToFirstAvailable() {
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
if (this.hasSelectableDatesLoaded) {
|
||||
// ensure dates have been loaded by now
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
}
|
||||
},
|
||||
showRecalAcknowledgementModal() {
|
||||
if (this.shouldShowRecalAckModal()) {
|
||||
|
|
@ -1899,7 +1925,7 @@ export default {
|
|||
const order = this.$store.getters.order;
|
||||
const isRepair = order.damage.isRepair;
|
||||
|
||||
let preSelectedMobileAppointment = null;
|
||||
let promotedMobileAppointment = null;
|
||||
const todaysDate = getTodayDate();
|
||||
let firstMobileAMAppt = await this.getFirstAvailableMobileApptByTOD("AM");
|
||||
let firstMobilePMAppt = await this.getFirstAvailableMobileApptByTOD("PM");
|
||||
|
|
@ -1997,7 +2023,7 @@ export default {
|
|||
if (this.isShowMobileFirstAppt) {
|
||||
if (pmMobileDays == 0) {
|
||||
// Selects the first available time slot
|
||||
preSelectedMobileAppointment =
|
||||
promotedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
|
|
@ -2006,20 +2032,20 @@ export default {
|
|||
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobilePMAppt;
|
||||
promotedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobileAMAppt
|
||||
promotedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
}
|
||||
|
||||
if (preSelectedMobileAppointment) {
|
||||
if (promotedMobileAppointment) {
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
promotedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
}
|
||||
|
|
@ -2027,11 +2053,158 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
async showMultiLocationModal() {
|
||||
const showMultiLocationAppointment = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_MULTI_LOCATION_APPT
|
||||
);
|
||||
|
||||
if (
|
||||
!this.selectedRouteCodeData?.routeCode &&
|
||||
showMultiLocationAppointment?.toLowerCase() === "true"
|
||||
) {
|
||||
let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION
|
||||
);
|
||||
let maxDayRangeToShowNoPmTimeslot = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_PM_DAYS_MULTI_LOCATION
|
||||
);
|
||||
let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION
|
||||
);
|
||||
let promotedMobileAppointment = null;
|
||||
let promotedInshopAppointment = null;
|
||||
const todaysDate = getTodayDate();
|
||||
|
||||
// Helper function to calculate days until appointment (handles null/undefined)
|
||||
const calculateDaysUntilDate = (appointmentObj) => {
|
||||
const dateString = appointmentObj?.date;
|
||||
return dateString
|
||||
? (new Date(dateString) - todaysDate) / (1000 * 60 * 60 * 24)
|
||||
: null;
|
||||
};
|
||||
|
||||
// Fetch all appointments
|
||||
const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAppts] = await Promise.all([
|
||||
this.getFirstAvailableMobileApptByTOD("AM"),
|
||||
this.getFirstAvailableMobileApptByTOD("PM"),
|
||||
this.getFirstAvailableInshopApptsByTOD(["AM", "PM"]),
|
||||
]);
|
||||
|
||||
// Extract dates
|
||||
const firstMobileAMDate = firstMobileAMAppt?.date;
|
||||
const firstMobilePMDate = firstMobilePMAppt?.date;
|
||||
const firstInshopAMDate = firstInshopAppts[0]?.date;
|
||||
const firstInshopPMDate = firstInshopAppts[1]?.date;
|
||||
|
||||
// Calculate days until appointments
|
||||
const numberOfDaysToFirstMobileAMDate = calculateDaysUntilDate(firstMobileAMAppt);
|
||||
const numberOfDaysToFirstMobilePMDate = calculateDaysUntilDate(firstMobilePMAppt);
|
||||
const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAppts[0]);
|
||||
const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopAppts[1]);
|
||||
|
||||
const shouldExposeMultiLocationModal = () => {
|
||||
const isMobileAppointmentInDateRange =
|
||||
(firstMobileAMDate &&
|
||||
numberOfDaysToFirstMobileAMDate <= maxDayRangeToShowAnyTimeslot) ||
|
||||
(firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowAnyTimeslot);
|
||||
const isInshopAppointmentInDateRange =
|
||||
(firstInshopAMDate &&
|
||||
numberOfDaysToFirstInshopAMDate <= maxDayRangeToShowAnyTimeslot) ||
|
||||
(firstInshopPMDate &&
|
||||
numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot);
|
||||
|
||||
return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange;
|
||||
};
|
||||
|
||||
if (shouldExposeMultiLocationModal()) {
|
||||
const multiLocationPopupExperiment =
|
||||
store.getters.applicationUser.experiments.find(
|
||||
(e) => e.universeName === experimentUniverses.MULTI_LOCATION_POPUP
|
||||
);
|
||||
const hasExposedMultiLocationPopup = multiLocationPopupExperiment?.isExposed;
|
||||
|
||||
if (!hasExposedMultiLocationPopup && multiLocationPopupExperiment) {
|
||||
baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
|
||||
{
|
||||
userId: getUserIdValue(),
|
||||
deviceId: getDeviceIdValue(),
|
||||
sessionKey: getSessionKeyValue(),
|
||||
pageName: "schedule",
|
||||
experiment: multiLocationPopupExperiment,
|
||||
},
|
||||
"schedule",
|
||||
false
|
||||
);
|
||||
}
|
||||
if (this.isShowMultiLocationPopup) {
|
||||
if (maxDayRangeToShowPmTimeslot == 0) {
|
||||
// Selects the first available mobile time slot
|
||||
promotedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
// Selects the first available inshop time slot
|
||||
promotedInshopAppointment =
|
||||
numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate
|
||||
? firstInshopAppts[0]
|
||||
: firstInshopAppts[1];
|
||||
} else {
|
||||
if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowPmTimeslot
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
promotedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= maxDayRangeToShowNoPmTimeslot
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
promotedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
}
|
||||
|
||||
if (
|
||||
firstInshopPMDate &&
|
||||
numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowPmTimeslot
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
promotedInshopAppointment = firstInshopAppts[1];
|
||||
} else if (
|
||||
firstInshopPMDate &&
|
||||
numberOfDaysToFirstInshopPMDate >= maxDayRangeToShowNoPmTimeslot
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
promotedInshopAppointment = firstInshopAppts[0]
|
||||
? firstInshopAppts[0]
|
||||
: firstInshopAppts[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (promotedMobileAppointment && promotedInshopAppointment) {
|
||||
this.$refs.multiLocationModal.setPromotedMobileAppointment(
|
||||
promotedMobileAppointment
|
||||
);
|
||||
this.$refs.multiLocationModal.setPromotedInshopAppointment(
|
||||
promotedInshopAppointment
|
||||
);
|
||||
this.$refs.multiLocationModal.openModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.isLoadingMultiLocationPopup = false;
|
||||
},
|
||||
|
||||
async getFirstAvailableMobileApptByTOD(timeOfDay) {
|
||||
if (!this.selectableDatesMobile?.days?.length) {
|
||||
const selectableDays = this.selectableDatesMobile?.days;
|
||||
if (!selectableDays?.length) {
|
||||
return null;
|
||||
} else {
|
||||
for (const dateObj of this.selectableDatesMobile.days) {
|
||||
for (const dateObj of selectableDays) {
|
||||
let matchingTimeSlot = {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: this.estimatedServiceMinutesMinimum,
|
||||
|
|
@ -2053,7 +2226,137 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
||||
|
||||
findMatchingInshopAppt(selectableDays, timeOfDay, provider) {
|
||||
if (!selectableDays?.length) return null;
|
||||
for (const dateObj of selectableDays) {
|
||||
const distanceInMiles = provider?.distanceInMiles
|
||||
? Math.round(provider.distanceInMiles * 2) / 2
|
||||
: 0;
|
||||
const providerAddress = this.getFormattedShopAddress(
|
||||
provider?.address,
|
||||
distanceInMiles
|
||||
);
|
||||
let matchingTimeSlot = {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: this.estimatedServiceMinutesMinimum,
|
||||
maximum: this.estimatedServiceMinutesMaximum,
|
||||
},
|
||||
timeSlot: null,
|
||||
date: null,
|
||||
addressCopy: providerAddress.address1 + ", " + providerAddress.address2,
|
||||
provider: provider,
|
||||
};
|
||||
const isMatchingApptDay = dateObj.timeSlots.some(
|
||||
(slot) =>
|
||||
slot.id.includes(timeOfDay) &&
|
||||
(matchingTimeSlot.timeSlot = slot) &&
|
||||
(matchingTimeSlot.date = dateObj.date)
|
||||
);
|
||||
if (isMatchingApptDay && matchingTimeSlot) {
|
||||
return matchingTimeSlot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
async getFirstAvailableInshopApptsByTOD(timeOfDayArray) {
|
||||
let matchingApptNearestAM1 = this.findMatchingInshopAppt(
|
||||
this.selectableDatesInshop?.days,
|
||||
timeOfDayArray[0],
|
||||
this.selectedProvider
|
||||
);
|
||||
let matchingApptNearestPM1 = this.findMatchingInshopAppt(
|
||||
this.selectableDatesInshop?.days,
|
||||
timeOfDayArray[1],
|
||||
this.selectedProvider
|
||||
);
|
||||
let matchingApptNearestAM2;
|
||||
let matchingApptNearestPM2;
|
||||
let matchingApptNearestAM3;
|
||||
let matchingApptNearestPM3;
|
||||
|
||||
const providerNearest2 = this.shopProviderData.shopProviders[1];
|
||||
const providerNearest3 = this.shopProviderData.shopProviders[2];
|
||||
|
||||
if (providerNearest2?.distanceInMiles && providerNearest2.distanceInMiles < 25) {
|
||||
this.calendarLoadingStatus = "more";
|
||||
const selectableDaysFromProviderNearest2 = await getScheduleApiResponse({
|
||||
startDateString: this.initialViewStartDate,
|
||||
endDateString: this.initialViewEndDate,
|
||||
inshopProviderNumber: providerNearest2.providerNumber,
|
||||
zipCode: this.zipCode,
|
||||
includeMobileTimeSlots: false,
|
||||
includeInshopTimeSlots: true,
|
||||
});
|
||||
matchingApptNearestAM2 = this.findMatchingInshopAppt(
|
||||
selectableDaysFromProviderNearest2?.inshopTimeSlotsData?.days,
|
||||
"AM",
|
||||
providerNearest2
|
||||
);
|
||||
matchingApptNearestPM2 = this.findMatchingInshopAppt(
|
||||
selectableDaysFromProviderNearest2?.inshopTimeSlotsData?.days,
|
||||
"PM",
|
||||
providerNearest2
|
||||
);
|
||||
}
|
||||
if (providerNearest3?.distanceInMiles && providerNearest3?.distanceInMiles < 25) {
|
||||
this.calendarLoadingStatus = "more";
|
||||
const selectableDaysFromProviderNearest3 = await getScheduleApiResponse({
|
||||
startDateString: this.initialViewStartDate,
|
||||
endDateString: this.initialViewEndDate,
|
||||
inshopProviderNumber: providerNearest3.providerNumber,
|
||||
zipCode: this.zipCode,
|
||||
includeMobileTimeSlots: false,
|
||||
includeInshopTimeSlots: true,
|
||||
});
|
||||
matchingApptNearestAM3 = this.findMatchingInshopAppt(
|
||||
selectableDaysFromProviderNearest3?.inshopTimeSlotsData?.days,
|
||||
"AM",
|
||||
providerNearest3
|
||||
);
|
||||
matchingApptNearestPM3 = this.findMatchingInshopAppt(
|
||||
selectableDaysFromProviderNearest3?.inshopTimeSlotsData?.days,
|
||||
"PM",
|
||||
providerNearest3
|
||||
);
|
||||
}
|
||||
this.calendarLoadingStatus = "none";
|
||||
|
||||
// WHICH OF THE 3 INSHOP APPTS IS THE EARLIEST?
|
||||
const compareAppointments = (appt1, appt2) => {
|
||||
// Handle null/undefined appointments
|
||||
if (!appt1?.date) return 1; // appt1 is later (or invalid)
|
||||
if (!appt2?.date) return -1; // appt2 is later (or invalid)
|
||||
|
||||
// Compare dates first (YYYY-MM-DD format allows string comparison)
|
||||
if (appt1.date < appt2.date) return -1; // appt1 is earlier
|
||||
if (appt1.date > appt2.date) return 1; // appt2 is earlier
|
||||
|
||||
// Dates are equal, compare times (HH:MM format allows string comparison)
|
||||
const time1 = appt1.timeSlot?.startTime || "23:59";
|
||||
const time2 = appt2.timeSlot?.startTime || "23:59";
|
||||
|
||||
if (time1 < time2) return -1; // appt1 is earlier
|
||||
if (time1 > time2) return 1; // appt2 is earlier
|
||||
|
||||
return 0; // Completely equal
|
||||
};
|
||||
// Find earliest AM appointment
|
||||
const preferredApptAM =
|
||||
[matchingApptNearestAM1, matchingApptNearestAM2, matchingApptNearestAM3]
|
||||
.filter((appt) => appt?.date) // Remove null/undefined
|
||||
.sort(compareAppointments)[0] || null;
|
||||
|
||||
// Find earliest PM appointment
|
||||
const preferredApptPM =
|
||||
[matchingApptNearestPM1, matchingApptNearestPM2, matchingApptNearestPM3]
|
||||
.filter((appt) => appt?.date)
|
||||
.sort(compareAppointments)[0] || null;
|
||||
|
||||
return [preferredApptAM, preferredApptPM];
|
||||
},
|
||||
updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
|
||||
this.selectedMobileFirstAppointment = true;
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.selectedDate = timeSlotObj.date;
|
||||
|
|
@ -2061,15 +2364,68 @@ export default {
|
|||
this.updateTimeSlot(timeSlotObj);
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
updateMultiLocationTimeSlotandNavigateForward({ selectedTimeSlot, appointmentType }) {
|
||||
if (!selectedTimeSlot) return;
|
||||
this.selectedMultiLocationAppointment = true;
|
||||
this.selectedDate = selectedTimeSlot.date;
|
||||
if (appointmentType?.toLowerCase() == "mobile") {
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.updateSelectedProvider();
|
||||
this.updateTimeSlot(selectedTimeSlot);
|
||||
} else if (appointmentType?.toLowerCase() == "inshop") {
|
||||
this.appointmentType = AppointmentTypeStrings.IN_SHOP;
|
||||
|
||||
this.updateSelectedProvider(selectedTimeSlot.provider);
|
||||
|
||||
// UPDATE SELECTED TIME SLOT INFO
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: selectedTimeSlot.date,
|
||||
routeCode: selectedTimeSlot.routeCode,
|
||||
startTime: selectedTimeSlot.timeSlot.startTime,
|
||||
endTime: selectedTimeSlot.timeSlot.endTime,
|
||||
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
|
||||
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(),
|
||||
},
|
||||
isPremiumAppointment: selectedTimeSlot.isPremiumAppointment ? true : false,
|
||||
};
|
||||
|
||||
// UPDATE APPT TYPE
|
||||
this.appointmentType = this.getInShopOrDropOffApptType(selectedTimeSlot.routeCode);
|
||||
}
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
|
||||
this.isRecalAcknowledgedForScheduling = isAcknowledged;
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
getFormattedShopAddress(providerAddress, distanceInMiles) {
|
||||
const toTitleCase = (str) => {
|
||||
if (!str) return "";
|
||||
return str.replace(/\w\S*/g, function (txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
if (!providerAddress) return {};
|
||||
|
||||
const streetAddress = toTitleCase(providerAddress.streetAddress);
|
||||
const city = toTitleCase(providerAddress.city);
|
||||
const state = providerAddress.state;
|
||||
const zipCode = providerAddress.zipCode;
|
||||
|
||||
return {
|
||||
city: city,
|
||||
distance: `${distanceInMiles} mi`,
|
||||
address1: streetAddress,
|
||||
address2: `${city}, ${state} ${zipCode}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
appointmentTypeFromAppointmentTypeQuestion: {
|
||||
handler(newValue, oldValue) {
|
||||
this.handleAppointmentTypeChange(newValue); // v-if on appointmentTypeQuestion ensures dates have been loaded by now
|
||||
this.handleAppointmentTypeChange(newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2084,6 +2440,7 @@ export default {
|
|||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
multiLocationModal,
|
||||
recalAckModal,
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,13 @@ export default {
|
|||
}
|
||||
}
|
||||
baseMixin.methods.ResetExternalParamsAndHideModal();
|
||||
|
||||
//clear any validation errors for external parameter flow
|
||||
const form = vm.$refs.theForm;
|
||||
if (form) {
|
||||
form.setFieldError("DamageLocationQuestion", undefined);
|
||||
form.setFieldTouched("DamageLocationQuestion", false, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
baseMixin.methods.ResetExternalParamsAndHideModal();
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ export default {
|
|||
sessionData.isPia = order?.payment?.isPia ?? false;
|
||||
sessionData.piaType = order?.payment?.piaType;
|
||||
sessionData.parentAccountNumber = order?.payment?.parentAccountNumber;
|
||||
sessionData.billToAccountNumber = order?.payment?.billToAccountNumber;
|
||||
sessionData.settledTenderAmount = order?.settledTenderAmount;
|
||||
sessionData.recalRequired = containsRecalParts(order?.lineItems);
|
||||
sessionData.recalType = getRecalPartNumbers(order?.lineItems?.glassParts);
|
||||
|
|
|
|||
|
|
@ -209,6 +209,15 @@ export default {
|
|||
userAgent.includes("webOS")
|
||||
);
|
||||
},
|
||||
isAppleBrowser() {
|
||||
const userAgent = navigator.userAgent;
|
||||
return (
|
||||
userAgent.includes("iPod") ||
|
||||
userAgent.includes("iPad") ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("Mac")
|
||||
);
|
||||
},
|
||||
getSubmittedOrder() {
|
||||
return JSON.parse(
|
||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,14 @@ export async function consumeReferralQuerystrings() {
|
|||
// If no referral number in querystring, check if heritage funnel updated last.
|
||||
// This would indicate a customer that went to heritage but did not return through the
|
||||
// normal route. ie: left the site in search of a discount and returned without querystrings.
|
||||
|
||||
//or if a referral number is in query string check if heritage funnel updated last
|
||||
// This would indicate a customer that returned through normal route like from sfa email
|
||||
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
|
||||
if (didHeritageFunnelUpdateLast && !referralNumber) {
|
||||
referralNumber = getFunnelCookie().ReferralNumber;
|
||||
parentAccount = getFunnelCookie().ReferralParentAccountNumber;
|
||||
correlationId = getFunnelCookie().ReferralCorrelationId;
|
||||
if (didHeritageFunnelUpdateLast) {
|
||||
referralNumber = referralNumber ?? getFunnelCookie().ReferralNumber;
|
||||
parentAccount = parentAccount ?? getFunnelCookie().ReferralParentAccountNumber;
|
||||
correlationId = correlationId ?? getFunnelCookie().ReferralCorrelationId;
|
||||
referralDate = getFunnelCookie().ReferralDate;
|
||||
savedSessionId = getFunnelCookie().SavedSessionId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getBoolFromString } from "@/helpers/boolean-helper";
|
|||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
|
||||
import { hasVehicleInfo } from "@/helpers/vehicle-helper";
|
||||
import store from "@/store";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
|
|
@ -33,7 +34,7 @@ export async function vehicleBeforeEnter(to, from) {
|
|||
}
|
||||
}
|
||||
|
||||
if (isVerifiedInsurance()) {
|
||||
if (isVerifiedInsurance() && hasVehicleInfo()) {
|
||||
return {
|
||||
name: routeData.VEHICLE_DAMAGE.name,
|
||||
replace: true,
|
||||
|
|
|
|||
|
|
@ -1645,6 +1645,7 @@ export const actions = {
|
|||
isPia,
|
||||
piaType,
|
||||
parentAccountNumber,
|
||||
billToAccountNumber,
|
||||
settledTenderAmount,
|
||||
recalRequired,
|
||||
recalType,
|
||||
|
|
@ -1718,6 +1719,7 @@ export const actions = {
|
|||
totalPrice: totalPrice,
|
||||
userAgent: userAgent,
|
||||
cashPriceSubTotal: cashPriceSubTotal,
|
||||
billToAccountNumber: billToAccountNumber,
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
|
|
@ -2126,14 +2128,14 @@ export const actions = {
|
|||
},
|
||||
async submitDonationPartToSV2(
|
||||
context,
|
||||
{ payload: { donationAmount, submittedOrder }, pageNameToLog }
|
||||
{ payload: { selectedItemNumber, submittedOrder }, pageNameToLog }
|
||||
) {
|
||||
const state = JSON.parse(
|
||||
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
|
||||
);
|
||||
const order = state.order;
|
||||
const payload = {
|
||||
donationAmount: parseInt(donationAmount),
|
||||
selectedItemNumber: selectedItemNumber,
|
||||
savedSessionId: state.applicationUser.savedSessionId,
|
||||
paymentInfo: {
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
|
|
@ -3138,6 +3140,8 @@ export const actions = {
|
|||
serviceLocationState,
|
||||
serviceLocationZipCode,
|
||||
pricedLineItems,
|
||||
serviceLocationAddressLine1,
|
||||
serviceLocationAddressLine2,
|
||||
},
|
||||
pageNameToLog,
|
||||
}
|
||||
|
|
@ -3171,6 +3175,8 @@ export const actions = {
|
|||
appointmentType: appointmentType,
|
||||
pricedLineItems: lineItemsWithOnlyPriceInfo,
|
||||
serviceLocation: {
|
||||
addressLine1: appointmentType == "Mobile" ? serviceLocationAddressLine1 : null,
|
||||
addressLine2: appointmentType == "Mobile" ? serviceLocationAddressLine2 : null,
|
||||
city: appointmentType == "Mobile" ? serviceLocationCity : null,
|
||||
state: appointmentType == "Mobile" ? serviceLocationState : null,
|
||||
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
|
||||
|
|
|
|||
|
|
@ -20,3 +20,9 @@
|
|||
}
|
||||
// Usage: @include responsive-font-size-md(mobile-font-size-in-rem, desktop-font-size-in-rem);
|
||||
// Example: @include responsive-font-size-md(0.875rem, 1rem);
|
||||
|
||||
// Filter to use on SVG elements that are background
|
||||
@mixin blue-filter {
|
||||
filter: brightness(0) saturate(100%) invert(31%) sepia(68%) saturate(1676%) hue-rotate(186deg)
|
||||
brightness(93%) contrast(110%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ $body-color: $gray-600;
|
|||
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||
$font-family-monospace:
|
||||
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
$font-family-sans-serif-semibold: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-bold: UrbanistBold, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-extrabold: UrbanistExtraBold, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-medium-italic: UrbanistMedium_Italic, Arial, Helvetica, sans-serif;
|
||||
$font-family-sans-serif-bold-italic: UrbanistBold_Italic, Arial, Helvetica, sans-serif;
|
||||
|
||||
// stylelint-enable value-keyword-case
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
$font-family-code: $font-family-monospace;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,18 @@
|
|||
isDismissible ? 'alert-dismissible' : '',
|
||||
this.alertClass,
|
||||
this.cssClassNameForCmsWidget,
|
||||
this.hasBorder ? 'bordered' : '',
|
||||
]">
|
||||
<p class="mx-6 my-0 fw-bold alert-heading">{{ alertHeadline }}</p>
|
||||
<p class="mx-6 my-0 fw-bold alert-heading">
|
||||
<img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
|
||||
<img
|
||||
v-if="showWarningIcon"
|
||||
:src="warningIcon"
|
||||
alt="Warning Icon"
|
||||
class="warning-icon" />
|
||||
{{ alertHeadline }}
|
||||
</p>
|
||||
<hr v-if="showHorizontalRow" />
|
||||
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
|
||||
<p
|
||||
class="mx-4 mt-1 mb-0 text-body small"
|
||||
|
|
@ -58,6 +68,10 @@ import textBlock from "@/digital-components/text-block/text-block";
|
|||
export default {
|
||||
name: "alert",
|
||||
props: {
|
||||
showInfoIcon: Boolean,
|
||||
showWarningIcon: Boolean,
|
||||
showHorizontalRow: Boolean,
|
||||
hasBorder: Boolean,
|
||||
isDismissible: Boolean,
|
||||
alertClass: String,
|
||||
/*
|
||||
|
|
@ -87,6 +101,12 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
infoIcon() {
|
||||
return require(`@/assets/img/icons/info-circle-blue.svg`);
|
||||
},
|
||||
warningIcon() {
|
||||
return require(`@/assets/img/icons/alert-circle-yellow.svg`);
|
||||
},
|
||||
pageQueryString() {
|
||||
return applicationConfig.PAGE_QUERYSTRING;
|
||||
},
|
||||
|
|
@ -148,6 +168,12 @@ export default {
|
|||
border-radius: 0.5rem;
|
||||
border-width: 0px;
|
||||
|
||||
hr {
|
||||
border: 1px solid $blue;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -196,12 +222,19 @@ export default {
|
|||
background-color: $yellow-100;
|
||||
.alert-heading {
|
||||
color: $yellow-600;
|
||||
|
||||
.warning-icon {
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
fill: $yellow-600;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
hr {
|
||||
border-color: $yellow-800;
|
||||
}
|
||||
}
|
||||
&.alert-success {
|
||||
background-color: $green-100;
|
||||
|
|
@ -214,6 +247,9 @@ export default {
|
|||
height: 1rem;
|
||||
}
|
||||
}
|
||||
&.bordered {
|
||||
border: 1px solid;
|
||||
}
|
||||
& p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue