Merge branch 'release/2026.04.09' into feature/CASH-1947

This commit is contained in:
credelinghuys 2026-03-31 12:09:30 -04:00
commit 27c54d591b
44 changed files with 793 additions and 327 deletions

View file

@ -10,6 +10,7 @@ SKIP_CONTENT_SITE="false"
# Experiments Flag # Experiments Flag
IS_MOBILEFIRST="false" IS_MOBILEFIRST="false"
IS_ADYENPAYMENTS="false" IS_ADYENPAYMENTS="false"
IS_MULTILOCATIONPOPUP="false"
# Base URLs by environment # Base URLs by environment
# qa # qa

View file

@ -10,6 +10,7 @@ SKIP_CONTENT_SITE="false"
# Experiments Flag # Experiments Flag
IS_MOBILEFIRST="false" IS_MOBILEFIRST="false"
IS_ADYENPAYMENTS="false" IS_ADYENPAYMENTS="false"
IS_MULTILOCATIONPOPUP="false"
# Base URLs by environment # Base URLs by environment
# qa # qa

View file

@ -18,5 +18,6 @@ export function getDefaultExperimentsData(): IExperiments {
return { return {
isAdyenPayments: !!process.env.IS_ADYENPAYMENTS && process.env.IS_ADYENPAYMENTS !== "" ? process.env.IS_ADYENPAYMENTS === "true" : false, 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, 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,
} }
} }

View file

@ -1,4 +1,5 @@
export interface IExperiments { export interface IExperiments {
isMobileFirst: boolean, isMobileFirst: boolean,
isAdyenPayments: boolean isAdyenPayments: boolean,
isMultiLocationPopup: boolean
} }

View file

@ -257,8 +257,12 @@ export class BasePage {
: "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true"; : "MobileFirstAppointment=MobileFirstAppt_V1=MobileFirstAppt_CONTROL=true";
experimentsURLExtension += experiments?.isAdyenPayments experimentsURLExtension += experiments?.isAdyenPayments
? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)" ? ",Adyen%20Payments=Adyen%20Payment%20Test=Adyen%20Payment%20(Test)=true"
: ",Adyen%20Payments=Adyen%20Payment%20Test=CyberSource%20(Control)"; : ",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 { } else {
console.log("Url extension without query string"); console.log("Url extension without query string");
} }

View file

@ -20,9 +20,8 @@ export class HomePage extends BasePage {
readonly paymentOptionDropdown: Locator; readonly paymentOptionDropdown: Locator;
readonly viewQuoteButton: Locator; readonly viewQuoteButton: Locator;
//Zip Entry //Zip Entry
readonly enterServiceZipTextBox: Locator; readonly widgetZipEntryField: Locator;
readonly zipEntryLetsGetStartedButton: Locator;
readonly getQuoteAndScheduleButton: Locator; readonly getQuoteAndScheduleButton: Locator;
url = process.env['BASE_URL']!; url = process.env['BASE_URL']!;
@ -31,7 +30,7 @@ export class HomePage extends BasePage {
super(page); super(page);
this.page = page; this.page = page;
this.letsGetStartedButton = this.page.locator('#zipCodeTextboxButton'); this.letsGetStartedButton = this.page.getByRole('button', { name: 'Let\'s get started' });
this.cusmodalPopup = this.page.locator('#Cusmodalpopup'); this.cusmodalPopup = this.page.locator('#Cusmodalpopup');
this.closePopupButton = this.page.getByRole('button', { name: '×' }); this.closePopupButton = this.page.getByRole('button', { name: '×' });
@ -47,8 +46,7 @@ export class HomePage extends BasePage {
this.viewQuoteButton = this.page.locator('#ctaSubmit'); this.viewQuoteButton = this.page.locator('#ctaSubmit');
//Zip Entry //Zip Entry
this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox'); this.widgetZipEntryField = this.page.getByRole('textbox', { name: 'Enter service ZIP code' });
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextbox');
this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' }); this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' });
} }
@ -63,7 +61,7 @@ export class HomePage extends BasePage {
async letsGetStarted(zip: string, enterFunnelWithZip: boolean) { async letsGetStarted(zip: string, enterFunnelWithZip: boolean) {
if (enterFunnelWithZip) { if (enterFunnelWithZip) {
await this.zipEntryLetsGetStartedButton.fill(zip); await this.widgetZipEntryField.fill(zip);
/* await this.letsGetStartedButton.evaluate((element, zip) => { /* await this.letsGetStartedButton.evaluate((element, zip) => {
const currentHref = element.getAttribute('href') || ''; const currentHref = element.getAttribute('href') || '';
element.setAttribute('href', `${currentHref}?zipCode=${zip}`); element.setAttribute('href', `${currentHref}?zipCode=${zip}`);

View 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);
}
}

View file

@ -8,6 +8,8 @@ import { AfterpayPage } from './AfterpayPage';
import { PaypalPage } from './PaypalPage'; import { PaypalPage } from './PaypalPage';
import { ITestData } from 'framework/TestData'; import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step'; import { step } from 'framework/localTypes/Step';
import { IExperiments } from 'framework/localTypes/IExperiments';
import { PaymentAdyenPage } from './PaymentAdyenPage';
export class PaymentMethodPage extends BasePage { export class PaymentMethodPage extends BasePage {
readonly page: Page; readonly page: Page;
@ -25,7 +27,9 @@ export class PaymentMethodPage extends BasePage {
readonly submitButton: Locator; readonly submitButton: Locator;
readonly recalibrationCheckbox: Locator; readonly recalibrationCheckbox: Locator;
readonly paymentPage: PaymentPage; readonly paymentPage: PaymentPage;
readonly paymentAdyenPage: PaymentAdyenPage;
readonly paypalPage: PaypalPage; readonly paypalPage: PaypalPage;
readonly afterpayPage: AfterpayPage
readonly afterPayBreakoutSection: Locator; readonly afterPayBreakoutSection: Locator;
readonly afterPayToggle: Locator; readonly afterPayToggle: Locator;
@ -59,7 +63,9 @@ export class PaymentMethodPage extends BasePage {
this.recalibrationCheckbox = this.page.locator('label:has(>input[name=\'recalAckOptIn\'])'); this.recalibrationCheckbox = this.page.locator('label:has(>input[name=\'recalAckOptIn\'])');
// this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1); // this.creditCardButton = page.locator('div').filter({ hasText: /^Credit or Debit$/ }).nth(1);
this.paymentPage = new PaymentPage(page); this.paymentPage = new PaymentPage(page);
this.paymentAdyenPage = new PaymentAdyenPage(page);
this.paypalPage = new PaypalPage(page); this.paypalPage = new PaypalPage(page);
this.afterpayPage = new AfterpayPage(page);
// Payment details validation locators // Payment details validation locators
this.reviewTable = this.page.locator('div.review-table'); this.reviewTable = this.page.locator('div.review-table');
@ -191,35 +197,57 @@ export class PaymentMethodPage extends BasePage {
return text; return text;
} }
async executePayment(paymentDetails: IPaymentDetails, isRecalVehicle: boolean) { async executePayment(testData: Partial<ITestData>) {
const browserContext = this.page.context(); const browserContext = this.page.context();
const { paymentDetails, isRecalVehicle, experiments } = testData;
switch (paymentDetails.paymentType) { switch (paymentDetails!.paymentType) {
case PaymentType.Credit: case PaymentType.Credit:
await this.selectCreditCard(); await this.selectCreditCard();
await this.nextPage(); await this.nextPage();
await this.paymentPage.populateCreditCardDetails(paymentDetails); experiments!.isAdyenPayments
? await this.paymentAdyenPage.populateAdyenCreditCardDetails(testData)
: await this.paymentPage.populateCreditCardDetails(paymentDetails!);
break; break;
case PaymentType.AfterPay: case PaymentType.AfterPay:
await this.payInFourButton.click(); await this.payInFourButton.click();
await this.continueButton.click(); await this.continueButton.click();
// Capture popup if (experiments?.isAdyenPayments) {
const afterpayPopup = await browserContext.waitForEvent('page'); await this.paymentAdyenPage.afterPayButton.click();
const afterpayPage = new AfterpayPage(afterpayPopup); 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; break;
case PaymentType.Paypal: case PaymentType.Paypal:
await this.selectPaypal(); await this.selectPaypal();
// TODO: Click paypal button // TODO: Click paypal button
await this.nextPage(); await this.nextPage();
await this.paymentPage.navigateToPaypalCheckout(); const paypalPage = experiments?.isAdyenPayments
await this.paypalPage.completePaypalPurchase(paymentDetails); ? 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; break;
case PaymentType.PayAtService: case PaymentType.PayAtService:
await this.selectPayAtService(isRecalVehicle); await this.selectPayAtService(isRecalVehicle!);
await this.nextPage(); await this.nextPage();
break; break;
case PaymentType.PayWithInsurance: case PaymentType.PayWithInsurance:
@ -527,7 +555,7 @@ export class PaymentMethodPage extends BasePage {
@step("PaymentMethodPage >> Select Payment Method: ") @step("PaymentMethodPage >> Select Payment Method: ")
async handlePaymentMethodPage(testData: Partial<ITestData>) { 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.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
await this.validatePaymentDetailsPage(testData); await this.validatePaymentDetailsPage(testData);
@ -542,7 +570,8 @@ export class PaymentMethodPage extends BasePage {
await this.verifyVAPS(); await this.verifyVAPS();
} }
if (paymentDetails?.paymentType) { if (paymentDetails?.paymentType) {
await this.executePayment(paymentDetails!, isRecalVehicle!); // await this.executePayment(paymentDetails!, isRecalVehicle!, experiments!);
await this.executePayment(testData);
} else { } else {
await this.nextPage(); await this.nextPage();
} }

View file

@ -1,6 +1,7 @@
import { type Locator, type Page } from '@playwright/test'; import { type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage'; import { BasePage } from './BasePage';
import { IPaymentDetails } from 'safelite-playwright-core'; import { IPaymentDetails } from 'safelite-playwright-core';
import { PaypalPage } from './PaypalPage';
export class PaymentPage extends BasePage { export class PaymentPage extends BasePage {
readonly page: Page; readonly page: Page;
@ -43,8 +44,8 @@ export class PaymentPage extends BasePage {
await this.submitPaymentButton.click(); await this.submitPaymentButton.click();
} }
async navigateToPaypalCheckout() { async navigateToPaypalCheckout(): Promise<PaypalPage> {
await this.payPalButton.click(); await this.payPalButton.click();
return new PaypalPage(this.page);
} }
} }

View file

@ -1,6 +1,6 @@
import { expect, type Locator, type Page } from '@playwright/test'; import { expect, type Locator, type Page } from '@playwright/test';
import { BasePage } from './BasePage'; import { BasePage } from './BasePage';
import { IPaymentDetails } from 'safelite-playwright-core'; import { ITestData } from 'framework/TestData';
export class PaypalPage extends BasePage { export class PaypalPage extends BasePage {
readonly page: Page; readonly page: Page;
@ -30,24 +30,28 @@ export class PaypalPage extends BasePage {
this.tryAnotherWayButton = page.getByRole('button', { name: 'Try another way' }); 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 // Wait for the PayPal login page to load
await this.page.waitForFunction(() => window.location.href.includes('sandbox.paypal.com'), null, { timeout: 10000 }); 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 // handle flow with and without the "Log in with a password instead" link
if (await this.loginWithPasswordButton.isVisible()) { if (await this.loginWithPasswordButton.isVisible()) {
await this.loginWithPasswordButton.click(); await this.loginWithPasswordButton.click();
await this.passwordTextBox.fill(paymentDetails.password!); await this.passwordTextBox.fill(paymentDetails!.password!);
await this.paypalLoginButton.click(); await this.paypalLoginButton.click();
await this.completePurchaseButton.click(); await this.completePurchaseButton.click();
} else { } else {
await this.usernameTextBox.waitFor({ state: 'visible' }); await this.usernameTextBox.waitFor({ state: 'visible' });
await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true }); 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.nextButton.click();
await this.tryAnotherWayButton.click(); if (!experiments!.isAdyenPayments){
await this.usePasswordInsteadButton.click(); await this.tryAnotherWayButton.click();
await this.passwordTextBox.fill(paymentDetails.password!); await this.usePasswordInsteadButton.click();
}
await this.passwordTextBox.fill(paymentDetails!.password!);
await this.paypalLoginButton.click(); await this.paypalLoginButton.click();
await this.payWithRadioButton.click(); await this.payWithRadioButton.click();
await this.page.waitForTimeout(2000); await this.page.waitForTimeout(2000);

View 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

View file

@ -43,6 +43,7 @@ const GaLabels = {
ADDRESS_LOOKUP: "Address_Look_up", ADDRESS_LOOKUP: "Address_Look_up",
YES: "yes", YES: "yes",
NO: "no", NO: "no",
GLASS_CASH_QUOTE: "GlassCashQuote",
}; };
const ValueToLogTypes = { const ValueToLogTypes = {

View file

@ -180,10 +180,6 @@ const endpoints = {
url: "/analytics/api/v1/analytics/log-part-questions", url: "/analytics/api/v1/analytics/log-part-questions",
method: "POST", method: "POST",
}, },
LogDigitalConsumer: {
url: "/analytics/api/v1/analytics/digitalconsumer-log",
method: "POST",
},
LogFmgSessionData: { LogFmgSessionData: {
url: "/analytics/api/v1/analytics/digitalconsumer-session-logging", url: "/analytics/api/v1/analytics/digitalconsumer-session-logging",
method: "POST", method: "POST",

View file

@ -19,12 +19,11 @@ const experimentSettings = {
PIA_INSURANCE: "DisplayPIAInsurance", PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA", SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional", IS_EMAIL_OPTIONAL: "isEmailOptional",
SERVICE_PACKAGE_DISCOUNT: "OfferServicePackageDiscount",
PROMO_ON_PACKAGE: "Offer_Promo_On_Pkg",
RECAL_PRICE_REMOVE: "RecalPriceRemove", RECAL_PRICE_REMOVE: "RecalPriceRemove",
INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL: "NextGen_InternalInsuranceTabDisplayThreshold", INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL: "NextGen_InternalInsuranceTabDisplayThreshold",
INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL: "NextGen_ExternalInsuranceTabDisplayThreshold", INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL: "NextGen_ExternalInsuranceTabDisplayThreshold",
DISPLAY_MSR: "DisplayMSR", DISPLAY_MSR: "DisplayMSR",
ENABLE_MSR_SPLIT_PAY: "EnableMSRSplitPay",
DISPLAY_WAITLIST: "DisplayWaitlist2.0", DISPLAY_WAITLIST: "DisplayWaitlist2.0",
WAITLIST_THRESHOLD_DAYS: "Waitlist_Threshold_Days", WAITLIST_THRESHOLD_DAYS: "Waitlist_Threshold_Days",
PRICING_BY_DAY: "DisplayPricingByDay", PRICING_BY_DAY: "DisplayPricingByDay",

View file

@ -9,7 +9,6 @@ const partTypeStrings = {
MOBILE_FEE: "MOBILE FEE", MOBILE_FEE: "MOBILE FEE",
REPAIR_FEE: "REPAIR FEE", REPAIR_FEE: "REPAIR FEE",
EARLY_BIRD: "EARLY BIRD", EARLY_BIRD: "EARLY BIRD",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT", QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION", DONATION: "DONATION",
WINDSHIELD: "WINDSHIELD", WINDSHIELD: "WINDSHIELD",

View file

@ -63,7 +63,6 @@ const storeActions = {
LOG_CUSTOM_EVENT: "logCustomEvent", LOG_CUSTOM_EVENT: "logCustomEvent",
INITIALIZE_SESSION: "initializeSession", INITIALIZE_SESSION: "initializeSession",
LOG_PART_QUESTIONS: "logPartQuestions", LOG_PART_QUESTIONS: "logPartQuestions",
LOG_DIGITALCONSUMER: "logDigitalConsumer",
LOG_FMG_SESSION_DATA: "logFmgSessionData", LOG_FMG_SESSION_DATA: "logFmgSessionData",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
@ -114,6 +113,7 @@ const storeActions = {
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling", SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",
SAVE_IS_MSR_FEE_COVERED_BY_INSURANCE: "saveIsMSRFeeCoveredByInsurance",
CREATE_SUBMITTED_STATE: "createSubmittedState", CREATE_SUBMITTED_STATE: "createSubmittedState",
RESET_SUBMITTED_STATE: "resetSubmittedState", RESET_SUBMITTED_STATE: "resetSubmittedState",
@ -123,6 +123,8 @@ const storeActions = {
SAVE_LOGGING_OPTION: "saveLoggingOption", SAVE_LOGGING_OPTION: "saveLoggingOption",
UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError",
GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey",
CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry",
}; };
export { storeActions }; export { storeActions };

View file

@ -72,6 +72,7 @@ const storeMutations = {
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling", UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected", UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_IS_MSR_FEE_COVERED_BY_INSURANCE: "updateIsMSRFeeCoveredByInsurance",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
@ -98,6 +99,7 @@ const storeMutations = {
UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited",
UPDATE_LOGGING_OPTION: "updateLoggingOption", UPDATE_LOGGING_OPTION: "updateLoggingOption",
UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError",
UPDATE_IDEMPOTENCY_KEY: "updateIdempotencyKey",
// EXPERIMENT MUTATIONS // EXPERIMENT MUTATIONS
UPDATE_EXPERIMENTS: "updateExperiments", UPDATE_EXPERIMENTS: "updateExperiments",

View file

@ -77,7 +77,7 @@
i % 2 == 0 ? 'even' : 'odd', i % 2 == 0 ? 'even' : 'odd',
cartItem.isRemovable ? 'removable-cart-item' : '', cartItem.isRemovable ? 'removable-cart-item' : '',
]"> ]">
<span v-if="cartItem != recycleFeeCartItem">{{ cartItem.name }}</span> <span v-if="!this.shouldShowInfoIcon(cartItem)">{{ cartItem.name }}</span>
<textLink <textLink
v-if="allowItemRemoval && cartItem.isRemovable" v-if="allowItemRemoval && cartItem.isRemovable"
ref="removeLink" ref="removeLink"
@ -93,6 +93,15 @@
<img :src="infoIcon" alt="Info Icon" class="info-icon" /> <img :src="infoIcon" alt="Info Icon" class="info-icon" />
</a> </a>
</span> </span>
<span v-if="cartItem == msrFeeCartItem">
{{ msrFeeCartItem.name }}
<a
href="javascript:void(0)"
@click="openModal(msrModalCmsWidgetName)"
id="msr-fee-cartItem">
<img :src="infoIcon" alt="Info Icon" class="info-icon" />
</a>
</span>
<span <span
v-if=" v-if="
!showCoverageAsVerified || !showCoverageAsVerified ||
@ -138,6 +147,10 @@
:pageName="pageName" :pageName="pageName"
modalWidgetName="PromoModalWidget" modalWidgetName="PromoModalWidget"
@promoAdded="saveVaps" /> @promoAdded="saveVaps" />
<removeMsrFeeModal
modalWidgetName="RemoveMsrFeeModalWidget"
ref="removeMsrFeeModal"
@switchToInshop="switchToInshop" />
</div> </div>
</div> </div>
<div <div
@ -153,6 +166,10 @@
<contentGroupModal ref="RecycleModal" cmsWidgetName="RecycleModal"> <contentGroupModal ref="RecycleModal" cmsWidgetName="RecycleModal">
<textBlock cmsWidgetName="RecycleTextBlock" /> <textBlock cmsWidgetName="RecycleTextBlock" />
</contentGroupModal> </contentGroupModal>
<contentGroupModal
ref="MSRModal"
cmsWidgetName="MSRModal"
:customBodyText="msrModalTextBlock"></contentGroupModal>
</div> </div>
</template> </template>
@ -162,6 +179,7 @@ import textLink from "@/ux-components/text-link/text-link";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal"; import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question"; import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question";
import removeMsrFeeModal from "./remove-msr-fee-modal/remove-msr-fee-modal.vue";
// Mixins // Mixins
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
@ -183,6 +201,7 @@ import {
getSalesTax, getSalesTax,
getAmountDueWithDonation, getAmountDueWithDonation,
} from "@/helpers/pricing-helper.js"; } from "@/helpers/pricing-helper.js";
import { formatToUSDollar } from "@/helpers/cms-content-helper";
// Constants // Constants
import { partTypeStrings } from "@/constants/part-type-strings"; import { partTypeStrings } from "@/constants/part-type-strings";
@ -191,6 +210,7 @@ import { cartItemTypes } from "@/constants/cart-item-types";
import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance"; import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { quotePageDiscountTable } from "@/constants/quote-page-discounts"; import { quotePageDiscountTable } from "@/constants/quote-page-discounts";
import { partNumberStrings } from "@/constants/part-number-strings";
export default { export default {
name: "cart", name: "cart",
@ -203,6 +223,7 @@ export default {
pageName: String, pageName: String,
allowItemRemoval: Boolean, allowItemRemoval: Boolean,
recyclingModalCmsWidgetName: String, recyclingModalCmsWidgetName: String,
msrModalCmsWidgetName: String,
showAsPaid: Boolean, showAsPaid: Boolean,
isInsurance: Boolean, isInsurance: Boolean,
insuranceDeductible: Number, insuranceDeductible: Number,
@ -214,6 +235,7 @@ export default {
isExpandedOnLoad: Boolean, isExpandedOnLoad: Boolean,
isCollapsible: { type: Boolean, default: true }, isCollapsible: { type: Boolean, default: true },
isMSRFeeApplicable: Boolean, isMSRFeeApplicable: Boolean,
IsMSRFeeCoveredByInsurance: Boolean,
donationCartItem: Object, donationCartItem: Object,
}, },
data() { data() {
@ -311,6 +333,10 @@ export default {
}, },
async removeItem(cartItemType, category) { async removeItem(cartItemType, category) {
if (cartItemType == cartItemTypes.MOBILE_FEE && this.isMSRFeeApplicable) {
this.$refs.removeMsrFeeModal.openModal();
return false;
}
this.lineItems[category] = this.lineItems[category].filter( this.lineItems[category] = this.lineItems[category].filter(
(lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType (lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType
); );
@ -375,6 +401,12 @@ export default {
} }
return []; return [];
}, },
shouldShowInfoIcon(cartItem) {
return cartItem == this.recycleFeeCartItem || cartItem == this.msrFeeCartItem;
},
switchToInshop() {
this.$emit("switchToInshop");
},
}, },
computed: { computed: {
isCartReadyToLoad() { isCartReadyToLoad() {
@ -490,6 +522,10 @@ export default {
cartItems.push(this.mobileFeeCartItem); cartItems.push(this.mobileFeeCartItem);
} }
if (this.msrFeeCartItem) {
cartItems.push(this.msrFeeCartItem);
}
if (this.quotePageDiscountCartItem) { if (this.quotePageDiscountCartItem) {
cartItems.push(this.quotePageDiscountCartItem); cartItems.push(this.quotePageDiscountCartItem);
} }
@ -892,7 +928,9 @@ export default {
let cartItem = null; let cartItem = null;
let showMobileFeeCartItem = false; let showMobileFeeCartItem = false;
const mobileFeeLineItem = this.supportingItems.find( const mobileFeeLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.MOBILE_FEE (lineItem) =>
lineItem.partType == partTypeStrings.MOBILE_FEE &&
lineItem.partNumber == partTypeStrings.MOBILE_FEE
); );
var mobileTotal = var mobileTotal =
@ -929,6 +967,68 @@ export default {
} }
return cartItem; return cartItem;
}, },
msrFeeCartItemName() {
return this.getCmsContent("MSRFeeTextWidget", "Text");
},
getMsrFeeLineItem() {
return this.supportingItems.find(
(lineItem) =>
lineItem.partType == partTypeStrings.MOBILE_FEE &&
lineItem.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
);
},
msrFeeCartItem() {
let cartItem = null;
const msrFeeLineItem = this.getMsrFeeLineItem;
const shouldCreateMsrFeeCartItem =
this.isMSRFeeApplicable &&
this.isInsurance &&
!this.IsMSRFeeCoveredByInsurance &&
msrFeeLineItem &&
this.msrFeeAmount > 0;
if (shouldCreateMsrFeeCartItem) {
cartItem = {
name: this.msrFeeCartItemName,
category: cartItemCategories.SUPPORTING_ITEMS,
cartItemType: cartItemTypes.MOBILE_FEE,
isDisplayed: this.isInsurance,
isRemovable: true,
subTotal: 0,
salesTax: 0,
lineItems: [],
isCoveredByInsurance: this.IsMSRFeeCoveredByInsurance,
};
msrFeeLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(msrFeeLineItem);
cartItem.subTotal += this.msrFeeAmount;
cartItem.salesTax += msrFeeLineItem.salesTax ?? 0;
}
return cartItem;
},
msrFeeAmount() {
const msrFeeLineItem = this.getMsrFeeLineItem;
if (!msrFeeLineItem) {
return 0;
}
return (
msrFeeLineItem.laborAmount + msrFeeLineItem.sellingPrice + msrFeeLineItem.kitPrice
);
},
msrModalTextBlock() {
let cmsContentText = this.getCmsContent("MSRModal", "BodyText");
if (cmsContentText) {
cmsContentText = cmsContentText.replaceAll(
"{custom:mobileFee}",
formatToUSDollar(this.msrFeeAmount)
);
cmsContentText = cmsContentText.replaceAll(
"{custom:submittedOrder.policy.insuranceCompanyName}",
this.insuranceCompanyName
);
}
return cmsContentText;
},
quotePageDiscountCartItemName() { quotePageDiscountCartItemName() {
return this.getCmsContent("ServicePackageDiscountTextWidget", "Text"); return this.getCmsContent("ServicePackageDiscountTextWidget", "Text");
}, },
@ -1189,6 +1289,7 @@ export default {
textLink, textLink,
contentGroupModal, contentGroupModal,
promoModalQuestion, promoModalQuestion,
removeMsrFeeModal,
}, },
}; };
</script> </script>
@ -1289,6 +1390,10 @@ export default {
order: 3; order: 3;
margin-right: 100%; margin-right: 100%;
text-decoration: none; text-decoration: none;
&#msr-fee-cartItem {
margin: 0 0 0 0.2rem;
}
} }
} }
.package-type, .package-type,

View file

@ -0,0 +1,153 @@
<template>
<div id="msr-fee-modal-container">
<modal
:ref="modalName"
:headerText="modalHeaderText"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:isFooterButtonPrimary="true"
@footer-button-event="switchToInshop"
@isModalOpened="setIsModalOpen">
<p class="modal-body-inner" v-html="modalBodyText"></p>
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot:modal-footer-slot>
<button
type="button"
class="btn btn-link"
id="cancel-button"
:disabled="isLoading"
@click="closeModal">
{{ buttonText }}
</button>
</template>
</modal>
</div>
</template>
<script>
// Supporting files
import modal from "@/digital-components/modal/modal";
export default {
name: "remove-msr-fee-modal",
props: {
cmsWidgetName: String,
modalWidgetName: String,
},
data() {
return {
isModalOpen: false,
switchedToInshop: false,
};
},
methods: {
openModal() {
this.modal.openModal();
},
onModalClosed() {
this.$emit("close-remove-msr-fee-modal");
this.switchedToInshop = false;
},
closeModal() {
this.modal.closeModal();
},
getIsModalOpen() {
return this.isModalOpen;
},
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
switchToInshop() {
this.switchedToInshop = true;
this.$emit("switchToInshop");
this.modal.closeModal();
},
},
computed: {
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
modalSubHeaderText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
return this.getCmsContent(this.modalWidgetName, "BodyText");
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
buttonText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
},
components: { modal },
};
</script>
<style lang="scss">
#msr-fee-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;
& > span {
color: $red;
font-size: $font-size-14;
font-weight: $font-weight-600;
text-transform: uppercase;
}
.modal-title {
font-size: $font-size-20;
font-weight: $font-weight-normal;
}
}
.modal-body {
padding: 0.5rem 1rem;
.modal-body-inner {
ul {
list-style: none;
padding: 1rem;
background-color: #f4f4f4;
font-size: $font-size-14;
}
}
}
.modal-footer {
flex-flow: wrap-reverse;
#cancel-button {
width: 100%;
text-decoration: none;
margin-top: 1.5rem;
font-weight: $font-weight-600;
}
}
}
}
}
}
</style>

View file

@ -26,6 +26,7 @@ export default {
name: "content-group-modal", name: "content-group-modal",
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
customBodyText: String,
footerButtonActionName: { footerButtonActionName: {
type: String, type: String,
default: null, default: null,
@ -43,6 +44,9 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText"); return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
}, },
ModalBodyText() { ModalBodyText() {
if (this.customBodyText) {
return this.customBodyText;
}
return this.getCmsContent(this.cmsWidgetName, "BodyText"); return this.getCmsContent(this.cmsWidgetName, "BodyText");
}, },
ModalSubBodyText() { ModalSubBodyText() {

View file

@ -102,7 +102,6 @@ export default {
}, },
(error) => { (error) => {
if ( if (
endpoint.toLowerCase().includes(endpoints.LogDigitalConsumer.url) ||
endpoint.toLowerCase().includes(endpoints.LogFmgSessionData.url) || endpoint.toLowerCase().includes(endpoints.LogFmgSessionData.url) ||
endpoint.toLowerCase().includes(endpoints.LogPageView.url) || endpoint.toLowerCase().includes(endpoints.LogPageView.url) ||
endpoint.toLowerCase().includes(endpoints.LogCustomEvent.url) || endpoint.toLowerCase().includes(endpoints.LogCustomEvent.url) ||

View file

@ -343,3 +343,14 @@ export function splitCMSCopyOnParagraphTag(copy) {
export function splitCMSCopyOnBR(copy) { export function splitCMSCopyOnBR(copy) {
return copy.split("<br>"); 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);
}

View file

@ -2,6 +2,7 @@ import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { deepClone } from "@/helpers/object-helper"; import { deepClone } from "@/helpers/object-helper";
import { partNumberStrings } from "@/constants/part-number-strings";
export function getDisplayAmountDue(lineItemsObject, includeTax = true) { export function getDisplayAmountDue(lineItemsObject, includeTax = true) {
return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", { return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", {
@ -32,6 +33,10 @@ export function getAmountDue(lineItemsObject, includeTax = true) {
if (store.getters.coverageIsVerified && !order.policy.isNoComp && !order.policy.isItac) { if (store.getters.coverageIsVerified && !order.policy.isNoComp && !order.policy.isItac) {
amountDue = order.policy.currentDeductible; amountDue = order.policy.currentDeductible;
if (order.isMSRFeeApplicable && !order.isMSRFeeCoveredByInsurance) {
amountDue += getMSRFeePartPrice(lineItemsObject?.supportingItems, includeTax);
}
} }
if (lineItemsObject?.vaps) { if (lineItemsObject?.vaps) {
@ -100,6 +105,20 @@ export async function getPricingByDayPartWithPrice(pageNameToLog) {
return pricingResults[0]; return pricingResults[0];
} }
export function getMSRFeePartPrice(supportingItems, includeTax) {
let msrFeePrice = 0;
const msrFeeLineItem = supportingItems?.find(
(lineItem) => lineItem.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE
);
if (msrFeeLineItem) {
msrFeePrice = baseMixin?.methods?.getTotalPriceOfAllLineItemsAndChildParts(
[msrFeeLineItem],
includeTax
);
}
return msrFeePrice;
}
export function addPricesToLineItems(lineItems, pricingLineItems) { export function addPricesToLineItems(lineItems, pricingLineItems) {
lineItems.forEach((lineItem) => { lineItems.forEach((lineItem) => {
const lineItemIndex = pricingLineItems.findIndex( const lineItemIndex = pricingLineItems.findIndex(

View file

@ -62,6 +62,7 @@
:showAsPaid="isPia" :showAsPaid="isPia"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal" recyclingModalCmsWidgetName="RecycleModal"
msrModalCmsWidgetName="MSRModal"
:isInsurance="isInsurance" :isInsurance="isInsurance"
:insuranceDeductible="currentDeductible" :insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName" :insuranceCompanyName="insuranceCompanyName"
@ -70,7 +71,8 @@
:isItac="isItac" :isItac="isItac"
:isNoComp="isNoComp" :isNoComp="isNoComp"
:isExpandedOnLoad="false" :isExpandedOnLoad="false"
:isMSRFeeApplicable="isMSRFeeApplicable" /> :isMSRFeeApplicable="isMSRFeeApplicable"
:IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance" />
<hr class="mb-5" /> <hr class="mb-5" />
</div> </div>
@ -419,6 +421,9 @@ export default {
isMSRFeeApplicable() { isMSRFeeApplicable() {
return this.submittedOrder?.isMSRFeeApplicable; return this.submittedOrder?.isMSRFeeApplicable;
}, },
isMSRFeeCoveredByInsurance() {
return this.submittedOrder?.isMSRFeeCoveredByInsurance;
},
isInsurance() { isInsurance() {
return this.submittedOrder?.payment?.isInsurance; return this.submittedOrder?.payment?.isInsurance;
}, },

View file

@ -62,6 +62,7 @@ import baseMixin from "@/mixins/base-mixin.js";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash"; import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
// Define Validation Rules // Define Validation Rules
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
@ -169,7 +170,7 @@ export default {
baseMixin.methods.ResetExternalParamsAndHideModal(); baseMixin.methods.ResetExternalParamsAndHideModal();
} }
} else { } else {
baseMixin.methods.ResetExternalParamsAndHideModal(); showFmgLoadingModal(false);
} }
} else { } else {
baseMixin.methods.ResetExternalParamsAndHideModal(); baseMixin.methods.ResetExternalParamsAndHideModal();

View file

@ -29,10 +29,12 @@
v-model="lineItems" v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal" recyclingModalCmsWidgetName="RecycleModal"
msrModalCmsWidgetName="MSRModal"
:isInsurance="isInsurance" :isInsurance="isInsurance"
:insuranceDeductible="currentDeductible" :insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName" :insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs" :showInsuranceCoverageAs="showInsuranceCoverageAs"
:isMSRFeeApplicable="isMSRFeeApplicable"
:isItac="isItac" :isItac="isItac"
:isNoComp="isNoComp" :isNoComp="isNoComp"
:isCollapsible="false" /> :isCollapsible="false" />
@ -146,13 +148,15 @@ export default {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
const order = store.getters.order; const order = store.getters.order;
const logQueue = []; const logQueue = [];
const result = const results = [
hasServiceLocationInfo(order, logQueue) && hasServiceLocationInfo(order, logQueue),
hasInsuranceInfo(order, logQueue) && hasInsuranceInfo(order, logQueue),
hasSchedulingInfo(order, logQueue) && hasSchedulingInfo(order, logQueue),
hasCustomerInfo(order, logQueue) && hasCustomerInfo(order, logQueue),
hasPaymentMethodInfo(order, logQueue) && hasPaymentMethodInfo(order, logQueue),
hasGlassPartsOrRepairInfo(order, logQueue); hasGlassPartsOrRepairInfo(order, logQueue),
];
const result = results.every(Boolean);
flushPagePrereqsLogs("payment-adyen.vue", result, logQueue); flushPagePrereqsLogs("payment-adyen.vue", result, logQueue);
return result; return result;
}, },
@ -161,7 +165,7 @@ export default {
console.log(`Price = ${this.amountDue}`); console.log(`Price = ${this.amountDue}`);
console.log(`Adyen Price = ${this.adyenPriceTotal}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`);
const requestBody = this.getAdyenInitRequestInfo(); const requestBody = await this.getAdyenInitRequestInfo();
console.log(`Calling with:`); console.log(`Calling with:`);
console.log(requestBody); console.log(requestBody);
@ -255,6 +259,7 @@ export default {
this.dropinComponent = dropin; this.dropinComponent = dropin;
dropin.mount("#adyen-container"); dropin.mount("#adyen-container");
await this.dispatchStoreAction(storeActions.CORRECT_IDEMPOTENCY_KEY_EXPIRY, expiryTime);
}, },
async handleCompletedPayment(result) { async handleCompletedPayment(result) {
@ -436,19 +441,13 @@ export default {
}, },
// Now a Method so it is always freshly called and not cached. // Now a Method so it is always freshly called and not cached.
getIdempotencyKey() { async getIdempotencyKey() {
const currentDateTime = new Date(); return await this.dispatchStoreAction(storeActions.GET_VALID_IDEMPOTENCY_KEY);
const currentHour = currentDateTime.getUTCHours();
const currentDate = currentDateTime.getUTCDate();
const id = this?.$store?.getters?.order?.referralCorrelationId;
const system = this.sourceSystem;
const total = this.adyenPriceTotal;
return `${id}-${system}-${currentDate}-${currentHour}-${total}`;
}, },
// Now a Method so it is always freshly called and not cached. // Now a Method so it is always freshly called and not cached.
getAdyenInitRequestInfo() { async getAdyenInitRequestInfo() {
const key = await this.getIdempotencyKey();
return { return {
sourceSystem: this.sourceSystem, sourceSystem: this.sourceSystem,
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
@ -462,10 +461,9 @@ export default {
stateOrProvince: this.locationInfo.state, stateOrProvince: this.locationInfo.state,
returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL, returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL,
email: this.$store.getters.order.customer.emailAddress, email: this.$store.getters.order.customer.emailAddress,
IP: "127.0.0.1", // TODO
firstName: this.$store.getters.order.customer.firstName, firstName: this.$store.getters.order.customer.firstName,
lastName: this.$store.getters.order.customer.lastName, lastName: this.$store.getters.order.customer.lastName,
idempotencyKey: this.getIdempotencyKey(), idempotencyKey: key,
}; };
}, },
}, },
@ -572,6 +570,9 @@ export default {
insuranceCompanyName() { insuranceCompanyName() {
return this.$store.getters.policy.insuranceCompanyName; return this.$store.getters.policy.insuranceCompanyName;
}, },
isMSRFeeApplicable() {
return this.$store.getters.order.isMSRFeeApplicable;
},
lineItems() { lineItems() {
return deepClone(this.$store.getters.lineItems); return deepClone(this.$store.getters.lineItems);
}, },

View file

@ -26,6 +26,7 @@
pageName="payment-method" pageName="payment-method"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal" recyclingModalCmsWidgetName="RecycleModal"
msrModalCmsWidgetName="MSRModal"
:isInsurance="isInsurance" :isInsurance="isInsurance"
:insuranceDeductible="currentDeductible" :insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName" :insuranceCompanyName="insuranceCompanyName"
@ -35,7 +36,9 @@
:isNoComp="isNoComp" :isNoComp="isNoComp"
:isExpandedOnLoad="false" :isExpandedOnLoad="false"
:isMSRFeeApplicable="isMSRFeeApplicable" :isMSRFeeApplicable="isMSRFeeApplicable"
@itemRemoved="evaluatePromosAndTaxItemsOnOrder" /> :IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance"
@itemRemoved="evaluatePromosAndTaxItemsOnOrder"
@switchToInshop="navigateToSchedulePage" />
<afterpayBreakout <afterpayBreakout
v-if="isAfterpayBreakoutDisplay" v-if="isAfterpayBreakoutDisplay"
@ -214,6 +217,7 @@ export default {
name: "paymentMethod", name: "paymentMethod",
props: { props: {
recyclingModalCmsWidgetName: String, recyclingModalCmsWidgetName: String,
msrModalCmsWidgetName: String,
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
@ -390,12 +394,15 @@ export default {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
const order = store.getters.order; const order = store.getters.order;
const logQueue = []; const logQueue = [];
const result = const results = [
hasServiceLocationInfo(order, logQueue) && hasServiceLocationInfo(order, logQueue),
hasInsuranceInfo(order, logQueue) && hasInsuranceInfo(order, logQueue),
hasSchedulingInfo(order, logQueue) && hasSchedulingInfo(order, logQueue),
hasCustomerInfo(order, logQueue) && hasCustomerInfo(order, logQueue),
hasGlassPartsOrRepairInfo(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); flushPagePrereqsLogs("payment-method.vue", result, logQueue);
return result; return result;
}, },
@ -604,6 +611,13 @@ export default {
openModal(modalName) { openModal(modalName) {
this.$refs[modalName].openModal(); this.$refs[modalName].openModal();
}, },
async navigateToSchedulePage() {
this.$refs.loadingModal.showModal();
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_SWITCH_TO_INSHOP,
this.pageName
);
},
}, },
computed: { computed: {
AppointmentType() { AppointmentType() {
@ -679,6 +693,9 @@ export default {
isMSRFeeApplicable() { isMSRFeeApplicable() {
return this.$store.getters.order.isMSRFeeApplicable; return this.$store.getters.order.isMSRFeeApplicable;
}, },
isMSRFeeCoveredByInsurance() {
return this.$store.getters.order.isMSRFeeCoveredByInsurance;
},
isInsurance() { isInsurance() {
return this.hasSubmittedOrder() return this.hasSubmittedOrder()
? this.getSubmittedOrder()?.payment.isInsurance ? this.getSubmittedOrder()?.payment.isInsurance

View file

@ -47,10 +47,12 @@
v-model="lineItems" v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle" servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal" recyclingModalCmsWidgetName="RecycleModal"
msrModalCmsWidgetName="MSRModal"
:isInsurance="isInsurance" :isInsurance="isInsurance"
:insuranceDeductible="currentDeductible" :insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName" :insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs" :showInsuranceCoverageAs="showInsuranceCoverageAs"
:isMSRFeeApplicable="isMSRFeeApplicable"
:isItac="isItac" :isItac="isItac"
:isNoComp="isNoComp" :isNoComp="isNoComp"
:isCollapsible="false" /> :isCollapsible="false" />
@ -410,6 +412,9 @@ export default {
insuranceCompanyName() { insuranceCompanyName() {
return this.$store.getters.policy.insuranceCompanyName; return this.$store.getters.policy.insuranceCompanyName;
}, },
isMSRFeeApplicable() {
return this.$store.getters.order.isMSRFeeApplicable;
},
isPaypal() { isPaypal() {
if (this.paymentType == "pp") { if (this.paymentType == "pp") {
return true; return true;
@ -445,13 +450,15 @@ export default {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
const order = store.getters.order; const order = store.getters.order;
const logQueue = []; const logQueue = [];
const result = const results = [
hasServiceLocationInfo(order, logQueue) && hasServiceLocationInfo(order, logQueue),
hasInsuranceInfo(order, logQueue) && hasInsuranceInfo(order, logQueue),
hasSchedulingInfo(order, logQueue) && hasSchedulingInfo(order, logQueue),
hasCustomerInfo(order, logQueue) && hasCustomerInfo(order, logQueue),
hasPaymentMethodInfo(order, logQueue) && hasPaymentMethodInfo(order, logQueue),
hasGlassPartsOrRepairInfo(order, logQueue); hasGlassPartsOrRepairInfo(order, logQueue),
];
const result = results.every(Boolean);
flushPagePrereqsLogs("payment.vue", result, logQueue); flushPagePrereqsLogs("payment.vue", result, logQueue);
return result; return result;
}, },

View file

@ -100,9 +100,6 @@ const mockExperimentSettings = experimentSettings;
jest.mock("@/mixins/experiment-mixin.js", () => ({ jest.mock("@/mixins/experiment-mixin.js", () => ({
methods: { methods: {
getSettingValue(settingName) { getSettingValue(settingName) {
if (settingName === mockExperimentSettings.SERVICE_PACKAGE_DISCOUNT) {
return true;
}
if ( if (
settingName === mockExperimentSettings.INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL settingName === mockExperimentSettings.INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL
) { ) {
@ -367,9 +364,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -391,59 +385,6 @@ describe("quote.vue", () => {
// This should have its own test // This should have its own test
//expect(vm.isInsuranceSelected !== null).toBe(true); //expect(vm.isInsuranceSelected !== null).toBe(true);
}); });
test("Returns true if service package discount setting is true", async () => {
//Arrange
store.getters = {
lineItems: {
glassParts: ["item", "item2"],
},
pageData: jest.fn((page) => {
if (page === "quote") {
return { saveProgressPopupSkipped: true };
}
return {};
}),
applicationUser: {
experiments: [],
},
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {},
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
},
},
vehicle: {
cardId: "123",
},
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
};
const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "false" } };
const isServicePackageDiscount = experimentMixin.methods.getSettingValue(
experimentSettings.SERVICE_PACKAGE_DISCOUNT
);
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(isServicePackageDiscount).toBe(true);
});
test("should default to insurance if query param 'isInsurance' is true", async () => { test("should default to insurance if query param 'isInsurance' is true", async () => {
//Arrange //Arrange
store.getters = { store.getters = {
@ -476,9 +417,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: { isInsurance: "true" } }; wrapper.vm.$route = { query: { isInsurance: "true" } };
@ -526,9 +464,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
mockTierOnePrice = 200; mockTierOnePrice = 200;
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -577,9 +512,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection // Ensure that query param isn't overriding selection
@ -628,9 +560,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
// Ensure that query param isn't overriding selection // Ensure that query param isn't overriding selection
@ -680,9 +609,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
mockTierOnePrice = 200; mockTierOnePrice = 200;
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -734,9 +660,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
mockTierOnePrice = 505; mockTierOnePrice = 505;
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -786,9 +709,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
mockTierOnePrice = 505; mockTierOnePrice = 505;
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -839,9 +759,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.$route = { query: null }; wrapper.vm.$route = { query: null };
@ -962,9 +879,6 @@ describe("quote.vue", () => {
vehicle: { vehicle: {
cardId: "123", cardId: "123",
}, },
experimentSettings: {
settingName: "SERVICE_PACKAGE_DISCOUNT",
},
}; };
// Set up the component // Set up the component

View file

@ -196,6 +196,7 @@ import {
} from "@/helpers/page-prerequisites-helper.js"; } from "@/helpers/page-prerequisites-helper.js";
import { savePageData } from "@/router/methods/helpers/save-page-data"; import { savePageData } from "@/router/methods/helpers/save-page-data";
import { quotePageDiscountTable } from "../../constants/quote-page-discounts"; import { quotePageDiscountTable } from "../../constants/quote-page-discounts";
import { GaLabels } from "@/constants/analytics";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -595,12 +596,6 @@ export default {
} }
// there are no active or inactive external parameters; use internal threshold // there are no active or inactive external parameters; use internal threshold
if (internalThreshold) thresholdToUse = internalThreshold; if (internalThreshold) thresholdToUse = internalThreshold;
vm.isInsuranceSelected = getIsInsuranceSelectedValue(
vm.availableLineItems,
thresholdToUse
);
baseMixin.methods.ResetExternalParamsAndHideModal();
} else { } else {
// user came from an external source, however, the externalParms may have been reset on service-zip, property-questions, etc... // user came from an external source, however, the externalParms may have been reset on service-zip, property-questions, etc...
if (getBoolFromString(store.getters.externalParameterQuote?.isInsurance)) { if (getBoolFromString(store.getters.externalParameterQuote?.isInsurance)) {
@ -618,8 +613,6 @@ export default {
) { ) {
await vm.skip(insuranceSelection, servicePackage); await vm.skip(insuranceSelection, servicePackage);
} }
baseMixin.methods.ResetExternalParamsAndHideModal();
} else { } else {
if (externalThreshold) thresholdToUse = externalThreshold; if (externalThreshold) thresholdToUse = externalThreshold;
@ -627,15 +620,17 @@ export default {
{ {
debugLog("quote.vue tab select NOT EXTERNAL"); debugLog("quote.vue tab select NOT EXTERNAL");
} }
vm.isInsuranceSelected = getIsInsuranceSelectedValue(
vm.availableLineItems,
thresholdToUse
);
baseMixin.methods.ResetExternalParamsAndHideModal();
} }
} }
if (vm.isInsuranceSelected == null) {
vm.isInsuranceSelected = getIsInsuranceSelectedValue(
vm.availableLineItems,
thresholdToUse
);
}
baseMixin.methods.ResetExternalParamsAndHideModal();
if (vm.skipToInsurance && !vm.showSaveProgressPopup) { if (vm.skipToInsurance && !vm.showSaveProgressPopup) {
// skip ahead only if no email popup. // skip ahead only if no email popup.
await vm.skip(true, externalParamPackageLabels.GLASS_ONLY); await vm.skip(true, externalParamPackageLabels.GLASS_ONLY);
@ -746,10 +741,11 @@ export default {
order.payment?.insuranceCoverage?.isVerified == null || order.payment?.insuranceCoverage?.isVerified == null ||
order.payment?.insuranceCoverage?.isVerified === false; order.payment?.insuranceCoverage?.isVerified === false;
const preReqResult = const results = [
hasServiceZipInfo(order, logQueue) && hasServiceZipInfo(order, logQueue),
hasGlassPartsOrRepairInfo(order, logQueue) && hasGlassPartsOrRepairInfo(order, logQueue),
isVerifiedOk; ];
const preReqResult = results.every(Boolean) && isVerifiedOk;
logQueue.push(() => { logQueue.push(() => {
debugLog( debugLog(
@ -958,8 +954,7 @@ export default {
priceLabel = price.toFixed(2); priceLabel = price.toFixed(2);
} }
} }
const subTotalLabel = this.Variables?.CASH_SUBTOTAL; this.pushVariableToDataLayer?.({ [this.GaLabels.GLASS_CASH_QUOTE]: priceLabel });
this.pushVariableToDataLayer?.({ [subTotalLabel]: priceLabel });
}); });
}, },
async skip(insuranceSelection, packageSelection) { async skip(insuranceSelection, packageSelection) {

View file

@ -31,14 +31,10 @@ const mockExperimentSettings = experimentSettings;
jest.mock("@/mixins/experiment-mixin.js", () => ({ jest.mock("@/mixins/experiment-mixin.js", () => ({
methods: { methods: {
getSettingValue(settingName) { getSettingValue(settingName) {
if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) { return false;
return true;
}
}, },
hasSetting(settingName) { hasSetting(settingName) {
if (settingName === mockExperimentSettings.PROMO_ON_PACKAGE) { return false;
return false;
}
}, },
hasSettingEqualTo(settingName, settingValue) { hasSettingEqualTo(settingName, settingValue) {
return false; return false;

View file

@ -81,6 +81,16 @@
cmsWidgetName="AlertNoShopsWidget" cmsWidgetName="AlertNoShopsWidget"
v-if="displayNoShopsAlert" v-if="displayNoShopsAlert"
alertClass="alert-warning" /> 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> </div>
<div <div
@ -229,7 +239,11 @@ import {
RECAL_ACK_YES, RECAL_ACK_YES,
} from "@/constants/schedule-constants"; } 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 { settleAllPromises } from "@/helpers/layout-helper";
import { import {
getPricedMobileFeePart, getPricedMobileFeePart,
@ -247,7 +261,6 @@ import { insuranceCompanyNameStrings } from "@/constants/insurance";
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import { import {
calcDaysBetweenDates, calcDaysBetweenDates,
convertDateStringToDate, convertDateStringToDate,
@ -693,9 +706,11 @@ export default {
}, },
isMobileStaticRecalibrationApplicable() { isMobileStaticRecalibrationApplicable() {
return ( return (
this.displayMSR && (this.displayMSR &&
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE && this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable) this.enableMSRSplitPay) ||
this.isCashItacNoComp ||
this.mobileFeePart?.isInsurable
); );
}, },
displayMSR() { displayMSR() {
@ -705,6 +720,35 @@ export default {
?.toLowerCase() === "true" ?.toLowerCase() === "true"
); );
}, },
enableMSRSplitPay() {
return (
experimentMixin.methods
.getSettingValue(experimentSettings.ENABLE_MSR_SPLIT_PAY)
?.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() { isCashItacNoComp() {
return !this.isInsurance || this.isITAC || this.isNoComp; return !this.isInsurance || this.isITAC || this.isNoComp;
}, },
@ -1141,6 +1185,13 @@ export default {
isMSRFeeApplicable isMSRFeeApplicable
); );
}, },
updateAndSaveIsMSRFeeCoveredByInsurance() {
const isMSRFeeCoveredByInsurance = this.mobileFeePart?.isInsurable;
this.dispatchStoreAction(
this.storeActions.SAVE_IS_MSR_FEE_COVERED_BY_INSURANCE,
isMSRFeeCoveredByInsurance
);
},
updateAndSaveSupportingItems() { updateAndSaveSupportingItems() {
let supportingItems = store.getters.lineItems.supportingItems; let supportingItems = store.getters.lineItems.supportingItems;
let shouldSaveSupportingItems = false; let shouldSaveSupportingItems = false;
@ -1496,6 +1547,7 @@ export default {
this.updateAndSaveSupportingItems(); this.updateAndSaveSupportingItems();
this.updateAndSaveIsMSRFeeApplicable(); this.updateAndSaveIsMSRFeeApplicable();
this.updateAndSaveIsMSRFeeCoveredByInsurance();
this.updateSupportingItems(); this.updateSupportingItems();
@ -2035,14 +2087,7 @@ export default {
} }
}, },
async showMultiLocationModal() { async showMultiLocationModal() {
const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( if (!this.selectedRouteCodeData?.routeCode) {
experimentSettings.SHOW_MULTI_LOCATION_APPT
);
if (
!this.selectedRouteCodeData?.routeCode &&
showMultiLocationAppointment?.toLowerCase() === "true"
) {
let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION
); );

View file

@ -80,6 +80,7 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { routeData } from "@/router/constants/routes"; import { routeData } from "@/router/constants/routes";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
// Define Validation Rules // Define Validation Rules
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
@ -137,6 +138,13 @@ export default {
}, },
false false
); );
vm.serviceZipCode = store.getters.externalParameterServiceZip.zipCode;
const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm);
if (isValid) {
showFmgLoadingModal(true);
vm.forwardButtonAction();
return;
}
} }
if (store.getters.externalParameterCustomer?.phoneNumber) { if (store.getters.externalParameterCustomer?.phoneNumber) {

View file

@ -93,6 +93,7 @@ import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
import { debugLog } from "@/helpers/debug-log-helper"; import { debugLog } from "@/helpers/debug-log-helper";
import store from "@/store"; import store from "@/store";
@ -199,7 +200,7 @@ export default {
return vm.forwardButtonAction(); return vm.forwardButtonAction();
} }
} }
baseMixin.methods.ResetExternalParamsAndHideModal(); showFmgLoadingModal(false);
//clear any validation errors for external parameter flow //clear any validation errors for external parameter flow
const form = vm.$refs.theForm; const form = vm.$refs.theForm;
@ -209,7 +210,7 @@ export default {
} }
} }
} else { } else {
baseMixin.methods.ResetExternalParamsAndHideModal(); showFmgLoadingModal(false);
} }
}); });
}, },

View file

@ -93,8 +93,11 @@ describe("vehicle.vue", () => {
}); });
describe("vehicle.vue", () => { describe("vehicle.vue", () => {
test("should call forwardButtonAction if isExternalParameter is true and form is valid", async () => { test("should call forwardButtonAction if isExternalParameter is true, has a year, make and model, and form is valid", async () => {
store.getters.externalParameterState.isExternalParameter = true; store.getters.externalParameterState.isExternalParameter = true;
store.getters.externalParameterVehicle.year = "1886";
store.getters.externalParameterVehicle.make = "Benz";
store.getters.externalParameterVehicle.model = "Patent-Motorwagen";
// Create a shallow mount of MyComponent // Create a shallow mount of MyComponent
const { wrapper } = setupMocks(); const { wrapper } = setupMocks();
// Set displayNoServiceAlert to false // Set displayNoServiceAlert to false

View file

@ -109,6 +109,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { applicationConfig } from "../../constants/application-config"; import { applicationConfig } from "../../constants/application-config";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
import { debugLog } from "@/helpers/debug-log-helper"; import { debugLog } from "@/helpers/debug-log-helper";
//define validation rules //define validation rules
@ -252,17 +253,17 @@ export default {
const matchingMake = resultMap.makeQuestionInitialData?.filter( const matchingMake = resultMap.makeQuestionInitialData?.filter(
(item) => (item) =>
item.toLowerCase() === item.toLowerCase() ===
store.getters.externalParameterVehicle.make.toLowerCase() store.getters.externalParameterVehicle.make?.toLowerCase()
); );
const matchingModel = resultMap.modelQuestionInitialData?.filter( const matchingModel = resultMap.modelQuestionInitialData?.filter(
(item) => (item) =>
item.toLowerCase() === item.toLowerCase() ===
store.getters.externalParameterVehicle.model.toLowerCase() store.getters.externalParameterVehicle.model?.toLowerCase()
); );
const matchingStyle = resultMap.styleQuestionInitialData?.filter( const matchingStyle = resultMap.styleQuestionInitialData?.filter(
(item) => (item) =>
item.toLowerCase() === item.toLowerCase() ===
store.getters.externalParameterVehicle.style.toLowerCase() store.getters.externalParameterVehicle.style?.toLowerCase()
); );
baseMixin.methods.dispatchStoreAction( baseMixin.methods.dispatchStoreAction(
storeActions.UPDATE_EXTERNAL_PARAMETER_MMS, storeActions.UPDATE_EXTERNAL_PARAMETER_MMS,
@ -291,7 +292,12 @@ export default {
resultMap.modelQuestionInitialData, resultMap.modelQuestionInitialData,
resultMap.styleQuestionInitialData resultMap.styleQuestionInitialData
); );
if (store.getters.externalParameterState?.isExternalParameter) { if (
store.getters.externalParameterState?.isExternalParameter &&
store.getters.externalParameterVehicle.year &&
store.getters.externalParameterVehicle.make &&
store.getters.externalParameterVehicle.model
) {
await vm.getVehicleDetails(); await vm.getVehicleDetails();
if (!vm.displayNoServiceAlert) { if (!vm.displayNoServiceAlert) {
const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm); const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm);
@ -304,7 +310,7 @@ export default {
baseMixin.methods.ResetExternalParamsAndHideModal(); baseMixin.methods.ResetExternalParamsAndHideModal();
} }
} else { } else {
baseMixin.methods.ResetExternalParamsAndHideModal(); showFmgLoadingModal(false);
} }
}); });
}, },

View file

@ -132,51 +132,6 @@ export default {
} }
}, },
async logDigitalConsumer() {
const currentPageName = getPageNameFromRouter();
const universes = store.getters.applicationUser.experiments;
const variationNames = universes
.filter((item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL)
.map((item) => item.variationName)
.filter(Boolean); // removes undefined/null
const conceptVariation = variationNames.length > 0 ? variationNames[0] : "";
const isConceptExposed = universes.find(
(item) => item.universeName === experimentUniverses.CONCEPT_FUNNEL
)?.isExposed;
const submittedOrder = baseMixin.methods.getSubmittedOrder();
const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder();
const hasSubmittedOrderAtConfirmationPage =
hasSubmittedOrder && currentPageName?.toLowerCase() == routeData.CONFIRMATION.name;
var payload = {
actionName: `Browser page:${currentPageName}`,
referralSequenceNumber: hasSubmittedOrderAtConfirmationPage
? submittedOrder.referralSequenceNumber
: store.getters.order.referralSequenceNumber,
referralNumber: hasSubmittedOrderAtConfirmationPage
? submittedOrder.referralNumber
: store.getters.order.referralNumber,
workOrderId: hasSubmittedOrderAtConfirmationPage
? submittedOrder.workOrderId
: store.getters.order.workOrderId,
workOrderNumber: hasSubmittedOrderAtConfirmationPage
? submittedOrder.workOrderNumber
: store.getters.order.workOrderNumber,
conceptVariation: conceptVariation,
isConceptExposed: isConceptExposed,
};
await baseMixin.methods.dispatchStoreAction(
storeActions.LOG_DIGITALCONSUMER,
payload,
false
);
},
async pushEventForChatsToGA(category, action, label, pushToLogApp = false) { async pushEventForChatsToGA(category, action, label, pushToLogApp = false) {
const currentPageName = getPageNameFromRouter(); const currentPageName = getPageNameFromRouter();
const value = `2.0_${currentPageName}`; const value = `2.0_${currentPageName}`;
@ -206,6 +161,11 @@ export default {
// S3 bucket, safelite-dev-digitalconsumer-session-data-us-east-2/1. // S3 bucket, safelite-dev-digitalconsumer-session-data-us-east-2/1.
// This bucket data is then picked up by snowflake for analytics use. // This bucket data is then picked up by snowflake for analytics use.
async pushFmgSessionData() { async pushFmgSessionData() {
var currentPageName = getPageNameFromRouter(true);
if (!currentPageName) {
return;
}
const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder();
const submittedOrder = baseMixin.methods.getSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder();
const order = hasSubmittedOrder ? submittedOrder : store.getters.order; const order = hasSubmittedOrder ? submittedOrder : store.getters.order;
@ -247,22 +207,6 @@ export default {
} }
var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`; var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`;
var currentPageName = getPageNameFromRouter();
// add query strings to the page name for debugging. on the vehicle page, if from an external link, pull it from the stash
if (currentPageName === "vehicle") {
if (!window.location.search) {
if (store.getters.externalParameterState?.qsStash) {
currentPageName += `${store.getters.externalParameterState.qsStash}`;
}
} else {
currentPageName += `${window.location.search}`;
}
} else {
if (window.location.search) {
currentPageName += `${window.location.search}`;
}
}
var sessionData = {}; var sessionData = {};
sessionData.currentPage = currentPageName; sessionData.currentPage = currentPageName;
@ -806,14 +750,14 @@ export default {
); );
coupons = promoString; coupons = promoString;
} }
const serviceZipPackage = submittedOrder.lineItems?.supportingItems?.find( const additionalDiscounts = submittedOrder.lineItems?.supportingItems?.find(
(x) => x.partType == "SERVICE PACKAGE DISCOUNT" (x) => x.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT
); );
if (isDefined(serviceZipPackage)) { if (isDefined(additionalDiscounts)) {
if (coupons) { if (coupons) {
coupons += ","; coupons += ",";
} }
coupons += serviceZipPackage.partNumber; coupons += additionalDiscounts.partNumber;
} }
refSequenceNum = submittedOrder.referralSequenceNumber; refSequenceNum = submittedOrder.referralSequenceNumber;
@ -846,17 +790,17 @@ export default {
} }
} }
} }
pushToDataLayerIfDefined({
event: "commissionJunctionGtmData",
commissionJunctionGtmData: {
cj_commission_junction_event: cjEvent,
cj_referral_sequence_number: refSequenceNum,
cj_amount: amount.toFixed(2),
cj_repair_replace: repairReplace,
cj_coupon: coupons,
},
});
} }
pushToDataLayerIfDefined({
event: "commissionJunctionGtmData",
commissionJunctionGtmData: {
cj_commission_junction_event: cjEvent,
cj_referral_sequence_number: refSequenceNum,
cj_amount: amount.toFixed(2),
cj_repair_replace: repairReplace,
cj_coupon: coupons,
},
});
}, },
pushPageErrorToDataLayer(error) { pushPageErrorToDataLayer(error) {
@ -911,6 +855,9 @@ export default {
setSessionIdIfUnset(response.data.sessionId); setSessionIdIfUnset(response.data.sessionId);
} }
} }
// logging on session init so we capture complete query string data
await this.pushFmgSessionData();
}, },
noSession() { noSession() {
@ -977,7 +924,7 @@ function pushToDataLayerIfDefined(data) {
} }
} }
function getPageNameFromRouter() { function getPageNameFromRouter(useDefaultUrl = false) {
if ( if (
router && router &&
router.currentRoute && router.currentRoute &&
@ -987,7 +934,9 @@ function getPageNameFromRouter() {
return router.currentRoute.value.name; return router.currentRoute.value.name;
} }
return window.location.href.replace(/\/$/, "").split("/").pop(); return useDefaultUrl
? window.location.search
: window.location.href.replace(/\/$/, "").split("/").pop();
} }
function getValueToLog(value, valueToLogType) { function getValueToLog(value, valueToLogType) {

View file

@ -64,6 +64,7 @@ const navigationScenarios = {
CLICKED_PAY_NOW: "CLICKED_PAY_NOW", CLICKED_PAY_NOW: "CLICKED_PAY_NOW",
CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN", CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN",
CLICKED_PAY_LATER: "CLICKED_PAY_LATER", CLICKED_PAY_LATER: "CLICKED_PAY_LATER",
CLICKED_SWITCH_TO_INSHOP: "CLICKED_SWITCH_TO_INSHOP",
PIA_ERROR: "PIA_ERROR", PIA_ERROR: "PIA_ERROR",
PIA_CC_ERROR: "PIA_CC_ERROR", PIA_CC_ERROR: "PIA_CC_ERROR",
PIA_APPLE_ERROR: "PIA_APPLE_ERROR", PIA_APPLE_ERROR: "PIA_APPLE_ERROR",

View file

@ -553,6 +553,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_INSURANCE, scenario: navigationScenarios.CLICKED_INSURANCE,
destinationPageData: routeData.INSURANCE_COMPANY, destinationPageData: routeData.INSURANCE_COMPANY,
}, },
{
scenario: navigationScenarios.CLICKED_SWITCH_TO_INSHOP,
destinationPageData: routeData.SCHEDULE,
},
], ],
}, },
{ {

View file

@ -5,9 +5,6 @@ import { storeMutations } from "@/constants/store-mutations";
export async function afterEach(to, from) { export async function afterEach(to, from) {
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
// digital consumer logging
analyticsMixin.methods.logDigitalConsumer();
// digital consumer fmg session logging to snowflake // digital consumer fmg session logging to snowflake
analyticsMixin.methods.pushFmgSessionData(); analyticsMixin.methods.pushFmgSessionData();

View file

@ -95,6 +95,7 @@ function updateExternalParameterState() {
} }
if (externalParameterZipCode) { if (externalParameterZipCode) {
store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_ZIP_CODE, externalParameterZipCode); store.commit(storeMutations.UPDATE_EXTERNAL_PARAMETER_ZIP_CODE, externalParameterZipCode);
store.commit(storeMutations.UPDATE_IS_EXTERNAL_PARAMETER, externalParameterStatus.ACTIVE);
} }
if (externalParameterEmail) { if (externalParameterEmail) {
store.commit( store.commit(

View file

@ -19,7 +19,6 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name, nextPage: to?.name,
}; };
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload); await handleHardError(errorPayload);
return; return;
} }
@ -33,7 +32,6 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name, nextPage: to?.name,
}; };
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload); await handleHardError(errorPayload);
return; return;
} else { } else {

View file

@ -59,7 +59,7 @@ import {
} from "@/helpers/recal-helper"; } from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters"; import { externalParameterStatus } from "@/constants/external-parameters";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { addPricesToLineItems } from "@/helpers/pricing-helper"; import { addPricesToLineItems, getAmountDue } from "@/helpers/pricing-helper";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
@ -192,6 +192,7 @@ const getDefaultState = () => {
isRecalAckOptIn: false, isRecalAckOptIn: false,
isRecalAcknowledgedForScheduling: "", isRecalAcknowledgedForScheduling: "",
isMSRFeeApplicable: false, isMSRFeeApplicable: false,
isMSRFeeCoveredByInsurance: false,
}, },
applicationUser: { applicationUser: {
eventBus: [], eventBus: [],
@ -207,6 +208,12 @@ const getDefaultState = () => {
loggingOption: false, loggingOption: false,
hasAlreadyTriggeredError: false, hasAlreadyTriggeredError: false,
}, },
idempotencyKeyFields: {
referralCorrelationId: null,
totalInCents: 0,
expiryTime: null,
idempotencyKey: null,
},
}; };
}; };
@ -353,6 +360,9 @@ export const mutations = {
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable; state.order.isMSRFeeApplicable = isMSRFeeApplicable;
}, },
updateIsMSRFeeCoveredByInsurance(state, isMSRFeeCoveredByInsurance) {
state.order.isMSRFeeCoveredByInsurance = isMSRFeeCoveredByInsurance;
},
updateCashPriceSubTotal(state, cashPriceSubTotal) { updateCashPriceSubTotal(state, cashPriceSubTotal) {
state.order.cashPriceSubTotal = state.order.cashPriceSubTotal =
cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString(); cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString();
@ -862,6 +872,15 @@ export const mutations = {
updateHasTriggeredError(state, hasTriggeredError) { updateHasTriggeredError(state, hasTriggeredError) {
state.applicationUser.hasAlreadyTriggeredError = hasTriggeredError; state.applicationUser.hasAlreadyTriggeredError = hasTriggeredError;
}, },
updateIdempotencyKey(
state,
{ idempotencyKey, referralCorrelationId, totalInCents, expiryTime }
) {
state.idempotencyKeyFields.idempotencyKey = idempotencyKey;
state.idempotencyKeyFields.referralCorrelationId = referralCorrelationId;
state.idempotencyKeyFields.totalInCents = totalInCents;
state.idempotencyKeyFields.expiryTime = expiryTime;
},
}; };
// Export Getters // Export Getters
@ -1105,6 +1124,35 @@ export const getters = {
state.order.customer.emailAddress state.order.customer.emailAddress
); );
}, },
isIdempotencyKeyValid: (state) => {
if (!state.idempotencyKeyFields?.idempotencyKey) {
return false;
}
const referralCorrelationId = state.order?.referralCorrelationId;
if (
!referralCorrelationId ||
referralCorrelationId !== state.idempotencyKeyFields?.referralCorrelationId
) {
return false;
}
const total = getAmountDue(state.order?.lineItems);
const totalInCents = Math.round(total * 100);
if (totalInCents !== state.idempotencyKeyFields?.totalInCents) {
return false;
}
const currentTime = new Date();
const expiryTime = new Date(state.idempotencyKeyFields.expiryTime);
if (!expiryTime || expiryTime.getTime() < currentTime.getTime()) {
return false;
}
// If all checks pass, then the key is valid.
return true;
},
}; };
// Export Actions // Export Actions
@ -1586,38 +1634,6 @@ export const actions = {
}); });
}, },
logDigitalConsumer(
context,
{
actionName,
referralSequenceNumber,
referralNumber,
workOrderId,
workOrderNumber,
conceptVariation,
isConceptExposed,
}
) {
var payload = {
sessionId: getSessionIdValue(),
deviceId: getDeviceIdValue(),
actionName: actionName ?? "",
referralSequenceNumber: referralSequenceNumber ?? "",
referralNumber: referralNumber ?? "",
applicationName: baseMixin.methods.isMobileDevice() ? "2.0 Mobile" : "2.0",
workOrderId: workOrderId ?? "",
workOrderNumber: workOrderNumber ?? "",
conceptVariation: conceptVariation,
isConceptExposed: isConceptExposed,
};
return globalMethods.callHttpClient({
method: endpoints.LogDigitalConsumer.method,
endpoint: endpoints.LogDigitalConsumer.url,
payload: payload,
});
},
logFmgSessionData( logFmgSessionData(
context, context,
{ {
@ -1726,6 +1742,7 @@ export const actions = {
method: endpoints.LogFmgSessionData.method, method: endpoints.LogFmgSessionData.method,
endpoint: endpoints.LogFmgSessionData.url, endpoint: endpoints.LogFmgSessionData.url,
payload: payload, payload: payload,
logApiCall: false,
}); });
}, },
@ -2567,6 +2584,7 @@ export const actions = {
lockToken: order.lockToken, lockToken: order.lockToken,
isRecalAckOptIn: order.isRecalAckOptIn, isRecalAckOptIn: order.isRecalAckOptIn,
isMSRFeeApplicable: order.isMSRFeeApplicable, isMSRFeeApplicable: order.isMSRFeeApplicable,
isMSRFeeCoveredByInsurance: order.isMSRFeeCoveredByInsurance,
cashPriceSubTotal: order.cashPriceSubTotal, cashPriceSubTotal: order.cashPriceSubTotal,
}, },
}, },
@ -2999,6 +3017,13 @@ export const actions = {
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable); context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
}, },
saveIsMSRFeeCoveredByInsurance(context, isMSRFeeCoveredByInsurance) {
context.commit(
storeMutations.UPDATE_IS_MSR_FEE_COVERED_BY_INSURANCE,
isMSRFeeCoveredByInsurance
);
},
saveParentAccountNumber(context, parentAccountNumber) { saveParentAccountNumber(context, parentAccountNumber) {
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber); context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccountNumber);
}, },
@ -3665,6 +3690,63 @@ export const actions = {
updateHasTriggeredError(context, hasAlreadyTriggeredError) { updateHasTriggeredError(context, hasAlreadyTriggeredError) {
context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, hasAlreadyTriggeredError); context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, hasAlreadyTriggeredError);
}, },
getValidIdempotencyKey(context) {
if (context.getters.isIdempotencyKeyValid) {
return context.state.idempotencyKeyFields.idempotencyKey;
}
// If invalid, need to regenerate.
const referralCorrelationId = context.state.order?.referralCorrelationId;
if (!referralCorrelationId) {
// Can't generate!
return null;
}
const total = getAmountDue(context.state.order?.lineItems);
const totalInCents = Math.round(total * 100);
if (Number.isNaN(totalInCents)) {
// Can't generate!
return null;
}
const currentTime = new Date();
const nextHour = currentTime.getUTCHours() + 1;
currentTime.setUTCHours(nextHour);
const newId = crypto.randomUUID();
const newKeyInfo = {
idempotencyKey: newId,
referralCorrelationId: referralCorrelationId,
totalInCents: totalInCents,
expiryTime: currentTime,
};
context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKeyInfo);
return newId;
},
correctIdempotencyKeyExpiry(context, expiryTime) {
if (!context.getters.isIdempotencyKeyValid) {
return;
}
const currentExpiryTime = new Date(context.state.idempotencyKeyFields.expiryTime);
const expiryTimeAsDate = new Date(expiryTime);
if (expiryTimeAsDate.getTime() < currentExpiryTime.getTime()) {
const existingKey = context.state.idempotencyKeyFields;
const newKey = {
referralCorrelationId: existingKey.referralCorrelationId,
totalInCents: existingKey.totalInCents,
idempotencyKey: existingKey.idempotencyKey,
expiryTime: expiryTimeAsDate,
};
context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey);
}
},
}; };
export default createStore({ export default createStore({

View file

@ -7,9 +7,15 @@
isDismissible ? 'alert-dismissible' : '', isDismissible ? 'alert-dismissible' : '',
this.alertClass, this.alertClass,
this.cssClassNameForCmsWidget, this.cssClassNameForCmsWidget,
this.hasBorder ? 'bordered' : '',
]"> ]">
<p class="mx-6 my-0 fw-bold alert-heading"> <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="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
<img
v-if="showWarningIcon"
:src="warningIcon"
alt="Warning Icon"
class="warning-icon" />
{{ alertHeadline }} {{ alertHeadline }}
</p> </p>
<hr v-if="showHorizontalRow" /> <hr v-if="showHorizontalRow" />
@ -63,7 +69,9 @@ export default {
name: "alert", name: "alert",
props: { props: {
showInfoIcon: Boolean, showInfoIcon: Boolean,
showWarningIcon: Boolean,
showHorizontalRow: Boolean, showHorizontalRow: Boolean,
hasBorder: Boolean,
isDismissible: Boolean, isDismissible: Boolean,
alertClass: String, alertClass: String,
/* /*
@ -96,6 +104,9 @@ export default {
infoIcon() { infoIcon() {
return require(`@/assets/img/icons/info-circle-blue.svg`); return require(`@/assets/img/icons/info-circle-blue.svg`);
}, },
warningIcon() {
return require(`@/assets/img/icons/alert-circle-yellow.svg`);
},
pageQueryString() { pageQueryString() {
return applicationConfig.PAGE_QUERYSTRING; return applicationConfig.PAGE_QUERYSTRING;
}, },
@ -211,12 +222,19 @@ export default {
background-color: $yellow-100; background-color: $yellow-100;
.alert-heading { .alert-heading {
color: $yellow-600; color: $yellow-600;
.warning-icon {
height: 1rem;
}
} }
svg { svg {
fill: $yellow-600; fill: $yellow-600;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
} }
hr {
border-color: $yellow-800;
}
} }
&.alert-success { &.alert-success {
background-color: $green-100; background-color: $green-100;
@ -229,6 +247,9 @@ export default {
height: 1rem; height: 1rem;
} }
} }
&.bordered {
border: 1px solid;
}
& p { & p {
font-size: 0.875rem; font-size: 0.875rem;
} }