Merge remote-tracking branch 'origin/develop' into feature/CASH-1527

This commit is contained in:
scottkiener-at-safelite 2026-01-22 14:48:23 -05:00
commit 7db73ef10e
84 changed files with 3033 additions and 546 deletions

View file

@ -13,6 +13,8 @@ const environmentVariables = {
['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
};
const keysOnly = Object.keys(environmentVariables);

41
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "Safelite",
"version": "0.1.0",
"dependencies": {
"@adyen/adyen-web": "^6.27.0",
"@iframe-resizer/child": "^5.3.3",
"axios": "^0.30.2",
"bootstrap": "^5.3.3",
@ -59,6 +60,18 @@
"node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22"
}
},
"node_modules/@adyen/adyen-web": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.27.0.tgz",
"integrity": "sha512-6E6hqEkMkoY/FevjO53zhgeCWMt+i/PDc11MX6eVM7Fvd+Sdgc9cIwTBz+NKIXs4ZerG6n2gd+ZIZEywqF/aBQ==",
"license": "MIT",
"dependencies": {
"@types/applepayjs": "14.0.9",
"@types/googlepay": "0.7.8",
"classnames": "2.5.1",
"preact": "10.22.1"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -3173,6 +3186,12 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/applepayjs": {
"version": "14.0.9",
"resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.9.tgz",
"integrity": "sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3328,6 +3347,12 @@
"@types/send": "*"
}
},
"node_modules/@types/googlepay": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.8.tgz",
"integrity": "sha512-EHl7G7jIeit/Px/fi/Du/19SZDvqrRhy0DjJX40Vzs/97m3sro9mzVyAYNC0pfzpWGDY+zPjtTLEfhV+OYWUcQ==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -5789,6 +5814,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@ -13681,6 +13712,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/preact": {
"version": "10.22.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz",
"integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View file

@ -12,6 +12,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@adyen/adyen-web": "^6.27.0",
"@iframe-resizer/child": "^5.3.3",
"axios": "^0.30.2",
"bootstrap": "^5.3.3",

View file

@ -3,7 +3,8 @@ import { PaymentMethod } from './localTypes/Enums'
export interface ITestData extends base {
// Put any project-specific data here. Anything useful to other Safelite projects should be submitted as a pull request to safelite-playwright-core.
paymentMethod: PaymentMethod
isOptedInForTextMessages: boolean
totalAmount?: number
paymentMethod: PaymentMethod,
isOptedInForTextMessages: boolean,
totalAmount?: number,
isForcedOEM?: boolean
}

View file

@ -24,6 +24,7 @@ import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage"
import { DuplicateCheckPage } from "../pages/DuplicateCheckPage"
import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage"
import { PolicyInfoSubmittedPage } from "../pages/PolicyInfoSubmittedPage"
import { HeritageProblemGlassQuestionsPage } from "../pages/HeritageProblemGlassQuestionsPage"
import RecalibrationInfoPage from "../pages/RecalibrationInfoPage"
import { CoverageStatementPage } from "../pages/CoverageStatementPage"
import { VerifyDetailsPage } from "../pages/VerifyDetailsPage"
@ -48,6 +49,7 @@ export interface ITestPages {
paymentMethodPage: PaymentMethodPage,
policyInfoSubmittedPage: PolicyInfoSubmittedPage,
policyVehiclesPage: PolicyVehiclesPage,
heritageProblemGlassQuestionsPage: HeritageProblemGlassQuestionsPage,
recalibrationInfoPage: RecalibrationInfoPage,
schedulePage: SchedulePage,
mobileDetailsPage: MobileDetailsPage,
@ -83,6 +85,7 @@ export const createTestPages: TestPagesFactory<ITestPages> = (page: Page) => {
paymentMethodPage: new PaymentMethodPage(page),
policyInfoSubmittedPage: new PolicyInfoSubmittedPage(page),
policyVehiclesPage: new PolicyVehiclesPage(page),
heritageProblemGlassQuestionsPage: new HeritageProblemGlassQuestionsPage(page),
recalibrationInfoPage: new RecalibrationInfoPage(page),
schedulePage: new SchedulePage(page),
mobileDetailsPage: new MobileDetailsPage(page),

View file

@ -169,4 +169,20 @@ export class BasePage {
Soft.expect(actualProgressPercentage).toBe(progressPercentage);
console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`);
}
async validateOEMPart( isOEM: boolean): Promise<void> {
if (isOEM!) {
// Get Vuex state from localStorage
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
// Validate the presence of an OEM part
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber;
await expect(firstGlassPartNumber.includes("OEM")).toBe(true);
} else {
throw new Error("No glass parts found in the order");
}
}
}
}

View file

@ -0,0 +1,42 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { InsuranceBasePage } from './InsuranceBasePage';
import { ITestData } from 'framework/TestData';
import { step } from 'framework/localTypes/Step';
export class HeritageProblemGlassQuestionsPage extends InsuranceBasePage {
readonly provideVinManuallyButton: Locator;
readonly provideLicensePlateButton: Locator;
readonly provideHomeAddressButton: Locator;
readonly continueOnVinPopUpButton: Locator;
readonly dontShareVinButton: Locator;
readonly yestoGlassQuestionButton: Locator;
readonly notoGlassQuestionButton: Locator;
readonly continueButton: Locator;
constructor(page: Page) {
super(page);
this.provideVinManuallyButton = page.locator('label[for="VINManually"]');
this.provideLicensePlateButton = page.locator('label[for="licensePlate"]');
this.provideHomeAddressButton = page.locator('label[for="HomeAddress"]');
this.continueOnVinPopUpButton = page.locator('button:has-text("Continue")');
this.dontShareVinButton = page.getByRole('button', { name: "I would rather not share my VIN" });
this.yestoGlassQuestionButton = page.locator('label:has-text("Yes")');
this.notoGlassQuestionButton = page.locator('label:has-text("No")');
this.continueButton = page.locator('button[type="submit"].btn-success.nav-continue');
}
@step("HeritageProblemGlassQuestionsPage >> Handle VIN popup and answer glass question on heritage")
// Handles OEM scenario with vehicle that has only one glass question
async handleHeritageProblemGlassQuestionsPage(testData: Partial<ITestData>){
await this.dontShareVinButton.click();
await this.yestoGlassQuestionButton.click();
await this.continueButton.click();
}
}

View file

@ -30,7 +30,7 @@ export class HomePage extends BasePage {
super(page);
this.page = page;
this.letsGetStartedButton = this.page.locator('.hero-content #zipCodeTextboxButton');
this.letsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
this.cusmodalPopup = this.page.locator('#Cusmodalpopup');
this.closePopupButton = this.page.getByRole('button', { name: '×' });
@ -47,7 +47,7 @@ export class HomePage extends BasePage {
//Zip Entry
this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox');
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextbox');
this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' });
}
@ -61,10 +61,11 @@ export class HomePage extends BasePage {
async letsGetStarted(zip: string, enterFunnelWithZip: boolean) {
if (enterFunnelWithZip) {
await this.letsGetStartedButton.evaluate((element, zip) => {
await this.zipEntryLetsGetStartedButton.fill(zip);
/* await this.letsGetStartedButton.evaluate((element, zip) => {
const currentHref = element.getAttribute('href') || '';
element.setAttribute('href', `${currentHref}&zipCode=${zip}`);
}, zip);
element.setAttribute('href', `${currentHref}?zipCode=${zip}`);
}, zip);*/
}
// Wait until modal pop up opens and close pop up

View file

@ -81,9 +81,9 @@ export class LookupPage extends BasePage {
// Map of expected messages by lookup type
const expectedMessages = {
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
'license plate': "Your license plate didnt return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
'VIN': "Your VIN didnt return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
'license plate': "Your license plate didnt return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
'VIN': "Your VIN didnt return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
'ZIP code': "Your ZIP code didn't return a match.Please re-enter the information above or provide your VIN in a different way."
};
@ -106,19 +106,19 @@ export class LookupPage extends BasePage {
}
}
async handleZipValidation(testData: Partial<ITestData>, zip: string, lookupType: VehicleLookupType, alertFlags?: IAlertFlags): Promise<boolean> {
async handleZipValidation(testData: Partial<ITestData>): Promise<boolean> {
let { vehicleDamage } = testData;
let { customerDetails,vehicleDamage, vehicleDetails, alertFlags } = testData;
// Only proceed with validation if alertFlags is provided
if (alertFlags) {
if (alertFlags.isUnserviceableZip) {
await this.checkForUnserviceableZipAlertMessage(zip);
await this.checkForUnserviceableZipAlertMessage(customerDetails!.address.postalCode!);
throw new TestSuccessAlert('Unserviceable ZIP validation successful.');
} else if (alertFlags.isInvalidZip) {
await this.checkForInvalidZipAlertMessage();
throw new TestSuccessAlert('Invalid ZIP validation successful.');
} else if (alertFlags.isVinNotFound) {
await this.checkForVinNotFoundAlertMessage(lookupType);
await this.checkForVinNotFoundAlertMessage(vehicleDetails!.vehicleLookupType!);
throw new TestSuccessAlert('Vin not found validation successful.');
} else {

View file

@ -47,7 +47,7 @@ export class OrderConfirmationPage extends BasePage {
async validateOrderConfirmationPage(testData: Partial<ITestData>) {
// Destructure data we use
const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow,
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified } = testData;
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified} = testData;
await this.serviceText.waitFor({ state: "visible" });
Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase()));

View file

@ -499,12 +499,16 @@ export class PaymentMethodPage extends BasePage {
@step("PaymentMethodPage >> Select Payment Method: ")
async handlePaymentMethodPage(testData: Partial<ITestData>) {
const { servicePackage, isRecalVehicle, paymentDetails } = testData;
const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData;
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
await this.validatePaymentDetailsPage(testData);
await this.ValidateAfterPayBreakOutSection();
if (isForcedOEM){
await this.validateOEMPart(isForcedOEM);
}
// Verify VAPS wipers on backend for standard and premium packages
if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) {
await this.verifyVAPS();

View file

@ -41,6 +41,8 @@ export class PaypalPage extends BasePage {
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.nextButton.click();
await this.tryAnotherWayButton.click();

View file

@ -35,6 +35,7 @@ export class SchedulePage extends BasePage {
readonly timeSlots: Locator;
readonly allDayDropOffButton: Locator;
readonly pickATimeButton: Locator;
readonly mobileFirstModalCloseButton: Locator;
constructor(page: Page) {
super(page);
@ -65,6 +66,7 @@ export class SchedulePage extends BasePage {
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
this.appointmentDuration = this.page.locator('.duration-text-block');
this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label').filter({ visible: true});
this.mobileFirstModalCloseButton = this.page.getByRole('dialog').getByRole('button', { name: 'Close' });
}
async selectLocation(testData: Partial<ITestData>) {
@ -229,6 +231,9 @@ export class SchedulePage extends BasePage {
async handleSchedulePage(testData: Partial<ITestData>) {
const { appointmentDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.SchedulePage);
if (await this.mobileFirstModalCloseButton.isVisible()) {
await this.mobileFirstModalCloseButton.click();
}
await this.selectLocation(testData);
await this.scheduleFirstAppointment(testData);
await this.nextPage();

View file

@ -44,7 +44,7 @@ export class ServicePackagesPage extends BasePage {
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']');
this.afterPayBanner = this.page.locator('#afterpay-banner');
this.afterPayBanner = this.page.locator('div.afterpay-modal-banner');
this.glassOnlypackagePrice = this.page.locator('fieldset:has(legend#ServicePackageQuestion) label').filter({ hasText: 'Glass service only' }).locator('.pricing-info');
}

View file

@ -12,9 +12,9 @@ export class ServiceZipPage extends LookupPage {
@step("ZipLookupPage >> Lookup by service ZIP: ")
async handleServiceZipPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
const { customerDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.ServiceZipPage);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(testData, customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
await this.handleZipValidation(testData);
}
}

View file

@ -29,10 +29,10 @@ export class VehicleLookupAddressPage extends LookupPage {
@step("VehicleLookupAddressPage >> Lookup by address: ")
async handleVehicleLookupAddressPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
const { customerDetails, vehicleDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage);
await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
await this.handleZipValidation(testData);
}
}

View file

@ -34,11 +34,11 @@ export class VehicleLookupLicensePage extends LookupPage {
@step("VehicleLookupLicensePage >> Lookup by license plate: ")
async handleVehicleLookupLicensePage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
const { customerDetails, vehicleDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage);
await this.enterPlateDetails(vehicleDetails!);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
await this.handleZipValidation(testData);
}
}

View file

@ -24,11 +24,11 @@ export class VinLookupPage extends LookupPage {
@step("VinLookupPage >> Lookup by VIN: ")
async handleVehicleLookupVinPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData;
const { customerDetails, vehicleDetails } = testData;
let vin: string;
if (Array.isArray(vehicleDetails?.vin)) {
vin = vehicleDetails.vin[0];
vin = vehicleDetails!.vin[0];
} else {
vin = vehicleDetails?.vin || '';
}
@ -36,6 +36,6 @@ export class VinLookupPage extends LookupPage {
await this.validateProgressBar(ProgressBarPercentages.VinLookupPage);
await this.enterVin(vin);
await this.enterZip(customerDetails!.address.postalCode!);
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
await this.handleZipValidation(testData);
}
}

View file

@ -57,7 +57,7 @@ export class AddressForm extends BasePage {
);
});
await this.page.hover(".pac-container .pac-item");
await this.addressSuggestionList.hover();
await new Promise(resolve => setTimeout(resolve, 1000));
await this.addressSuggestionList.click(); // Select the first suggestion
//await this.streetAddressTextBox.press('Tab'); // Move focus to ZIP code field

View file

@ -28,6 +28,7 @@ import insuranceITAC21stCenturyTests from "./InsuranceITAC21stCentury";
import insuranceGeicoTests from "./InsuranceGeico";
import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState";
import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive";
import insuranceOEMAllstateTests from "./InsuranceOEMAllState";
import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay";
import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal";
import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff";
@ -82,6 +83,7 @@ const allStandardTests = [
{name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests},
{name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests},
// {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests},
{name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests},
{name: "InsuranceUnverified", tests: insuranceUnverifiedTests},
// {name: "InsuranceGeico", tests: insuranceGeicoTests},
// {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests}
@ -323,7 +325,7 @@ async function runWorkflow(page: Page, testCase: TestCase) {
}
export async function handleInsuranceFlow(testCase: TestCase) {
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, flow } = testCase.testData;
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, isForcedOEM, flow } = testCase.testData;
// Check if the insurance policy has endorsements
const hasEndorsements = endorsements && endorsements.length > 0;
@ -332,6 +334,13 @@ export async function handleInsuranceFlow(testCase: TestCase) {
let insuranceCompanyPage = testCase.pages.insuranceCompanyPage;
await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData);
// Handle OEM scenario
if (isForcedOEM) {
// User should be on heritage problem glass questions page after selecting Allstate on insurance company page
let HeritageProblemGlassQuestionsPage = testCase.pages.heritageProblemGlassQuestionsPage;
await HeritageProblemGlassQuestionsPage.handleHeritageProblemGlassQuestionsPage(testCase.testData);
}
// Handle ccPolicyInfoPage
let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage;
await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData);

View file

@ -1,6 +1,6 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, PaymentType } from 'safelite-playwright-core';
import { ServiceLocation, ServicePackage, PaymentType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
@ -29,7 +29,8 @@ const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
postalCode: '21237'
}
},
servicePackage: ServicePackage.Premium,
// Override vehicle details
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
@ -59,7 +60,8 @@ const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
// Override payment details
paymentDetails: {
paymentType: PaymentType.PayAtService
paymentType: PaymentType.PayAtService,
promoCode: 'digitalrain'
}
}

View file

@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
appointmentDetails: {
serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "6826 sawmill rd, Columbus, OH 43235"
shopAddress: "1343 cameron ave, lewis center, oh 43035"
},
// Override vehicle details

View file

@ -0,0 +1,88 @@
//Imports here
import { ITestData } from 'framework/TestData'
import { ServiceLocation, DamageType, PaymentType, PartQuestionType } from 'safelite-playwright-core';
import { ITestCase } from '../framework/Typedefs'
import { PaymentMethod } from "framework/localTypes/Enums";
import { VehicleLookupType } from 'safelite-playwright-core';
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
// Set the seed based on test name for consistent but unique data
setFakerSeedFromTestName("InsuranceOEMAllstate");
// Now get the test data with the seeded faker
const insuranceOEMAllstateData: Partial<ITestData> = {
...getDefaultTestData(), // Get default data with current seed
// Key feature: Insurance flow with GEICO
paymentMethod: PaymentMethod.Insurance,
// Insurance claim flags
isPolicyFound: false,
isPolicyUnverified: true,
isRecalVehicle: true,
isForcedOEM: true,
// Override customer details with specific name and California location
customerDetails: {
...getDefaultTestData().customerDetails!,
firstName: 'Jane',
lastName: 'OEMTest',
address: {
...getDefaultTestData().customerDetails!.address,
city: 'Richmond',
state: 'Virginia',
postalCode: '23219'
}
},
// Insurance claim details
claimDetails: {
client: 'Allstate',
policyNumber: 'MockOEMTest',
policyDeductible: "Unverified",
policyZip: '43085',
damageDate: new Date(new Date().setDate(new Date().getDate() - 1)).toLocaleDateString('en-US', {month: '2-digit', day: '2-digit', year: 'numeric'}),
damageCause: DamageType.Rock
},
// Hyundai vehicle details with VIN lookup
vehicleDetails: {
...getDefaultTestData().vehicleDetails!,
year: '2024',
make: 'Jeep',
model: 'Wrangler',
style: '4 door utility',
vehicleLookupType: VehicleLookupType.Zip,
},
// Part questions related to recalibration
partQuestions: [
{
partQuestionType: PartQuestionType.GeneralQuestion1,
isOnPage: true,
optionToSelect: 'Yes'
},
],
// Override for in-shop appointment
appointmentDetails: {
serviceLocation: ServiceLocation.InShop,
shopAddress: '5719 Brandt Pike, Dayton, OH 45424',
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate
},
// Override payment details (empty because we skip payment method page in insurance flow)
paymentDetails: {}
}
const insuranceOEMAllstateTests: ITestCase[] = [];
const tc = {
name: `InsuranceOEMAllstate`,
tags: ['@E2E','@InsuranceOEMAllstate', '@test_report', '@Insurance'],
testData: insuranceOEMAllstateData
};
insuranceOEMAllstateTests.push(tc);
export default insuranceOEMAllstateTests;

View file

@ -75,10 +75,10 @@ const lookupTypesToTest: LookupTestCase[] = [
},
customerDetails: {
address: {
street: '4076 Spectacle Drive',
street: '4219 Turpin Ln',
city: 'Columbus',
state: 'OH',
postalCode: '59261',
postalCode: '43230',
country: 'United States'
}
},

View file

@ -114,7 +114,7 @@ body {
debug: true,
},
{
name: 'Test',
name: 'SysTest',
hostName: 'www-test2.safelite.com',
apiHostname: 'digitalapi.test.safelite.io',
debug: true,
@ -142,83 +142,178 @@ body {
return match;
}
function getExperimentSettingsFromVuex(vuexData) {
const experiments = vuexData?.applicationUser?.experiments;
if(experiments) {
return experiments
.filter((e) => !!e.isActive)
.map((e) => e.settings)
.reduce((prev, next) => Object.assign(prev, next), {}) ?? {};
}
return {};
}
function extractDataFromVuex(existingVuexData) {
let bailoutInfo = [];
let headerInfo = [];
try {
// =============== Collect diagnostic data:
bailoutInfo = [];
// App & Site Name
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
// Not included (yet) from heritage:
// - Site ID
// - Code
// - Module
// - Page
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
// - URL
// - Server
const sessionId = getCookieValueByName('sid');
bailoutInfo.push({ name: 'Session Id', value: sessionId });
const userAgent = window.navigator.userAgent;
bailoutInfo.push({ name: 'User Agent', value: userAgent });
// - IP
// - Session Log Sequence Number
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
// - Referral Insured Zip
// - Referral Insured Service Zip
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
const vehicle = existingVuexData.order?.vehicle;
const vehicleString = vehicle?.year
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
: null;
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
const environmentData = getCurrentEnvironmentData();
const userIdCookieName = `FunnelUserId-${environmentData?.name}`;
bailoutInfo.push({ name: 'User Id', value: getCookieValueByName(userIdCookieName) });
// - Parent Account Name
// - Client GUID
// =============== Collect header data:
headerInfo = [];
headerInfo.push({ name: 'X-Experiment-Data', value: JSON.stringify(getExperimentSettingsFromVuex(existingVuexData)) });
headerInfo.push({ name: 'X-Application-Name', value: 'FixMyGlass' });
const sessionKeyCookieName = `FunnelSessionKey-${environmentData?.name}`;
headerInfo.push({ name: 'X-Session-Sequence-Number', value: getCookieValueByName(sessionKeyCookieName) });
headerInfo.push({ name: 'X-Referral-Sequence-Number', value: existingVuexData?.order?.referralSequenceNumber });
headerInfo.push({ name: 'X-Enterprise-Order-Number', value: existingVuexData?.order?.eon });
headerInfo.push({ name: 'log-enabled', value: existingVuexData?.applicationUser?.loggingOption ?? false });
} finally {
return {
headerInfo: headerInfo,
bailoutInfo: bailoutInfo
};
}
}
function generateRequestHeader(headerInfo) {
let headers = {};
headerInfo.forEach(
keyPair => {
headers[keyPair.name] = keyPair.value;
}
);
headers['X-Transaction-Id'] = crypto.randomUUID();
headers['Content-Type'] = 'application/json';
return headers;
}
function getFieldWithName(info, name) {
const match = info?.find(
(row) => row.name === name
);
return match?.value;
}
function clearApplicationData() {
window.localStorage.removeItem('vuex');
const environmentData = getCurrentEnvironmentData();
const cookieNames = [
`FunnelUserId-${environmentData?.name}`,
`FunnelSessionKey-${environmentData?.name}`,
`FunnelSessionInfo-${environmentData?.name}`,
`sid`,
`dxdev`,
];
cookieNames.forEach(
(cookieName) => {
let cookieToAdd = `${cookieName}=undefined; path=/; domain=${environmentData.hostName}; max-age=0`;
document.cookie = cookieToAdd;
}
)
}
window.onload = async () => {
// =============== Check for session info:
const existingVuexDataJSON = window.localStorage.getItem('vuex');
const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null;
const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo');
const existingBailoutInfo = existingBailoutInfoJSON ? JSON.parse(existingBailoutInfoJSON) : null;
const existingHeaderInfoJSON = window.sessionStorage.getItem('headerInfo');
const existingHeaderInfo = existingHeaderInfoJSON ? JSON.parse(existingHeaderInfoJSON) : null;
console.log(`================ VUEX DATA`);
console.log(existingVuexData);
console.log(`================ PRIOR BAILOUT DATA`);
console.log(existingBailoutInfo);
console.log(`================ PRIOR HEADER DATA`);
console.log(existingHeaderInfo);
let bailoutInfo = null;
let headerInfo = null;
if(existingVuexData) {
try {
// =============== Collect diagnostic data:
bailoutInfo = [];
const results = extractDataFromVuex(existingVuexData);
bailoutInfo = results.bailoutInfo;
headerInfo = results.headerInfo;
// App & Site Name
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
clearApplicationData();
// Not included (yet) from heritage:
// - Site ID
// - Code
// - Module
// - Page
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
// - URL
// - Server
const sessionId = getCookieValueByName('sid');
bailoutInfo.push({ name: 'Session Id', value: sessionId });
const userAgent = window.navigator.userAgent;
bailoutInfo.push({ name: 'User Agent', value: userAgent });
// - IP
// - Session Log Sequence Number
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
// - Referral Insured Zip
// - Referral Insured Service Zip
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
const vehicle = existingVuexData.order?.vehicle;
const vehicleString = vehicle?.year
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
: null;
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
// - Parent Account Name
// - Client GUID
} finally {
// If any information gathered, write to session storage.
if(bailoutInfo) {
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
}
// Then *always* clear vuex data.
window.localStorage.removeItem('vuex');
if(bailoutInfo) {
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
}
} else if(existingBailoutInfo) {
// Proceed with prior data.
bailoutInfo = existingBailoutInfo;
if(headerInfo) {
window.sessionStorage.setItem('headerInfo', JSON.stringify(headerInfo));
}
} else {
bailoutInfo = existingBailoutInfo ?? [];
headerInfo = existingHeaderInfo ?? [];
}
const environmentInfo = getCurrentEnvironmentData();
@ -275,11 +370,11 @@ body {
);
const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`;
const headers = generateRequestHeader(headerInfo);
const request = new Request(endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: headers,
body: JSON.stringify({
entry: entryString,
}),
@ -292,6 +387,38 @@ body {
console.error(`=== ERROR SENDING LOGGING`);
console.error(e);
}
try {
const endpointUrl = `https://${apiHostname}/analytics/api/v1/analytics/log-page-view`;
const headers = generateRequestHeader(headerInfo);
const payload = {
AppName: "FixMyGlass",
action: "",
applicationName: "FixMyGlassNextGen",
event: "ENTRY",
experimentsForUser: [],
pageName: "static-error",
parentAccountNumber: getFieldWithName(bailoutInfo, 'Parent Account Number') ?? 0,
referralSequenceNumber: getFieldWithName(bailoutInfo, 'Referral Seq Number'),
sessionId: getFieldWithName(bailoutInfo, 'Session Id'),
sessionKey: getFieldWithName(headerInfo, 'X-Session-Sequence-Number'),
shouldUseSessionId: false,
userId: getFieldWithName(bailoutInfo, 'User Id'),
};
const request = new Request(endpointUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
});
const response = await fetch(request);
} catch(e) {
console.error(`=== ERROR SENDING PAGEVIEW`);
console.error(e);
}
}
};
</script>

View file

@ -509,7 +509,7 @@
'AccountService': {
baseUrl: apiUrl + '/account/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/account/insurance-companies', method: 'GET' },
{ path: '/account/account-details/373341', method: 'GET' },
{ path: '/account/bill-to-account-number?ParentAccountNumber=373341&ProviderNumber=01820&IsItac=false&TypeOfClaim=Personal&LineOfBusiness=Commercial', method: 'GET' }
@ -518,7 +518,7 @@
'LocationService': {
baseUrl: apiUrl + '/location/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/location/zip/43015/Replace', method: 'GET' },
{ path: '/location/alert-reasons/03357', method: 'GET' },
{ path: '/location/providers/43015/Replace/100/167132/true/CR00046193/false', method: 'GET' },
@ -528,7 +528,7 @@
'VehicleService': {
baseUrl: apiUrl + '/vehicle/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/vehicle/years', method: 'GET' },
{ path: '/vehicle/makes/1997', method: 'GET' },
{ path: '/vehicle/Models/1997/Toyota', method: 'GET' },
@ -539,13 +539,12 @@
'PartService': {
baseUrl: apiUrl + '/parts/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/parts/damage-options/CR00046193', method: 'GET' },
{ path: '/parts/rain-repel', method: 'GET' },
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
{ path: '/parts/wipers/CR00046193/43015', method: 'GET' },
{ path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' },
{ path: '/parts/service-package-discount', method: 'POST', body: {"glassPieces":[{"location":"Windshield","name":"Single"}],"damageType":"Replace","AppName":"FixMyGlass"} },
{ path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' },
]
},
@ -553,7 +552,7 @@
'PriceService': {
baseUrl: apiUrl + '/price/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/price/taxed-order-items?ParentAccountNumber=167132&BillToAccountNumber=87291&ProviderNumber=003357&AppointmentType=Mobile&ServiceLocation.City=asdf&ServiceLocation.State=OH&ServiceLocation.ZipCode=43015&lineItems[0].partNumber=FW02018GBNN&lineItems[0].laborAmount=60&lineItems[0].kitPrice=0&lineItems[0].sellingPrice=297.33&lineItems[1].partNumber=GGG%202018&lineItems[1].laborAmount=0&lineItems[1].kitPrice=0&lineItems[1].sellingPrice=13.4&lineItems[2].partNumber=RECYCLE%20FEE&lineItems[2].laborAmount=39.99&lineItems[2].kitPrice=0&lineItems[2].sellingPrice=0&ServerData=' + encodeURIComponent(environmentContext.serverData), method: 'GET' },
{ path: '/price/order-items', method: 'POST', body: { "parentAccountNumber": 167132, "billToAccountNumber": "87291", "providerNumber": "03357", "carId": "CR00046193", "year": 1997, "make": "Toyota", "model": "Camry", "eon": environmentContext.eon, "state": "OH", "referralNumber": environmentContext.referralNumber, "referralSequenceNumber": environmentContext.referralNumber, "zipCode": "43015", "serviceZipCode": "43015", "referralDate": "2025-10-29T10:55:27.18", "isReplacement": true, "deductible": 0, "coverageStatus": null, "lineItems": [ { "partNumber": "RECYCLE FEE" }, { "partNumber": "FW02018GBNN" }, { "partNumber": "GGG 2018" }, { "partNumber": "SBB19" }, { "partNumber": "SBB21" } ], "serverData": "", "AppName": "FixMyGlass" } },
{ path: '/price/order-promo', method: 'POST', body: {"promoCode":"WEBSL45","addableVaps":[{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null}],"order":{"appointmentType":"Mobile","carId":"CR00046193","correlationId":"b0261e48-1111-44fc-abc9-34b41380687c","eon":environmentContext.eon,"isInsurance":false,"isRepair":false,"glassToReplace":[{"location":"Windshield","name":"Single"}],"lineItemsOnOrder":[{"partNumber":"FW02018GBNN","basePartNumber":"FW02018","description":"solar","color":"Green Tint, Blue Shade","partType":"WINDSHIELD","canSafeliteRecalibrate":false,"requiresRecalibration":false,"requiresCapabilityQuestions":false,"recalibrationType":null,"childParts":[{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94}],"kitPrice":0,"sellingPrice":368.94,"laborAmount":60,"id":"4326cee6-b39e-41ff-bb29-df9220e32298","isChildPart":false,"salesTax":30.02},{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94},{"partNumber":"RECYCLE FEE","description":null,"partType":"REPLACE FEE","isInsurable":null,"isChildPart":false,"laborAmount":39.99,"sellingPrice":0,"kitPrice":0,"salesTax":2.8,"id":"3771aed5-0de1-4f0e-b1ad-394f73e08f38","cartItemType":"REPLACE FEE"},{"partNumber":"MOBILE FEE","description":null,"partType":"MOBILE FEE","isInsurable":true,"isChildPart":false,"laborAmount":0,"sellingPrice":0,"kitPrice":0,"salesTax":0,"id":"2bc936ec-ef5e-4fe2-95fb-e946792b1ae5"},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":3.15,"cartItemType":"RAIN REPEL"}],"parentAccountNumber":"167132","referralSequenceNumber":environmentContext.referralNumber,"serviceState":"OH","serverData":environmentContext.serverData,"vehicleYear":"1997","zipCodeOrProviderCtu":"03357"},"AppName":"FixMyGlass"} },
@ -563,7 +562,7 @@
'ScheduleService': {
baseUrl: apiUrl + '/schedule/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/schedule/mobile-time-slots', method: 'POST', body: {"startDate":startDate,"endDate":endDate,"applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"SBB19","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"SBB21","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"RAIN REPEL","partType":"RAIN REPEL","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"zipCode":"43015","AppName":"FixMyGlass"} },
{ path: '/schedule/shop-time-slots', method: 'POST', body: {"providerNumber":"003417","startDate":startDate,"endDate":endDate,"shopAppointmentType":"InshopOrDropoff","applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"AppName":"FixMyGlass"} }
]
@ -571,25 +570,25 @@
'AnalyticsService': {
baseUrl: apiUrl + '/analytics/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
]
},
'ExperimentService': {
baseUrl: apiUrl + '/experiments/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
]
},
'OrderService': {
baseUrl: apiUrl + '/order/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
]
},
'ContentService': {
baseUrl: apiUrl + '/content/api/v1',
endpoints: [
//{ path: '/healthcheck', method: 'GET' },
{ path: '/healthcheck', method: 'GET' },
{ path: '/content/fmg/vehicle', method: 'GET' },
]
}
@ -674,7 +673,7 @@
}
function updateAtAGlanceText() {
const healthyCount = Object.values(healthData).filter(service => service.status === 'healthy').length;
const healthyCount = Object.values(healthData).filter(service => (service.status === 'healthy' || service.status === 'degraded')).length;
const totalCount = Object.values(healthData).length;
let text = 'Healthy';
let className = 'stat-pill stat-pill-healthy';
@ -763,14 +762,15 @@
// Determine overall service status
const endpoints = healthData[serviceName].endpoints;
const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length;
const unhealthyCount = endpoints.filter(ep => isUnhealthy(ep)).length;
const totalCount = endpoints.length;
if (healthyCount === totalCount) {
healthData[serviceName].status = 'healthy';
} else if (healthyCount > 0) {
healthData[serviceName].status = 'degraded';
} else {
} else if (unhealthyCount > 0) {
healthData[serviceName].status = 'unhealthy';
} else {
healthData[serviceName].status = 'degraded';
}
}

View file

@ -14,6 +14,8 @@ const GaCategories = {
EVOX: "Evox",
APPOINTMENT: "Appointment",
SERVICE_LOCATION: "service-location",
CONFIRMATION_CLICKED: "confirmation clicked",
RECAL_DISCLAIMER: "Recalibration disclaimer",
};
const GaActions = {
@ -26,6 +28,9 @@ const GaActions = {
MOBILE_AVAILABLE: "mobile_available",
SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
MORE_LOCATIONS_CLICKED: "more_locations_clicked",
YES: "yes",
NO: "no",
ACKNOWLEDGED: "Acknowledged",
};
const GaLabels = {

View file

@ -22,6 +22,7 @@ const applicationConfig = {
PIA_ERROR_URL:
location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel",
CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/confirmation",
PIA_ADYEN_RETURN_URL: location.protocol + "//" + location.host + "/fmg/virtual/payment/return",
GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE",
YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60",
OUTLOOK_CALENDAR:
@ -31,6 +32,8 @@ const applicationConfig = {
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
RETURN_USER_PAGE: "/fmg/return-user",
ADYEN_CLIENT_KEY: process.env.__VUE_APP_ADYEN_CLIENT_KEY__,
ADYEN_ENVIRONMENT: process.env.__VUE_APP_ADYEN_ENVIRONMENT__,
};
export { applicationConfig };

View file

@ -3,7 +3,7 @@ const cartItemCategories = {
GLASS_PARTS: "glassParts",
SUPPORTING_ITEMS: "supportingItems",
PROMOS: "promos",
SERVICE_PACKAGE_DISCOUNT: "servicePackageDiscount",
QUOTE_PAGE_DISCOUNT: "quotePageDiscount",
};
export { cartItemCategories };

View file

@ -10,7 +10,7 @@ const cartItemTypes = {
PROMOS: "PROMOS",
EARLY_BIRD: "EARLY BIRD",
SUPPLIES_REPAIR: "SUPPLIES-REPAIR",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION",
};

View file

@ -76,10 +76,6 @@ const endpoints = {
url: "/parts/api/v1/parts/pricing-by-day-part-number",
method: "GET",
},
GetServicePackageDiscountPart: {
url: "/parts/api/v1/parts/service-package-discount",
method: "POST",
},
GetServiceabilityDetails: {
url: "/location/api/v1/location/serviceability-details",
method: "GET",
@ -124,6 +120,14 @@ const endpoints = {
url: "/order/api/v1/order/save-session/fmg",
method: "POST",
},
SubmitOrder: {
url: "/order/api/v1/order/submit-order",
method: "POST",
},
SubmitOrderForPia: {
url: "/order/api/v1/order/submit-order-for-pia",
method: "POST",
},
LoadSession: {
url: "/order/api/v1/order/load-session",
method: "POST",
@ -154,7 +158,7 @@ const endpoints = {
},
TaxOrderItems: {
url: "/price/api/v1/price/taxed-order-items",
method: "GET",
method: "POST",
},
LogExperimentExposureIfAssigned: {
url: "/experiments/api/v1/experiments/log-exposure",
@ -208,6 +212,18 @@ const endpoints = {
url: "/order/api/v1/order/add-donation-part",
method: "POST",
},
GetOemAfterMarketParts: {
url: "/parts/api/v1/parts/oem-after-market-parts",
method: "POST",
},
InitializeAdyenPayment: {
url: "/payment/api/v1/payment/payment/session/initialize-session",
method: "POST",
},
GetAdyenSessionResult: {
url: "/payment/api/v1/payment/payment/session/session-result",
method: "POST",
},
};
export { endpoints };

View file

@ -12,6 +12,9 @@ const experimentSettings = {
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
PIA_EXPERIENCE: "PIA Experience",
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
PIA_INSURANCE: "DisplayPIAInsurance",
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
IS_EMAIL_OPTIONAL: "isEmailOptional",
@ -33,6 +36,8 @@ const experimentSettings = {
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
OFFER_OEM: "OfferOem",
USE_ADYEN_PAYMENT: "UseAdyenPayment",
};
const experimentTriggers = {

View file

@ -10,7 +10,9 @@ const partTypeStrings = {
REPAIR_FEE: "REPAIR FEE",
EARLY_BIRD: "EARLY BIRD",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
DONATION: "DONATION",
WINDSHIELD: "WINDSHIELD",
};
export { partTypeStrings };

View file

@ -80,6 +80,10 @@ export const pageProgressMapper = {
percent: 98.5,
step: 4,
},
"payment-adyen": {
percent: 98.5,
step: 4,
},
confirmation: {
percent: 100,
step: 4,

View file

@ -0,0 +1,80 @@
import { packageNames } from "./package-names";
import { partTypeStrings } from "./part-type-strings";
export const quotePageDiscountTable = [
// Repair
{
name: "Repair Glass Only",
experimentCode: "Show_GlassOnly_Repair_Discount",
partNumber: "GL RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Repair Standard",
experimentCode: "Show_Standard_Repair_Discount",
partNumber: "STD RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Repair Premium",
experimentCode: "Show_Premium_Repair_Discount",
partNumber: "PRM RPR CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RPR GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - No recal
{
name: "Replace Glass Only",
experimentCode: "Show_GlassOnly_Replace_Discount",
partNumber: "GL RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Replace Standard",
experimentCode: "Show_Standard_Replace_Discount",
partNumber: "STD RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Replace Premium",
experimentCode: "Show_Premium_Replace_Discount",
partNumber: "PRM RPL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RPL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
// Replace - with recal
{
name: "Recal Glass Only",
experimentCode: "Show_GlassOnly_Recal_Discount",
partNumber: "GL RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "GLASS RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_ONE,
},
{
name: "Recal Standard",
experimentCode: "Show_Standard_Recal_Discount",
partNumber: "STD RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "STD RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_TWO,
},
{
name: "Recal Premium",
experimentCode: "Show_Premium_Recal_Discount",
partNumber: "PRM RCL CSH PKG",
partType: partTypeStrings.QUOTE_PAGE_DISCOUNT,
description: "PREM RCL GEOTARGET DISC",
packageLevel: packageNames.TIER_THREE,
},
];

View file

@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
const RECAL_ACK_YES = "Yes";
const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
@ -31,4 +33,5 @@ export {
PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags,
cmsWidgetFieldMappings,
RECAL_ACK_YES,
};

View file

@ -23,6 +23,7 @@ const storeActions = {
GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts",
GET_OEM_AFTERMARKET_PARTS: "getOemAfterMarketParts",
GET_WIPERS: "getWipers",
GET_RAIN_REPEL: "getRainRepel",
GET_SUPPORTING_ITEMS: "getSupportingItems",
@ -32,7 +33,6 @@ const storeActions = {
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
@ -111,6 +111,8 @@ const storeActions = {
SAVE_PAYPAL_TOKEN: "savePaypalToken",
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",
CREATE_SUBMITTED_STATE: "createSubmittedState",

View file

@ -69,6 +69,8 @@ const storeMutations = {
LOCK_TOKEN: "updateLockToken",
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",

View file

@ -466,7 +466,7 @@ export default {
const response = config.getSelectableDatesCallback({
startDateString: initialViewStartDate,
endDateString: initialViewEndDate,
providerNumber: config.providerNumber,
inshopProviderNumber: config.inshopProviderNumber,
zipCode: config.zipCode,
includeMobileTimeSlots: config.includeMobileTimeSlots,
includeInshopTimeSlots: config.includeInshopTimeSlots,

View file

@ -33,6 +33,7 @@
<div class="modal-footer">
<slot name="modal-footer-slot"></slot>
<modalButtonMain
v-if="!isFooterButtonSuppressed"
:isPrimary="isFooterButtonPrimary"
class="w-100 modal-footer-button"
:id="modalId + '-modalbtn'"
@ -67,6 +68,7 @@ export default {
staticBackdrop: Boolean,
footerButtonDisabled: Boolean,
isFooterButtonPrimary: Boolean,
isFooterButtonSuppressed: Boolean,
onModalOpenedCallback: {
type: Function,
},
@ -158,6 +160,10 @@ export default {
return this.staticBackdrop ? "static" : "true";
},
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.closeModal();
},
components: {
modalButtonMain,
},

View file

@ -145,9 +145,7 @@ export default {
return "$" + decimalPrice.toFixed(2);
},
hasPackageDiscount() {
return (
this.additionalButtonData.servicePackageDiscount && this.additionalButtonData.Text
);
return this.additionalButtonData.hasDiscount && this.additionalButtonData.Text;
},
},
};

View file

@ -4,11 +4,11 @@
class="package-label pricing-by-day-pkg-lbl"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
!this.additionalButtonData.servicePackageDiscount ? 'adjust-top' : '',
!this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
]"
for="testradio">
<div class="package-specs">
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
<div v-if="this.additionalButtonData.hasDiscount" class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
@ -84,10 +84,7 @@
</div>
<div
v-if="
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span>
</div>

View file

@ -190,6 +190,7 @@ import { cartItemCategories } from "@/constants/cart-item-categories";
import { cartItemTypes } from "@/constants/cart-item-types";
import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance";
import { experimentSettings } from "@/constants/experiments";
import { quotePageDiscountTable } from "@/constants/quote-page-discounts";
export default {
name: "cart",
@ -256,11 +257,11 @@ export default {
return subTotal;
},
getServicePackageDiscount() {
const servicePackageDiscountLineItems = this.servicePackageDiscountCartItem;
getQuotePageDiscount() {
const quotePageDiscountLineItems = this.quotePageDiscountCartItem;
let subTotal = 0;
subTotal += servicePackageDiscountLineItems?.subTotal ?? 0;
subTotal += quotePageDiscountLineItems?.subTotal ?? 0;
return subTotal;
},
getVapsCartItemsForSelectedPackage(packageName) {
@ -303,7 +304,8 @@ export default {
getLineItemAmount(amount, useVerifyingText, category) {
if (useVerifyingText) return this.verifyingCoverageText;
return category == "promos" || category == "servicePackageDiscount"
return category == cartItemCategories.PROMOS ||
category == cartItemCategories.QUOTE_PAGE_DISCOUNT
? "(" + this.currencyFormatter.format(amount * -1) + ")"
: this.currencyFormatter.format(amount);
},
@ -316,16 +318,21 @@ export default {
if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) {
this.saveVaps(this.lineItems);
// Check if service package discount should be removed after vaps change
if (
this.servicePackageDiscountCartItem &&
this.discountPackageNames != this.packageLevel
) {
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.servicePackageDiscountCartItem.cartItemType
if (this.quotePageDiscountCartItem) {
const existingLineItem = this.quotePageDiscountCartItem.lineItems.at(0);
const discountInfo = quotePageDiscountTable.find(
(info) => info.partNumber === existingLineItem?.partNumber
);
shouldSaveSupportingItems = true;
const packageLevelForDiscount = discountInfo?.packageLevel;
if (this.packageLevel !== packageLevelForDiscount) {
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
(lineItemsToKeep) =>
lineItemsToKeep.cartItemType !=
this.quotePageDiscountCartItem.cartItemType
);
shouldSaveSupportingItems = true;
}
}
}
if (shouldSaveSupportingItems) {
@ -483,8 +490,8 @@ export default {
cartItems.push(this.mobileFeeCartItem);
}
if (this.servicePackageDiscountCartItem) {
cartItems.push(this.servicePackageDiscountCartItem);
if (this.quotePageDiscountCartItem) {
cartItems.push(this.quotePageDiscountCartItem);
}
if (this.premiumAppointmentDiscountCartItem) {
@ -500,12 +507,6 @@ export default {
return cartItems;
},
},
discountPackageNames() {
const discountServicePackage = experimentMixin.methods.getSettingValue(
experimentSettings.PROMO_ON_PACKAGE
);
return getDiscountedPackageName(discountServicePackage);
},
servicePackageTitleWidget() {
const servicePackageNames = this.getCmsContent(
this.servicePackageOptionsCmsName,
@ -550,8 +551,8 @@ export default {
}
packagePrice += this.getVapsPrice(this.packageLevel);
if (this.servicePackageDiscountCartItem) {
packagePrice -= this.getServicePackageDiscount();
if (this.quotePageDiscountCartItem) {
packagePrice -= this.getQuotePageDiscount();
}
return packagePrice;
@ -928,25 +929,25 @@ export default {
}
return cartItem;
},
servicePackageDiscountCartItemName() {
quotePageDiscountCartItemName() {
return this.getCmsContent("ServicePackageDiscountTextWidget", "Text");
},
servicePackageDiscountCartItem() {
quotePageDiscountCartItem() {
let cartItem = null;
const servicePackageDiscountLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT
const quotePageDiscountLineItem = this.supportingItems.find(
(lineItem) => lineItem.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT
);
if (servicePackageDiscountLineItem) {
if (quotePageDiscountLineItem) {
cartItem = {
name:
this.servicePackageDiscountCartItemName +
this.quotePageDiscountCartItemName +
Math.abs(
baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem)
baseMixin.methods.getTotalLineItemPrice(quotePageDiscountLineItem)
),
category: cartItemCategories.SERVICE_PACKAGE_DISCOUNT,
cartItemType: cartItemTypes.SERVICE_PACKAGE_DISCOUNT,
category: cartItemCategories.QUOTE_PAGE_DISCOUNT,
cartItemType: cartItemTypes.QUOTE_PAGE_DISCOUNT,
isDisplayed: true,
isRemovable: false,
subTotal: 0,
@ -955,15 +956,15 @@ export default {
isCoveredByInsurance: false,
};
servicePackageDiscountLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(servicePackageDiscountLineItem);
quotePageDiscountLineItem.cartItemType = cartItem.cartItemType;
cartItem.lineItems.push(quotePageDiscountLineItem);
cartItem.subTotal +=
(servicePackageDiscountLineItem.kitPrice ?? 0) +
(servicePackageDiscountLineItem.laborAmount ?? 0) +
(servicePackageDiscountLineItem.sellingPrice ?? 0);
(quotePageDiscountLineItem.kitPrice ?? 0) +
(quotePageDiscountLineItem.laborAmount ?? 0) +
(quotePageDiscountLineItem.sellingPrice ?? 0);
cartItem.salesTax += servicePackageDiscountLineItem.salesTax ?? 0;
cartItem.salesTax += quotePageDiscountLineItem.salesTax ?? 0;
}
return cartItem;
@ -972,7 +973,7 @@ export default {
let cartItem = null;
const otherSupportingItems = this.supportingItems.filter((item) => {
return item.partType != partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
});
// Don't include fees they are part of the package total
let otherSupportingItemsLineItems =

View file

@ -75,17 +75,12 @@ export default {
:deep(a) {
text-decoration: none;
&.new-window-link {
font-family: Urbanist, Arial, Helvetica, sans-serif;
margin: 0 0.75rem;
color: $gray-600;
font-weight: 400;
}
&.new-window-link,
&.navigation-link {
font-family: Urbanist, Arial, Helvetica, sans-serif;
font-family: UrbanistRegular, Arial, Helvetica, sans-serif;
margin: 0 0.75rem;
color: $gray-600;
font-weight: 400;
font-weight: initial;
}
}

View file

@ -57,10 +57,6 @@ export default {
modalWidgetName: String,
pageName: String,
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal?.closeModal();
},
computed: {
buttonText() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");

View file

@ -54,10 +54,6 @@ export default {
mounted() {
if (this.showSaveProgressPopup) this.modal.openModal();
},
unmounted() {
// remove any modal effects before leaving page (e.g. user hits browser back button)
this.modal.closeModal();
},
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
props: {
modelValue: Object,

168
src/helpers/adyen-helper.js Normal file
View file

@ -0,0 +1,168 @@
import { AdyenCheckout } from "@adyen/adyen-web/auto";
import store from "@/store";
import globalMethods from "@/global-methods";
import { endpoints } from "@/constants/endpoints";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { paymentMethods } from "@/constants/payment-method-constants";
import { applicationConfig } from "../constants/application-config";
export async function getNewSession(sessionConfig) {}
export async function getSessionInfo(sessionId, encryptedSessionResult, pageNameToLog = "") {
const order = store.getters.order;
const location = getLocationInfo(order);
const workOrder = getWorkOrderLastSixDigits(order);
const system = getSourceSystem();
const request = {
sessionId: sessionId,
sessionResult: encryptedSessionResult,
zipCodeCtu: location.zipCodeCtu,
workOrderNumber: workOrder,
sourceSystem: system,
};
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: request,
logApiCall: true,
pageNameToLog: pageNameToLog,
});
return response?.data;
}
export async function createAdyenCheckout({
session: { id, sessionData },
handlers: { onPaymentCompleted, onPaymentFailed, onError },
options: { showPayButton = true, amount },
}) {
const config = {
session: {
id: id,
sessionData: sessionData,
},
clientKey: applicationConfig.ADYEN_CLIENT_KEY,
environment: applicationConfig.ADYEN_ENVIRONMENT,
locale: "en_US",
countryCode: "US",
showPayButton: showPayButton,
translations: {
en_US: {
"creditCard.securityCode.label": "CVV/CVC",
},
},
onPaymentCompleted: onPaymentCompleted,
onPaymentFailed: onPaymentFailed,
onError: onError,
};
if (amount !== undefined && amount !== null) {
config.amount = {
value: amount,
currency: "USD",
};
}
console.log(`Creating checkout with configuration:`);
console.log(config);
return await AdyenCheckout(config);
}
export function mapAdyenToFmgPaymentMethod(adyenMethod) {
const paymentMethodMap = {
["scheme"]: paymentMethods.CREDIT_CARD,
["afterpaytouch"]: paymentMethods.AFTERPAY,
["paypal"]: paymentMethods.PAYPAL,
["applepay"]: paymentMethods.APPLE_PAY,
};
const match = paymentMethodMap[adyenMethod];
return match ?? paymentMethods.CREDIT_CARD;
}
export function mapFmgToAdyenPaymentMethod(fmgMethod) {
const paymentMethodMap = {
[paymentMethods.CREDIT_CARD]: "scheme",
[paymentMethods.AFTERPAY]: "afterpaytouch",
[paymentMethods.PAYPAL]: "paypal",
[paymentMethods.APPLE_PAY]: "applepay",
[paymentMethods.PAY_NOW]: null,
[paymentMethods.LATER]: null,
};
const match = paymentMethodMap[fmgMethod];
return match;
}
export function generateCcToken(adyenSessionInfo) {
const order = store.getters.order;
const locationInfo = getLocationInfo(order);
const ccToken = {
subscriptionId: adyenSessionInfo?.storedToken,
expMonth: adyenSessionInfo?.cardExpiryMonth,
expYear: adyenSessionInfo?.cardExpiryYear,
cardType: adyenSessionInfo?.cardType,
billToPostalCode: locationInfo.zipCode, // TODO - real data?
billToFirstName: order.customer.firstName, // TODO - real data?
billToLastName: order.customer.lastName, // TODO - real data?
referenceNumber: adyenSessionInfo?.workOrderNumber,
authCode: adyenSessionInfo?.authCode,
transactionId: adyenSessionInfo?.transactionReference,
transReferenceNumber: adyenSessionInfo?.transactionReference,
lastFour: adyenSessionInfo?.last4DigitsOfCard,
};
return ccToken;
}
function getLocationInfo(order) {
const serviceLocation = order.serviceLocation;
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
if (isMobile) {
return {
address: serviceLocation.address,
address2: serviceLocation.address2,
city: serviceLocation.city,
state: serviceLocation.state,
zipCode: serviceLocation.zipCode,
zipCodeCtu: serviceLocation.zipCodeCtu,
};
} else {
const providerInfo = serviceLocation.provider.address;
return {
address: providerInfo.streetAddress,
address2: "",
city: providerInfo.city,
state: providerInfo.state,
zipCode: providerInfo.zipCode,
zipCodeCtu: providerInfo.zipCodeCtu,
};
}
}
function getWorkOrderLastSixDigits(order) {
const workOrderNumber = order.workOrderNumber ?? "";
const tokens = workOrderNumber.split("-");
const lastSegment = tokens[tokens.length - 1] ?? "";
const numDigits = lastSegment.length;
const numZeroes = 6 - numDigits;
let result = "";
for (let i = 0; i < numZeroes; i++) {
result += "0";
}
result += lastSegment;
return result;
}
function getSourceSystem() {
return "FMG-2.0";
}

View file

@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION];
export function anyPartWithRequiresRecalFlag(lineItems) {
if (!lineItems) {
return false;
}
return lineItems.glassParts?.some((li) => li.requiresRecalibration === true);
}
export function isRecalPartOrHasChildRecalPart(lineItem) {
if (lineItem.childParts && lineItem.childParts.length > 0) {
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);

View file

@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
return package_names[discountPackage] || null;
}
export function getPackageLabel(packageName) {
switch (packageName) {
case packageNames.TIER_ONE:
return externalParamPackageLabels.GLASS_ONLY;
case packageNames.TIER_TWO:
return externalParamPackageLabels.STANDARD;
case packageNames.TIER_THREE:
return externalParamPackageLabels.PREMIUM;
default:
return null;
}
}
function maxTier(tierA, tierB) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return packageNames.TIER_THREE;

View file

@ -164,6 +164,8 @@ export default {
// await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it
await flushSaveSessionQueue();
await this.removeQPDLineItemIfExists();
await this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
this.parentAccountNumber.toString(),
@ -193,6 +195,17 @@ export default {
handlePageShow() {
window.location.reload();
},
async removeQPDLineItemIfExists() {
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.supportingItems = baseMixin.methods.filterOutQuotePageDiscountPart(
lineItems.supportingItems
);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
lineItems.supportingItems,
false
);
},
},
unmounted() {
window.removeEventListener("pageshow", this.handlePageShow);

View file

@ -25,6 +25,11 @@
cmsWidgetName="PleaseProvideAddressWidget"
class="provide-address-header" />
<textBlock
v-if="isMSRFeeApplicable()"
cmsWidgetName="VehicleKeysMSRMessageWidget"
class="keys-message" />
<textBlock
v-else
cmsWidgetName="VehicleKeysMessageWidget"
class="keys-message" />
<mobileAddressQuestions
@ -143,6 +148,9 @@ export default {
getIsVehicleProtectedFromStore() {
return store.getters.order.serviceLocation.isVehicleProtected;
},
isMSRFeeApplicable() {
return store.getters.order.isMSRFeeApplicable;
},
async forwardButtonAction() {
await this.dispatchStoreAction(
this.storeActions.SAVE_MOBILE_DETAILS,

View file

@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
//Assert
expect(spy).toHaveBeenNthCalledWith(
1,
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
false,
false
);
expect(spy).toHaveBeenNthCalledWith(
2,
"savePartQuestionAnswers",
[
{

View file

@ -188,6 +188,8 @@ export default {
}
});
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// save to vuex store as order.damage.partQuestionAnswers (array)
// used in GET_PARTS call following this one
await this.dispatchStoreAction(

View file

@ -0,0 +1,686 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<div class="container">
<div class="row">
<div class="col-12">
<div>
<alert
ref="piaErrorAlert"
class="my-4"
v-if="hasPaymentFailureError"
cmsWidgetName="PIAErrorAlertWidget"
alertClass="alert-danger"
@textLinkClicked="paymentFailedPayLater"
v-bind:isDismissible="false" />
</div>
<div id="card-container">
<div id="payment-container">
<funnelSubHeader class="py-5" cmsWidgetName="FunnelSubHeaderWidget" />
<div id="adyen-container"></div>
</div>
<div id="cart-container">
<cart
ref="cart"
:damage="damageInfo"
:availableVaps="[]"
:allowItemRemoval="false"
v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal"
:isInsurance="isInsurance"
:insuranceDeductible="currentDeductible"
:insuranceCompanyName="insuranceCompanyName"
:showInsuranceCoverageAs="showInsuranceCoverageAs"
:isItac="isItac"
:isNoComp="isNoComp"
:isCollapsible="false" />
<textBlock
class="payment-disclaimer"
marginTopSizeOverride="0"
cmsWidgetName="PaymentDisclaimerWidget" />
</div>
</div>
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
isForwardActionDisabled="true"
isSubmitHidden="true"
@back-clicked="backButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
import Form from "vee-validate";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert";
import textBlock from "@/digital-components/text-block/text-block";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import globalMethods from "@/global-methods";
import { endpoints } from "../../constants/endpoints";
import { AppointmentTypeStrings } from "../../constants/schedule-constants";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { getAmountDue } from "@/helpers/pricing-helper.js";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import { createAdyenCheckout } from "@/helpers/adyen-helper";
import { Dropin } from "@adyen/adyen-web/auto";
import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants";
import "@adyen/adyen-web/styles/adyen.css";
import { mapAdyenToFmgPaymentMethod, mapFmgToAdyenPaymentMethod } from "../../helpers/adyen-helper";
import { showFmgLoadingModal } from "../../helpers/loading-modal-helper";
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";
export default {
name: "payment-adyen",
mixins: [],
data() {
return {
sessionId: String,
dropinComponent: null,
hasPaymentFailureError: false,
};
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.name);
// Settle API calls in parallel before handling results.
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const hasExistingPiaError = to.query?.piaFailure;
// Hydrate page with results
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
if (hasExistingPiaError) {
vm.hasPaymentFailureError = true;
}
});
},
methods: {
async backButtonAction() {
// Stub, for navigating back via nav-bar.
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_BACK,
this.pageName
);
},
async forwardButtonAction() {
// Stub, for navigating forward via nav-bar
},
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;
},
async initializeAdyen() {
console.log(`Price = ${this.amountDue}`);
console.log(`Adyen Price = ${this.adyenPriceTotal}`);
const requestBody = this.adyenInitRequestInfo;
console.log(`Calling with:`);
console.log(requestBody);
const response = await globalMethods.callHttpClient({
method: endpoints.InitializeAdyenPayment.method,
endpoint: endpoints.InitializeAdyenPayment.url,
payload: requestBody,
logApiCall: true,
pageNameToLog: "payment-adyen",
});
const responseJson = response?.data;
console.log(`Got data:`);
console.log(responseJson);
const session = {
id: responseJson.sessionId,
sessionData: responseJson.sessionData,
};
this.sessionId = responseJson.sessionId;
const checkout = await createAdyenCheckout({
session: session,
handlers: {
onPaymentCompleted: (result, component) => {
console.log(`Payment completed from Adyen.`);
this.handleCompletedPayment(result);
},
onPaymentFailed: (result, component) => {
console.log(`Payment failed from Adyen`);
this.handleFailedPayment(result);
},
onError: (error, component) => {
console.log(`Error from Adyen`);
this.handleError(error);
},
},
options: {
amount: this.adyenPriceTotal,
},
});
console.log(checkout);
const configuration = {
paymentMethodsConfiguration: {
ideal: {
showImage: true,
},
paypal: {
amount: {
value: this.adyenPriceTotal,
currency: "USD",
},
environment: applicationConfig.ADYEN_ENVIRONMENT,
countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production.
blockPayPalVenmoButton: true,
blockPayPalPayLaterButton: true,
},
card: {
hasHolderName: true,
holderNameRequired: true,
billingAddressRequired: true,
name: "Credit or debit card",
},
},
};
const adyenPaymentType = mapFmgToAdyenPaymentMethod(this.piaType);
if (adyenPaymentType) {
configuration.openPaymentMethod = {
type: adyenPaymentType,
};
}
const dropin = new Dropin(checkout, configuration);
this.dropinComponent = dropin;
dropin.mount("#adyen-container");
},
async handleCompletedPayment(result) {
console.log(`Result =`);
console.log(result);
// Fetch session info
const paymentSessionRequest = {
zipCodeCtu: this.locationInfo.zipCodeCtu,
workOrderNumber: this.workOrderNumberLastSixDigits,
sourceSystem: this.sourceSystem,
sessionId: this.sessionId,
sessionResult: result?.sessionResult,
};
console.log(`Get session payload =`);
console.log(paymentSessionRequest);
showFmgLoadingModal(true);
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: paymentSessionRequest,
logApiCall: true,
pageNameToLog: "payment-adyen",
});
const responseJson = response?.data;
console.log(`Session response =`);
console.log(responseJson);
const ccToken = {
subscriptionId: responseJson?.storedToken,
expMonth: responseJson?.cardExpiryMonth,
expYear: responseJson?.cardExpiryYear,
cardType: responseJson?.cardType,
billToPostalCode: this.locationInfo.zipCode, // TODO
billToFirstName: this.$store.getters.order.customer.firstName, // TODO
billToLastName: this.$store.getters.order.customer.lastName, // TODO
referenceNumber: responseJson?.workOrderNumber,
authCode: responseJson?.authCode,
transactionId: responseJson?.transactionReference, // TODO
transReferenceNumber: responseJson?.transactionReference,
lastFour: responseJson?.last4DigitsOfCard,
};
console.log(`CC Token generated =`);
console.log(ccToken);
const paymentMethodFromSession = responseJson?.paymentMethod;
const paymentMethod = mapAdyenToFmgPaymentMethod(paymentMethodFromSession);
// Save payment type.
// For now, CC.
// Need to determine payment type from Adyen response
await this.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethod,
false
);
if (paymentMethod === paymentMethods.PAYPAL) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PAYPAL_TOKEN,
responseJson?.storedToken,
false
);
} else {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_CCTOKEN,
ccToken,
false
);
}
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
this.amountDue,
false
);
await this.saveAndSubmitWorkOrder();
},
async handleFailedPayment(result) {
console.log(`Result =`);
console.log(result);
const wasPaymentCancelled = result?.resultCode === "Cancelled";
if (!wasPaymentCancelled) {
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`;
await global.$logger.logError(stringToLog);
this.hasPaymentFailureError = true;
}
this.dropinComponent?.update();
},
async handleError(error) {
console.log(error);
const wasPaymentCancelled = error?.name === "CANCEL";
if (!wasPaymentCancelled) {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
await global.$logger.logError(stringToLog);
this.hasPaymentFailureError = true;
}
this.dropinComponent?.update();
},
async saveAndSubmitWorkOrder() {
// Work order submission after successful payment.
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
try {
await submitWorkOrder({
pageNameToLog: this.pageName,
submitAfterSave: true,
});
} catch (error) {
console.log(`error: response from submit work order: ${error.message}`);
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.pageName
);
return;
}
// clear order
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.pageName
);
},
async paymentFailedPayLater() {
console.log(`Doing final work order submission...`);
showFmgLoadingModal(true);
await this.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethods.PAY_LATER,
false
);
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
try {
await submitWorkOrder({
pageNameToLog: this.pageName,
submitAfterSave: true,
});
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.pageName
);
} catch (error) {
console.log(`Final submission failed after failed payment.`);
this.$router.handleSoftError();
return;
}
},
},
computed: {
piaType() {
return this.$store.getters.order.payment.piaType;
},
adyenInitRequestInfo() {
return {
sourceSystem: this.sourceSystem,
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
workOrderNumber: this.workOrderNumberLastSixDigits,
zipCodeCtu: this.locationInfo.zipCodeCtu,
amount: this.adyenPriceTotal,
city: this.locationInfo.city,
houseNumberOrName: this.splitStreetAddress.number,
street: this.splitStreetAddress.street,
zipCode: this.locationInfo.zipCode,
stateOrProvince: this.locationInfo.state,
returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL,
email: this.$store.getters.order.customer.emailAddress,
IP: "127.0.0.1", // TODO
firstName: this.$store.getters.order.customer.firstName,
lastName: this.$store.getters.order.customer.lastName,
idempotencyKey: this.idempotencyKey,
};
},
workOrderNumberLastSixDigits() {
const workOrderNumber = this.$store.getters.order.workOrderNumber ?? "";
const tokens = workOrderNumber.split("-");
const lastSegment = tokens[tokens.length - 1] ?? "";
const numDigits = lastSegment.length;
const numZeroes = 6 - numDigits;
let result = "";
for (let i = 0; i < numZeroes; i++) {
result += "0";
}
result += lastSegment;
return result;
},
locationInfo() {
const serviceLocation = this.$store.getters.order.serviceLocation;
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
if (isMobile) {
return {
address: serviceLocation.address,
address2: serviceLocation.address2,
city: serviceLocation.city,
state: serviceLocation.state,
zipCode: serviceLocation.zipCode,
zipCodeCtu: serviceLocation.zipCodeCtu,
};
} else {
const providerInfo = serviceLocation.provider.address;
return {
address: providerInfo.streetAddress,
address2: "",
city: providerInfo.city,
state: providerInfo.state,
zipCode: providerInfo.zipCode,
zipCodeCtu: providerInfo.zipCodeCtu,
};
}
},
splitStreetAddress() {
const address = this.locationInfo.address;
const tokens = address.split(" ") ?? [""];
const number = tokens[0];
const street = tokens.slice(1).reduce((prev, next) => `${prev} ${next}`);
return {
number: number ?? "",
street: street ?? "",
};
},
idempotencyKey() {
return `${this.$store.getters.order.referralCorrelationId}-${this.sourceSystem}`;
},
sourceSystem() {
return "FMG-2.0";
},
amountDue() {
return getAmountDue(this.$store.getters.order.lineItems);
},
adyenPriceTotal() {
return this.amountDue * 100;
},
// Cart info
damageInfo() {
return this.$store.getters.damage;
},
isInsurance() {
return this.$store.getters.payment.isInsurance;
},
isNoComp() {
return this.$store.getters.policy.isNoComp;
},
isItac() {
return this.$store.getters.policy.isItac;
},
showInsuranceCoverageAs() {
if (!this.isInsurance) {
return null;
}
if (this.isNoComp || this.isItac) {
return coverageStatus.NOCOMP;
} else {
return this.$store.getters.payment.insuranceCoverage.coverageStatus;
}
},
currentDeductible() {
return this.$store.getters.policy.currentDeductible;
},
insuranceCompanyName() {
return this.$store.getters.policy.insuranceCompanyName;
},
lineItems() {
return deepClone(this.$store.getters.lineItems);
},
},
mounted() {
this.initializeAdyen();
},
components: {
Form,
funnelHeader,
funnelSubHeader,
navbar,
cart,
alert,
textBlock,
},
};
</script>
<style lang="scss">
#card-container {
display: flex;
& > #payment-container {
flex-basis: 55%;
margin-right: 9%;
}
& > #cart-container {
flex-basis: 36%;
margin-top: 2rem;
}
@include media-breakpoint-down(lg) {
flex-direction: column;
& > #payment-container {
flex-basis: auto;
margin-right: 0;
}
& > #cart-container {
flex-basis: auto;
margin: 0 0.5rem;
}
}
.payment-disclaimer p {
font-size: 0.875rem;
line-height: 1.2;
}
}
</style>

View file

@ -170,6 +170,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas
import { debugLog } from "@/helpers/debug-log-helper";
import { ErrorMessage } from "vee-validate";
import { Field } from "vee-validate";
import experimentMixin from "../../mixins/experiment-mixin";
defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
@ -609,10 +610,17 @@ export default {
}
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_PAY_NOW,
this.pageName
const shouldUseAdyen = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.USE_ADYEN_PAYMENT,
"true"
);
const scenarioName = shouldUseAdyen
? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN
: this.navigationScenarios.CLICKED_PAY_NOW;
this.$router.navigateWithoutSaving(scenarioName, this.pageName);
},
async evaluatePromosAndTaxItemsOnOrder() {
//Revalidate promos if there are any inactive, or active promos
@ -802,11 +810,51 @@ export default {
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
if (this.isInsurance) {
return piaInsurance === "true" && this.currentDeductible > 0;
return (
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
this.currentDeductible > 0
);
} else {
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
if (piaExperience?.includes("PIA Disabled")) {
return this.isConditionalPIAEnabled;
} else {
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
}
}
},
isConditionalPIAEnabled() {
const conditionalProviderNumbers = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS
);
const conditionalProviderNumbers2 = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS_2
);
const conditionalProviderShutoffDate = this.getSettingValue(
experimentSettings.CONDITIONAL_PROVIDER_SHUTOFF_DATE
);
const allConditionalProviderNumbers = [
conditionalProviderNumbers,
conditionalProviderNumbers2,
]
.filter(Boolean)
.join(",");
const providerNumber =
this.$store.getters.order.serviceLocation.provider.providerNumber;
const appointmentDate = new Date(this.$store.getters.order.schedule.date);
const shutoffDate = new Date(conditionalProviderShutoffDate);
if (allConditionalProviderNumbers && conditionalProviderShutoffDate) {
const providerNumbersArray = allConditionalProviderNumbers
.split(",")
.map((num) => num.trim());
if (!providerNumbersArray.includes(providerNumber)) {
return false;
} else {
return appointmentDate <= shutoffDate;
}
}
return false;
},
paymentMethod() {
if (!this.isPiaEnabled) {
return paymentMethods.LATER;
@ -986,8 +1034,9 @@ export default {
}
}
}
:deep(span.label) {
font-weight: 600;
:deep(span.label),
:deep(span) {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
}
:deep(div.service-questions span) {
color: $black;

View file

@ -156,6 +156,9 @@ export default {
lastFour: lastFour,
};
console.log(`CCToken =`);
console.log(ccToken);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,

View file

@ -1,10 +1,5 @@
<template>
<div id="afterpay-banner" role="alert">
<component
:is="'script'"
src="https://js.squarecdn.com/square-marketplace.js"
async></component>
<div class="afterpay-modal-banner" role="alert">
<div>
<span
class="callout"
@ -22,22 +17,52 @@
<span v-else v-html="token"></span>
<span class="nbsp">&nbsp;</span>
</span>
<a
id="afterpay-learnmore"
href="#"
data-afterpay-modal="en_US"
data-bind="click:afterpayLearnMore">
{{ modalCopy }}
</a>
<textLink
linkType="text"
:text="bannerCta"
href="javascript:void(0)"
@click-event="openModal" />
</div>
</div>
<modal
:ref="modalName"
class="afterpay-modal"
:headerText="modalHeaderText"
:isFooterButtonSuppressed="true">
<template v-slot:modal-header-slot>
<span>{{ modalSubHeaderText }}</span>
</template>
<template v-slot>
<span class="afterpay-sections">
<span
v-for="section in afterpaySectionsContent"
:key="section"
class="afterpay-section">
<span class="section-header">
<img v-if="section.AnswerImageUrl" :src="section.AnswerImageUrl" />
<h3 v-if="section.Text" v-html="section.Text"></h3>
</span>
<span v-html="section.content"></span>
</span>
</span>
</template>
<template v-slot:modal-footer-slot>
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
</template>
</modal>
</template>
<script>
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper";
import { getArrayOfAllLineItemsAndChildParts } from "@/store";
import {
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
splitCMSCopyOnBR,
} from "@/helpers/cms-content-helper";
import textLink from "@/ux-components/text-link/text-link";
import modal from "@/digital-components/modal/modal";
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
@ -48,20 +73,16 @@ function isInlineImageToken(token) {
function isAfterpayPriceToken(token) {
return token.includes(AFTERPAY_PRICE_TOKEN);
}
function getInlineAltText(token) {
const innerTokens = token.split(",");
return innerTokens[1] ?? "";
}
export default {
name: "afterpay-modal-banner",
name: "afterpay-banner",
props: {
cmsWidgetName: String,
lineItems: Array,
isInsuranceSelected: Boolean,
afterpayExtendedPayOptionThreshold: Number,
modalWidgetName: String,
},
data() {
return {};
@ -70,6 +91,17 @@ export default {
isInlineImageToken,
isAfterpayPriceToken,
getInlineAltText,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
openModal() {
this.modal?.openModal();
},
closeModal() {
this.modal?.closeModal();
},
},
computed: {
imageUrl() {
@ -84,39 +116,40 @@ export default {
afterpayCopyTokens() {
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
},
modalCopy() {
bannerCta() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
getTierOnePackagePrice() {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
if (this.lineItems.promos) {
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT");
}
let price = baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(allLineItems)
);
if (this.lineItems.promos) {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
const promos = getPromosThatMatchLineItemsOnOrder(
this.lineItems.promos,
allLineItems
);
promos.forEach((promo) => {
price += baseMixin.methods.getTotalLineItemPrice(promo);
});
}
return price;
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalDisclaimerText() {
return this.getCmsContent(this.modalWidgetName, "BodyText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
afterpaySectionsContent() {
const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
if (!sections) return [];
sections.forEach((section) => {
const sectionContent = this.getCmsContent(section.Name, "BodyText");
section["content"] = sectionContent;
});
return sections;
},
},
components: {},
components: {
textLink,
modal,
},
};
</script>
<style lang="scss" scoped>
#afterpay-banner {
.afterpay-modal-banner {
display: flex;
flex-direction: column;
background-color: $blue-150;
@ -130,6 +163,7 @@ export default {
.callout {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
white-space: nowrap;
}
> div:first-of-type {
@ -163,15 +197,120 @@ export default {
}
@include media-breakpoint-up(md) {
padding-left: 1rem;
max-width: 32rem;
max-width: 36rem;
}
}
& .alert-heading {
font-size: 0.875rem;
}
.afterpay-modal {
&.modal.modal-component {
:deep(.modal-dialog) {
margin: 0 1rem;
top: 0;
@include media-breakpoint-up(md) {
width: 100%;
max-width: 840px;
top: 3.4rem;
right: 0;
left: 0;
transform: none;
margin: 0 auto;
padding: 0 1rem;
}
.modal-content {
border-radius: 0.5rem;
margin: 1.5rem 0;
.modal-header.mt-6 {
background-color: $blue-100;
margin-top: 0;
padding: 1.5rem 2rem 1.5rem 1.5rem;
font-family: UrbanistSemibold;
.modal-title.justify-content-center {
// OVERRIDE
justify-content: flex-start;
}
.btn-close {
top: 1.25rem;
right: 1.5rem;
}
}
.afterpay-section {
display: block;
border: 1px solid $gray-200;
border-radius: 0.25rem;
padding: 1rem;
.section-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
img {
width: 40px;
height: 40px;
}
h3 {
font-family: UrbanistBold;
font-size: 1rem;
margin: 0 0 0 0.5rem;
color: $black;
}
}
p {
font-size: 0.75rem;
}
strong {
font-family: UrbanistSemibold;
font-size: 0.875rem;
}
}
.modal-body {
padding: 1.5rem;
.afterpay-sections {
@include media-breakpoint-up(md) {
display: flex;
column-gap: 1rem;
align-items: stretch;
}
.afterpay-section {
margin-bottom: 1rem;
@include media-breakpoint-up(md) {
flex: 0 1 33%;
margin-bottom: 0;
}
&:last-of-type {
margin-bottom: 0;
}
}
}
}
}
&.modal-dialog-centered {
height: auto;
min-height: auto;
}
}
}
:deep(.modal-footer) {
position: relative;
background: $gray-100;
.modal-disclaimer {
font-style: italic;
font-size: 0.75rem;
a {
padding: 0;
line-height: 1.625;
}
}
}
}
#afterpay-learnmore {
font-weight: 600;
font-size: 0.875rem;
}
</style>

View file

@ -0,0 +1,167 @@
<template>
<div
class="oem-glass-question-container py-5 mt-3 mb-6"
:class="[isExpanded ? 'expanded' : '']">
<div class="oem-glass-question-header" @click="toggleIsExpanded">
{{ headerText }}
</div>
<div class="oem-glass-question-body">
<div class="oem-glass-question-copy" v-html="bodyText"></div>
<div class="oem-glass-question-option">
<checkboxQuestion
class="mt-3 ms-1"
cmsWidgetName="OemGlassInputQuestionWidget"
v-model="installOemGlass" />
</div>
</div>
</div>
</template>
<script>
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
export default {
name: "oem-part-question",
props: {
cmsWidgetName: String,
modelValue: Boolean,
oemGlassPartPrice: Number,
aftermarketGlassPartPrice: Number,
},
data() {
return {
isExpanded: false,
};
},
computed: {
headerText() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
bodyText() {
let priceDifference = this.oemGlassPartPrice - this.aftermarketGlassPartPrice;
let bodyText = this.getCmsContent(this.cmsWidgetName, "BodyText");
bodyText = bodyText.replace(
"{custom:OEMGLASSPRICE}",
"<span id='price-dif'>$" + priceDifference.toFixed(2) + "</span>"
);
return bodyText;
},
footerText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
installOemGlass: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
toggleIsExpanded() {
this.setIsExpanded(!this.isExpanded);
},
setIsExpanded(newVal) {
this.isExpanded = newVal;
},
},
components: {
checkboxQuestion,
},
};
</script>
<style lang="scss">
.oem-glass-question-container {
border-top: 1px solid gray;
@include media-breakpoint-up(lg) {
margin: 0 2rem;
}
.oem-glass-question-header {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
color: $blue;
display: flex;
align-items: center;
justify-content: flex-start;
cursor: pointer;
span {
flex-grow: 1;
color: $black;
}
&::after {
content: "";
transition: all 0.5s ease;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 9'%3E%3Cpath d='M7.99282 0.117702C7.75716 0.116049 7.53044 0.207797 7.3623 0.37286L0.255553 7.46878C0.0862976 7.63796 -0.00878906 7.86742 -0.00878906 8.10667C-0.00878906 8.34593 0.0862976 8.57539 0.255553 8.74457C0.424808 8.91374 0.654368 9.00879 0.893731 9.00879C1.13309 9.00879 1.36265 8.91374 1.53191 8.74457L7.99282 2.27378L14.4614 8.74457C14.6307 8.91205 14.8595 9.00547 15.0977 9.00428C15.3359 9.00308 15.5638 8.90737 15.7314 8.73819C15.8989 8.56901 15.9924 8.34022 15.9912 8.10216C15.99 7.8641 15.8942 7.63627 15.725 7.46878L8.6259 0.377963C8.45765 0.21088 8.22999 0.117289 7.99282 0.117702Z' fill='%230070d1'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center center;
color: $blue;
width: 16px;
height: 16px;
display: inline-flex;
position: relative;
margin-left: 0.5rem;
transform: rotate(180deg);
}
}
.oem-glass-question-body {
max-height: 0;
transition: all 350ms ease-in;
overflow: hidden;
visibility: hidden;
.oem-glass-question-option {
margin-top: 0.5em;
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
.oem-glass-question-strikethrough-price {
font-size: smaller;
text-decoration: line-through;
}
.oem-glass-question-free-price {
display: inline-block;
font-weight: bolder;
border-radius: 1em;
background-color: $green-300;
padding: 0 0.5em;
margin-inline-start: 0.5em;
}
}
}
&.expanded {
.oem-glass-question-header {
&::after {
transform: unset;
}
}
.oem-glass-question-body {
visibility: visible;
max-height: none;
padding-top: 0.5em;
margin-top: 0.5em;
.oem-glass-question-copy {
#price-dif {
font-weight: $font-weight-700;
color: $black;
}
}
.oem-glass-question-option {
.small {
font-weight: $font-weight-700;
color: $black;
}
}
}
}
}
</style>

View file

@ -82,7 +82,7 @@ jest.mock("@/mixins/base-mixin", () => ({
filterOutFees(items) {
return null;
},
filterOutServicePackageDiscountPart(items) {
filterOutQuotePageDiscountPart(items) {
return null;
},
isFormValid(form) {
@ -118,6 +118,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
return false;
}
if (settingName === mockExperimentSettings.OFFER_OEM) {
return false;
}
return "test";
},
},

View file

@ -31,10 +31,12 @@
:isRecalibrationOnOrder="isRecalibrationOnOrder"
:shouldHideRecalibration="shouldHideRecalibration"
@vapsItemsSelected="vapsItemsSelectedAction"
@servicePackageDiscountSelected="servicePackageDiscountSelectedAction"
@quotePageDiscountSelected="quotePageDiscountSelectedAction"
@currentNumberOfPackages="currentNumberOfPackagesAction"
@servicePackageSelected="servicePackageSelectedAction"
:servicePackage="servicePackage"
:activePromos="lineItems.promos"
:availableQuotePageDiscounts="quoteDiscountPartsInfo"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
isRequired />
@ -45,10 +47,8 @@
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
<afterpayModalBanner
v-if="showAfterpayBanner"
cmsWidgetName="AfterpayModalWidget"
:afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
:isInsuranceSelected="isInsuranceSelected"
:lineItems="lineItems" />
cmsWidgetName="AfterpayBannerWidget"
modalWidgetName="AfterpayModalWidget" />
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
<recalDisclaimer
@ -86,6 +86,13 @@
isInsuranceSelected ? isInsuranceContinueButtonText : ''
" />
<oemGlassQuestion
cmsWidgetName="OemGlassQuestionWidget"
v-if="offerOem && !isInsuranceSelected"
v-model="isOemGlassSelected"
:oemGlassPartPrice="oemGlassPartPrice"
:aftermarketGlassPartPrice="aftermarketGlassPartPrice" />
<saveProgressModalQuestion
modalWidgetName="SaveProgressModalWidget"
modalName="SaveProgressModal"
@ -129,6 +136,7 @@ import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
import oemGlassQuestion from "./oem-glass-question/oem-glass-question.vue";
// Supporting files
import baseMixin from "@/mixins/base-mixin.js";
@ -176,6 +184,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas
import { storeMutations } from "@/constants/store-mutations";
import { debugLog } from "@/helpers/debug-log-helper";
import { savePageData } from "@/router/methods/helpers/save-page-data";
import { quotePageDiscountTable } from "../../constants/quote-page-discounts";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -186,6 +195,14 @@ const ACCEPTED_QUOTE_TYPES = {
INSURANCE: "insurance",
};
function getAvailableQuotePageDiscounts() {
const activeDiscounts = quotePageDiscountTable?.filter((discountEntry) =>
experimentMixin?.methods?.hasSettingEqualTo(discountEntry?.experimentCode, "true")
);
return activeDiscounts ?? [];
}
export default {
name: "quote",
async beforeRouteEnter(to, from, next) {
@ -209,6 +226,15 @@ export default {
null,
"quote"
);
const experimentAllowsOfferOem = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.OFFER_OEM,
"true"
);
const oemGlassCheckPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_OEM_AFTERMARKET_PARTS,
null,
"quote"
);
const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder;
const skipQuoteExperiment = store.getters.applicationUser.experiments.find(
@ -282,6 +308,10 @@ export default {
resultKey: "supportingItems",
promise: supportingItemsPromise,
},
{
resultKey: "oemGlassCheck",
promise: oemGlassCheckPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
@ -302,31 +332,48 @@ export default {
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.vaps = lineItems.vaps ?? [];
const nullSafeGlassParts = lineItems.glassParts ?? [];
const servicePackageDiscountSettingValue = experimentMixin.methods.getSettingValue(
experimentSettings.SERVICE_PACKAGE_DISCOUNT
);
const isServicePackageDiscount = servicePackageDiscountSettingValue === "True";
//Service package cash discount api call when experiment is active
var servicePackageDiscountPart = [];
if (isServicePackageDiscount) {
const servicePackageDiscountPartResponse =
await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SERVICE_PACKAGE_DISCOUNT_PART,
null,
"quote"
);
if (servicePackageDiscountPartResponse.data) {
servicePackageDiscountPart.push(servicePackageDiscountPartResponse.data);
}
}
const oemGlassCheck = resultMap.oemGlassCheck;
const offerOem = experimentAllowsOfferOem ? oemGlassCheck?.offerOem : false;
const oemGlassPieceParts = oemGlassCheck?.glassPieceParts;
const oemGlassParts =
oemGlassPieceParts && oemGlassPieceParts.length > 0
? oemGlassPieceParts[0].parts
: null;
const isOemGlassSelected = store.getters.order.damage?.installOemGlass ?? false;
let alternateGlassPartsForOem;
if (oemGlassParts) alternateGlassPartsForOem = oemGlassParts;
const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts();
const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({
partNumber: partInfo.partNumber,
partType: partInfo.partType,
description: partInfo.description,
isInsurable: null,
}));
const availableLineItems = [
resultMap.rainRepel,
...resultMap.supportingItems,
...resultMap.wipers,
...nullSafeGlassParts,
...servicePackageDiscountPart,
...quotePageDiscountParts,
];
let pricedAlternateGlassParts = null;
if (alternateGlassPartsForOem && alternateGlassPartsForOem.length > 0) {
pricedAlternateGlassParts = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: alternateGlassPartsForOem,
},
"quote",
false
);
}
// When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards
var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber;
var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber;
@ -351,7 +398,7 @@ export default {
false
);
// This will remove any servicePackageDiscount item from lineItems.supportingItems
// This will remove any discount item from lineItems.supportingItems
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS,
resultMap.supportingItems,
@ -388,6 +435,41 @@ export default {
vm.holdParentAccountNumber = holdParentAccountNumber;
vm.holdBillToAccountNumber = holdBillToAccountNumber;
vm.skipToInsurance = shouldSkipToInsurance;
vm.offerOem = offerOem;
vm.alternateGlassPartsForOem = pricedAlternateGlassParts; // oemParts
vm.isOemGlassSelected = isOemGlassSelected;
const getOEMGlassPartPrice = () => {
let price = 0;
if (vm.alternateGlassPartsForOem) {
let oemGlassPart = vm.isOemGlassSelected
? vm.lineItems.glassParts
: vm.alternateGlassPartsForOem;
if (oemGlassPart) {
price =
oemGlassPart.find(
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
)?.sellingPrice || 0;
}
}
return price;
};
const getAftermarketGlassPartPrice = () => {
let price = 0;
if (vm.alternateGlassPartsForOem) {
let aftermarketGlassPart = vm.isOemGlassSelected
? vm.alternateGlassPartsForOem
: vm.lineItems.glassParts;
if (aftermarketGlassPart) {
price = aftermarketGlassPart.find(
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
)?.sellingPrice;
}
}
return price;
};
vm.oemGlassPartPrice = getOEMGlassPartPrice();
vm.aftermarketGlassPartPrice = getAftermarketGlassPartPrice();
if (revalidatePromoResponse) {
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
@ -453,10 +535,9 @@ export default {
)
: baseMixin.methods.filterOutFees(availableLineItems);
const lineItemsNoDiscount =
baseMixin.methods.filterOutServicePackageDiscountPart(
lineItemsForCalculatingPrice
);
const lineItemsNoDiscount = baseMixin.methods.filterOutQuotePageDiscountPart(
lineItemsForCalculatingPrice
);
if (isInsuranceFromQueryString != null) {
if (isInsuranceFromQueryString.toLowerCase() === "true") {
@ -556,6 +637,8 @@ export default {
availableLineItems: null,
lineItems: [],
addableVaps: [],
offerOem: null,
alternateGlassPartsForOem: null,
servicePackage: null,
holdParentAccountNumber: null,
holdBillToAccountNumber: null,
@ -564,6 +647,8 @@ export default {
isSaveProgressComplete: null,
packageNumber: null,
skipToInsurance: null,
servicePackageSelected: null,
isOemGlassSelected: false,
};
},
async mounted() {
@ -580,17 +665,8 @@ export default {
isRecalibrationOnOrder() {
return store.getters.isRecalibrationOnOrder;
},
isServicePackageDiscountOnOrder() {
return containsLineItemWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.availableLineItems
);
},
servicePackageDiscountParts() {
return findLineItemsWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.availableLineItems
);
isQuotePageDiscountOnOrder() {
return this.quoteDiscountPartsInfo?.length > 0;
},
shouldHideRecalibration() {
return (
@ -619,6 +695,23 @@ export default {
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
return thresholdAmount;
},
quoteDiscountPartsInfo() {
const defs = getAvailableQuotePageDiscounts();
const withPrices = defs
.map((def) => {
const match = this.availableLineItems?.find?.(
(pricedItem) => pricedItem.partNumber === def.partNumber
);
return {
packageLevel: def.packageLevel,
item: match,
};
})
.filter((result) => result.item);
return withPrices;
},
},
methods: {
getColCount() {
@ -662,7 +755,10 @@ export default {
vapsItemsSelectedAction(vapsItemsSelected) {
this.lineItems.vaps = vapsItemsSelected;
},
servicePackageDiscountSelectedAction(supportingItemsSelected) {
servicePackageSelectedAction(newValue) {
this.servicePackageSelected = newValue;
},
quotePageDiscountSelectedAction(supportingItemsSelected) {
this.lineItems.supportingItems = supportingItemsSelected;
},
backButtonAction() {
@ -717,6 +813,20 @@ export default {
this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems);
}
if (
this.lineItems.glassParts &&
this.alternateGlassPartsForOem &&
this.alternateGlassPartsForOem.length > 0
) {
let isOEMGlassOnOrder = this.isOEMGlassOnOrder(this.lineItems.glassParts);
if (
(this.isOemGlassSelected && !isOEMGlassOnOrder) ||
(!this.isOemGlassSelected && isOEMGlassOnOrder)
) {
this.swapOEMGlassPartsOnOrder();
}
}
if (this.lineItems.glassParts?.length > 0) {
this.dispatchStoreAction(
this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
@ -724,10 +834,10 @@ export default {
false
);
}
if (this.isInsuranceSelected && this.isServicePackageDiscountOnOrder) {
if (this.isInsuranceSelected && this.isQuotePageDiscountOnOrder) {
let supportingItems = this.lineItems.supportingItems;
supportingItems = supportingItems.filter((item) => {
return item.partType != this.servicePackageDiscountParts[0].partType;
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
});
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
@ -749,8 +859,20 @@ export default {
},
false
);
this.dispatchStoreAction(
this.storeActions.SAVE_IS_OEM_GLASS_SELECTED,
this.isOemGlassSelected,
false
);
await savePageData(this.pageName, { saveProgressPopupSkipped: true }, true);
await savePageData(
this.pageName,
{
saveProgressPopupSkipped: true,
servicePackageSelected: this.servicePackageSelected,
},
true
);
const payment = this.$store.getters.payment;
if (payment.isInsurance) {
@ -813,19 +935,23 @@ export default {
lineItemsToPrice =
baseMixin.methods.filterOutRecalibration(lineItemsToPrice);
}
if (this.isServicePackageDiscountOnOrder) {
if (this.isQuotePageDiscountOnOrder) {
lineItemsToPrice = lineItemsToPrice?.filter((item) => {
return (
item.partType != this.servicePackageDiscountParts[0].partType
);
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
});
}
let price = 0;
if (
tier.additionalButtonData?.servicePackageDiscount &&
this.isServicePackageDiscountOnOrder
tier.additionalButtonData?.hasDiscount &&
this.isQuotePageDiscountOnOrder
) {
lineItemsToPrice.push(...this.servicePackageDiscountParts);
const parts = this.quoteDiscountPartsInfo;
const matchPart = parts.find(
(discountInfo) => discountInfo.packageLevel === tier
);
if (matchPart) {
lineItemsToPrice.push(matchPart);
}
}
price += baseMixin.methods.getTierOnePackagePrice(
baseMixin.methods.filterOutFees(lineItemsToPrice)
@ -856,6 +982,26 @@ export default {
const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
},
isOEMGlassOnOrder(glassParts) {
if (glassParts && glassParts.length > 0) {
return glassParts.some((part) => part.partNumber.endsWith("OEM"));
}
return false;
},
swapOEMGlassPartsOnOrder() {
this.lineItems.glassParts.forEach((part, index) => {
if (part.partType === partTypeStrings.WINDSHIELD) {
let altPartToUse = this.alternateGlassPartsForOem.find(
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
);
if (altPartToUse) {
let childParts = this.lineItems.glassParts[index].childParts;
altPartToUse.childParts = childParts;
this.lineItems.glassParts[index] = altPartToUse;
}
}
});
},
},
components: {
funnelHeader,
@ -872,6 +1018,7 @@ export default {
recalDisclaimer,
saveProgressModalQuestion,
saveProgressPopupQuestion,
oemGlassQuestion,
},
};
</script>

View file

@ -14,6 +14,7 @@ import { experimentSettings } from "@/constants/experiments";
import baseMixin from "@/mixins/base-mixin.js";
import { partTypeStrings } from "@/constants/part-type-strings";
import { nextTick } from "vue";
import { packageNames } from "../../../constants/package-names";
jest.mock("@/store", () => ({
commit: jest.fn(),
@ -73,9 +74,9 @@ describe("service-package-question.vue", () => {
price: 35.5,
},
{
partNumber: "DISC CASHSAVE70",
partNumber: "PRM RCL CSH PKG",
description: null,
partType: "SERVICE PACKAGE DISCOUNT",
partType: "QUOTE PAGE DISCOUNT",
price: -70,
},
],
@ -105,13 +106,27 @@ describe("service-package-question.vue", () => {
description: null,
kitPrice: 0,
laborAmount: 0,
partNumber: "DISC CASHSAVE70",
partType: "SERVICE PACKAGE DISCOUNT",
partNumber: "PRM RCL CSH PKG",
partType: "QUOTE PAGE DISCOUNT",
salesTax: null,
sellingPrice: -70,
},
],
},
availableQuotePageDiscounts: [
{
packageLevel: packageNames.TIER_THREE,
item: {
description: null,
kitPrice: 0,
laborAmount: 0,
partNumber: "PRM RCL CSH PKG",
partType: "QUOTE PAGE DISCOUNT",
salesTax: null,
sellingPrice: -70,
},
},
],
activePromos: [],
};
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
@ -136,41 +151,17 @@ describe("service-package-question.vue", () => {
});
it("should return price when there is discount", () => {
const wrapper = setupMocks({});
const partType = partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
wrapper.vm.isServicePackageDiscountOnOrder = containsLineItemWithPartType(
partType,
mockProps.lineItems.supportingItems
);
const servicePackageDiscountLineItem = findLineItemsWithPartType(
partType,
mockProps.lineItems.supportingItems
);
wrapper.vm.getServicePackageDiscountPrice();
const price = baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem[0]);
const price = wrapper.vm.getDiscountPrice(packageNames.TIER_THREE);
expect(Math.abs(price)).toEqual(70);
});
it("isServicePackageDiscountOnOrder returns true if it contains service package discount lineItems", () => {
it("isDiscountOnOrder returns true if it contains quote package discount lineItems", () => {
// Arrange
const wrapper = setupMocks({});
// Assert
expect(wrapper.vm.isServicePackageDiscountOnOrder).toBe(true);
});
it("servicePackageDiscountParts should returns service package discount lineItems", () => {
// Arrange
const wrapper = setupMocks({});
// Assert
expect(wrapper.vm.servicePackageDiscountParts).toEqual([
{
partNumber: "DISC CASHSAVE70",
description: null,
partType: "SERVICE PACKAGE DISCOUNT",
price: -70,
},
]);
expect(wrapper.vm.isDiscountOnOrder).toBe(true);
});
it("should have the correct insurance pricing text when insurance is selected", () => {
// Arrange

View file

@ -34,6 +34,7 @@ import {
findLineItemsWithPartType,
getPackageNameByType,
getDiscountedPackageName,
getPackageLabel,
} from "@/helpers/service-package-helper";
import {
getPromosThatMatchLineItemsOnOrder,
@ -45,6 +46,7 @@ import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-h
import experimentMixin from "@/mixins/experiment-mixin.js";
import { experimentSettings } from "@/constants/experiments";
import { savePageData } from "@/router/methods/helpers/save-page-data";
import { debugLog } from "@/helpers/debug-log-helper";
export default {
name: "servicePackageQuestion",
@ -60,6 +62,7 @@ export default {
servicePackage: null,
isRecalibrationOnOrder: Boolean,
shouldHideRecalibration: Boolean,
availableQuotePageDiscounts: null,
},
data() {
return {
@ -83,29 +86,26 @@ export default {
if (this.activePromos?.length) this.selectDefaultPackage();
},
selectedPackageName(newValue) {
debugLog("selectedPackageName:", newValue);
const packageLabel = getPackageLabel(newValue);
debugLog("getPackageLabel:", packageLabel);
this.$emit("servicePackageSelected", `ServicePackage-${packageLabel}`);
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
const servicePackageDiscountPartInSelectedPackage =
this.getServicePackageDiscountPartForSelectedPackage(newValue);
const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue);
let supportingItems = this.supportingLineItems;
if (
containsLineItemWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
supportingItems
)
) {
supportingItems = supportingItems.filter(
(item) => item.partType !== partTypeStrings.SERVICE_PACKAGE_DISCOUNT
);
}
if (
servicePackageDiscountPartInSelectedPackage?.length > 0 &&
supportingItems != null
) {
supportingItems.push(servicePackageDiscountPartInSelectedPackage[0]);
supportingItems = supportingItems.filter(
(item) => item.partType !== partTypeStrings.QUOTE_PAGE_DISCOUNT
);
if (discountPartInSelectedPackage) {
supportingItems.push(discountPartInSelectedPackage);
}
this.$emit("servicePackageDiscountSelected", supportingItems);
this.$emit("quotePageDiscountSelected", supportingItems);
},
servicePackage(newValue) {
if (newValue !== null) {
@ -118,7 +118,7 @@ export default {
return this.availableLineItems ?? [];
},
supportingLineItems() {
return this.$store.getters.lineItems?.supportingItems;
return this.$store.getters.lineItems?.supportingItems ?? [];
},
servicePackageAnswers() {
const cmsWidgetName = this.isInsuranceSelected
@ -149,18 +149,15 @@ export default {
? this.getDiscountedPackagePriceString(answer.Name, false)
: this.getDiscountedPackagePriceString(
answer.Name,
this.isServicePackageDiscountOnOrder &&
this.discountPackageNames == answer.Name
this.hasDiscountForPackage(answer.Name)
),
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
additionalButtonData: {
strikeThroughPrice: this.getPackagePriceString(answer.Name),
servicePackageDiscount:
this.isServicePackageDiscountOnOrder &&
this.discountPackageNames == answer.Name,
hasDiscount: this.hasDiscountForPackage(answer.Name),
Text: this.isInsuranceSelected
? false
: "Special: Save $" + this.getServicePackageDiscountPrice(),
: "Special: Save $" + this.getDiscountPrice(answer.Name),
isInsuranceSelected: this.isInsuranceSelected,
},
}));
@ -168,17 +165,8 @@ export default {
this.$emit("currentNumberOfPackages", modifiedAnswers.length);
return modifiedAnswers;
},
isServicePackageDiscountOnOrder() {
return containsLineItemWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.nullSafeAvailableLineItems
);
},
discountPackageNames() {
const discountServicePackage = experimentMixin.methods.getSettingValue(
experimentSettings.PROMO_ON_PACKAGE
);
return getDiscountedPackageName(discountServicePackage);
isDiscountOnOrder() {
return this.availableQuotePageDiscounts?.length > 0;
},
frontWipersApplicableForTierTwo() {
return shouldFrontWipersBeAvailable(
@ -226,12 +214,6 @@ export default {
isRepair() {
return this.$store.getters.order.damage.isRepair;
},
servicePackageDiscountParts() {
return findLineItemsWithPartType(
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
this.nullSafeAvailableLineItems
);
},
showRecalInServicePackages() {
return this.isRecalibrationOnOrder && !this.shouldHideRecalibration;
},
@ -280,16 +262,16 @@ export default {
const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, {
discountedPrice: false,
servicePackageDiscount: false,
quotePageDiscount: false,
})
).toFixed(2);
return "$" + formattedPriceFloat;
},
getDiscountedPackagePriceString(packageName, servicePackageDiscount) {
getDiscountedPackagePriceString(packageName, quotePageDiscount) {
const formattedPriceFloat = parseFloat(
this.getPackagePrice(packageName, {
discountedPrice: true,
servicePackageDiscount,
quotePageDiscount,
})
).toFixed(2);
@ -298,7 +280,7 @@ export default {
}
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
},
getPackagePrice(packageName, { discountedPrice = false, servicePackageDiscount = false }) {
getPackagePrice(packageName, { discountedPrice = false, quotePageDiscount = false }) {
let lineItemsToPrice = [...this.nullSafeAvailableLineItems];
if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) {
@ -306,17 +288,20 @@ export default {
}
//remove service package discount part
if (this.isServicePackageDiscountOnOrder) {
if (this.isDiscountOnOrder) {
lineItemsToPrice = lineItemsToPrice.filter((item) => {
return item.partType != this.servicePackageDiscountParts[0].partType;
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
});
}
if (discountedPrice) {
lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos));
}
if (servicePackageDiscount) {
lineItemsToPrice.push(...this.servicePackageDiscountParts);
if (quotePageDiscount) {
const part = this.getDiscountPartForSelectedPackage(packageName);
if (part) {
lineItemsToPrice.push(part);
}
}
let priceFloat = this.isInsuranceSelected
? 0
@ -328,14 +313,16 @@ export default {
return priceFloat;
},
getServicePackageDiscountPrice() {
if (this.isServicePackageDiscountOnOrder) {
let price = 0;
price += baseMixin.methods.getTotalLineItemPrice(
this.servicePackageDiscountParts[0]
);
getDiscountPrice(packageName) {
const part = this.getDiscountPartForSelectedPackage(packageName);
if (part) {
const price = baseMixin.methods.getTotalLineItemPrice(part);
return Math.abs(price);
}
return null;
},
getVapsPrice(packageName, applyPromoDiscounts = false) {
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
@ -398,13 +385,18 @@ export default {
return vapsLineItemsForSelectedPackage;
},
getServicePackageDiscountPartForSelectedPackage(packageName) {
const selectedPackage = this.servicePackageAnswers.filter(
(item) => item.value === packageName
);
if (selectedPackage?.[0]?.additionalButtonData?.servicePackageDiscount) {
return this.servicePackageDiscountParts;
} else return [];
getDiscountPartForSelectedPackage(packageName) {
if (this.availableQuotePageDiscounts?.length > 0) {
const match = this.availableQuotePageDiscounts.find(
(discountInfo) => discountInfo.packageLevel === packageName
);
return match?.item;
}
return null;
},
hasDiscountForPackage(packageName) {
return !!this.getDiscountPartForSelectedPackage(packageName);
},
combineLineItemsWithoutDuplicates(lineItemsOne, lineItemsTwo) {
const combinedLineItemArray = [...lineItemsTwo];

View file

@ -5,7 +5,7 @@
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
for="testradio">
<div class="package-specs">
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
<div v-if="this.additionalButtonData.hasDiscount" class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
@ -77,10 +77,7 @@
</div>
<div
v-if="
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span>
</div>
@ -297,12 +294,11 @@ export default {
.pricing-info .insurance-pricing span {
font-size: 0.875rem;
font-family: Urbanist, Arial, Helvetica, sans-serif;
font-weight: 500;
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
}
p {
font-family: Urbanist, Arial, Helvetica, sans-serif;
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
display: flex;
justify-content: space-between;
align-items: center;
@ -315,7 +311,6 @@ export default {
color: $green;
font-size: 0.875rem;
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
font-weight: 600;
@include media-breakpoint-up(md) {
position: absolute;
bottom: 0.75rem;

View file

@ -50,7 +50,7 @@ export default {
data() {
return {
isModalOpen: false,
selectedAppontment: {
selectedAppointment: {
estimatedServiceMinutes: {
minimum: null,
maximum: null,
@ -58,6 +58,7 @@ export default {
timeSlot: null,
date: null,
},
confirmedAppointment: false,
};
},
methods: {
@ -67,21 +68,25 @@ export default {
this.modal.openModal();
},
onModalClosed() {
this.pushEvent();
this.$emit("close-mobile-first-modal");
this.confirmedAppointment = false;
},
closeModal() {
this.modal.closeModal();
},
confirmAppointment() {
let selectedTimeSlot = {
timeSlot: this.selectedAppontment.timeSlot,
routeCode: this.selectedAppontment.timeSlot.id,
timeSlot: this.selectedAppointment.timeSlot,
routeCode: this.selectedAppointment.timeSlot.id,
date: this.selectedAppointment.date,
};
this.confirmedAppointment = true;
this.$emit("confirm-appointment", selectedTimeSlot);
this.modal.closeModal();
},
setSelectedAppointment(appointment) {
this.selectedAppontment = appointment;
this.selectedAppointment = appointment;
},
getIsModalOpen() {
return this.isModalOpen;
@ -89,6 +94,21 @@ export default {
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
pushEvent() {
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
if (this.selectedAppointment && this.selectedAppointment.timeSlot) {
this.pushEventToGA(
this.GaCategories.CONFIRMATION_CLICKED,
gaAction,
this.selectedAppointment.timeSlot.startTime +
"-" +
this.selectedAppointment.timeSlot.endTime +
" " +
this.selectedAppointment.date,
true
);
}
},
},
computed: {
modalName() {
@ -111,37 +131,37 @@ export default {
if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
const serviceType = "<strong>" + this.getServiceType + "</strong>";
tokens.splice(tokens.indexOf(token), 1, serviceType);
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppontment.date) {
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppointment.date) {
const day =
"<strong>" +
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", {
new Date(this.selectedAppointment.date).toLocaleDateString("en-US", {
weekday: "long",
timeZone: "UTC",
}) +
"</strong>";
tokens.splice(tokens.indexOf(token), 1, day);
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) {
const date = new Date(this.selectedAppontment.date).toLocaleDateString(
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppointment.date) {
const date = new Date(this.selectedAppointment.date).toLocaleDateString(
"en-US",
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
);
tokens.splice(tokens.indexOf(token), 1, date);
} else if (
token.includes(INLINE_TIMESLOT_TOKEN) &&
this.selectedAppontment.timeSlot
this.selectedAppointment.timeSlot
) {
const timeslot =
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.startTime) +
this.get12HourTimeFormat(this.selectedAppointment.timeSlot.startTime) +
" - " +
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.endTime);
this.get12HourTimeFormat(this.selectedAppointment.timeSlot.endTime);
tokens.splice(tokens.indexOf(token), 1, timeslot);
} else if (
token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
this.selectedAppontment.estimatedServiceMinutes
this.selectedAppointment.estimatedServiceMinutes
) {
const inshopDurationTime = getDisplayTextForDurationLength(
this.selectedAppontment.estimatedServiceMinutes.minimum,
this.selectedAppontment.estimatedServiceMinutes.maximum
this.selectedAppointment.estimatedServiceMinutes.minimum,
this.selectedAppointment.estimatedServiceMinutes.maximum
);
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime);
}

View file

@ -0,0 +1,223 @@
// This modal is used to acknowledge the recal disclaimer during scheduling. // It will trigger when
there is a glass part with requiresRecalibration = true // but there is no recalibration part in
lineItems. This mostly occurs when // a zip code that does mobile only is selected but the vehicle
requires // recalibration.
<template>
<div id="recal-ack-modal-container">
<modal
:ref="modalName"
suppressPageScroll
:onModalClosedCallback="onModalClosed"
:footerButtonText="modalFooterText"
:isFooterButtonPrimary="true"
@footer-button-event="ackRecalDisclaimer">
<p class="modal-body-inner" v-html="modalBodyText"></p>
<div>
<div class="learn-more">
<a href="javascript:;" @click="learnMore = !learnMore">{{ learnMoreLabel }}</a>
<div v-if="learnMore">{{ learnMoreText }}</div>
</div>
<buttonQuestion
:questionText="questionText"
:answers="answersFromCms"
:groupName="groupName"
buttonTypeString="listButtonHorizontal"
v-model="selectedValues"
isRequired />
</div>
<template v-slot:modal-header-slot>
<span>{{ modalHeaderText }}</span>
</template>
</modal>
</div>
</template>
<script>
import modal from "@/digital-components/modal/modal";
import buttonQuestion from "@/digital-components/button-question/button-question";
import { RECAL_ACK_YES } from "@/constants/schedule-constants";
export default {
name: "recal-ack-modal",
props: {
agreementWidgetName: String,
modalWidgetName: String,
groupName: String,
},
data() {
return {
learnMore: false,
isModalOpen: true,
isRecalAcknowledged: "",
};
},
methods: {
openModal() {
this.modal.openModal();
},
onModalClosed() {
this.$emit("recal-ack-modal");
},
closeModal() {
this.modal.closeModal();
},
setIsModalOpen(isOpen) {
this.isModalOpen = isOpen;
},
ackRecalDisclaimer() {
this.modal.closeModal();
if (this.isRecalAcknowledged === RECAL_ACK_YES) {
this.pushEventToGA(
this.GaCategories.RECAL_DISCLAIMER,
this.GaActions.ACKNOWLEDGED,
this.isRecalAcknowledged,
true
);
this.$emit("recalAcknowledged", this.isRecalAcknowledged);
}
},
},
computed: {
questionText() {
return this.getCmsContent(this.agreementWidgetName, "QuestionText");
},
answersFromCms() {
return this.getCmsContent(this.agreementWidgetName, "Answers");
},
learnMoreLabel() {
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
},
learnMoreText() {
return this.getCmsContent(this.modalWidgetName, "FooterText2");
},
modalName() {
return this.modalWidgetName;
},
modal() {
return this.$refs[this.modalName];
},
modalHeaderText() {
return this.getCmsContent(this.modalWidgetName, "HeaderText");
},
modalBodyText() {
return this.getCmsContent(this.modalWidgetName, "BodyText");
},
modalFooterText() {
return this.getCmsContent(this.modalWidgetName, "FooterText");
},
selectedValues: {
get: function () {
return this.isRecalAcknowledged;
},
set: function (newValue) {
this.isRecalAcknowledged = this.answersFromCms.find(
(a) => a.Name === newValue
)?.Text;
},
},
},
components: { modal, buttonQuestion },
};
</script>
<style lang="scss">
#recal-ack-modal-container {
.modal-component {
@include media-breakpoint-up(md) {
.modal-dialog {
left: 0;
align-content: center;
flex-wrap: wrap;
width: 34.875rem;
transform: translate(0, 0);
.modal-content {
border-radius: $border-radius-lg;
}
}
}
.modal-dialog {
.modal-content {
.btn-close {
display: none;
}
.modal-header {
width: 75%;
padding: 0 1.25rem 0 1.25rem;
flex-direction: column;
& > span {
color: $black;
font-size: 1.25rem;
font-weight: normal;
}
.modal-title {
font-size: $font-size-20;
font-weight: $font-weight-normal;
}
}
.modal-body {
padding: 0.875rem 1.25rem 1.875rem 1.25rem;
.modal-body-inner {
ul {
list-style: none;
padding: 1rem;
background-color: #f4f4f4;
font-size: $font-size-14;
}
p {
font-size: 0.875rem;
}
}
.button-inner-wrapper > .col:first-child .list-button-horizontal {
border-bottom-left-radius: 50rem;
border-top-left-radius: 50rem;
}
.col:first-of-type .list-button-horizontal .list-button-horizontal-content,
.list-group:first-of-type
.list-button-horizontal
.list-button-horizontal-content {
border-bottom-left-radius: 50rem;
border-top-left-radius: 50rem;
}
.button-inner-wrapper > .col:last-child .list-button-horizontal {
border-bottom-right-radius: 50rem;
border-top-right-radius: 50rem;
}
.col:last-of-type .list-button-horizontal .list-button-horizontal-content,
.list-group:first-of-type
.list-button-horizontal
.list-button-horizontal-content {
border-bottom-right-radius: 50rem;
border-top-right-radius: 50rem;
}
.learn-more {
padding-bottom: 0;
font-size: 0.875rem;
a {
font-weight: 600;
}
}
.question-text {
margin-top: 0.875rem;
}
}
.modal-footer {
flex-flow: wrap-reverse;
padding-top: 0;
padding-bottom: 2rem;
}
.modal-footer .modal-footer-button:hover {
background-color: $red-1000;
border-color: $red-1100;
}
}
}
}
}
</style>

View file

@ -1,11 +1,17 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<loadingModal ref="loadingModal" />
<loadingModal notFullScreen ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<mobileFirstModal
modalWidgetName="MobileFirstModalWidget"
ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" />
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
groupName="recalAckGroup"
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
ref="recalAckModal" />
<div
class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
@ -190,6 +196,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 recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
@ -216,6 +223,7 @@ import {
RouteCodeFlags,
PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE,
RECAL_ACK_YES,
} from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -242,6 +250,7 @@ import {
isDropOffRouteCode,
getTodayDate,
} from "@/layouts/schedule/helpers/schedule-helper";
import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper";
import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
import { required } from "@/helpers/validation-rules";
@ -287,6 +296,7 @@ const getScheduleApiResponse = async ({
startDateString,
endDateString,
providerNumber,
inshopProviderNumber,
zipCode,
includeMobileTimeSlots = true,
includeInshopTimeSlots = true,
@ -329,7 +339,7 @@ const getScheduleApiResponse = async ({
startDate: apiStartDate,
endDate: apiEndDate,
shopAppointmentType: "InshopOrDropoff",
providerNumber: providerNumber,
providerNumber: inshopProviderNumber,
},
};
if (apiStartDate < apiEndDate) {
@ -429,6 +439,7 @@ export default {
name: "schedule",
data() {
return {
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
selectedDate: this.getSelectedDateFromStore(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
mobilePremiumAppointmentFee: null,
@ -469,6 +480,11 @@ export default {
navigatingForward: false,
shopListButton: shopListButton,
lastSelectedInshopOrDropoffProvider: null,
selectedMobileFirstAppointment: false,
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
),
};
},
async beforeRouteEnter(to, from, next) {
@ -597,7 +613,9 @@ export default {
pricedMobileFeePart,
shopProviderData.data
);
vm.initializeDatePicker();
vm.initializeDatePicker().then(() => {
vm.showMobileFirstModal();
});
});
},
computed: {
@ -614,7 +632,10 @@ export default {
},
},
isMobileSelected() {
return this.appointmentType == AppointmentTypeStrings.MOBILE;
return (
this.appointmentType == AppointmentTypeStrings.MOBILE ||
(this.isServiceableMobile && !this.isServiceableInshop)
);
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
@ -626,6 +647,9 @@ export default {
return this.isGlassServiceableMobile;
}
},
closestInshopProviderNumber() {
return this.shopProviderData?.shopProviders[0]?.providerNumber;
},
showTimeSlotQuestion() {
if (
this.selectedDate &&
@ -858,7 +882,9 @@ export default {
);
},
isMobileFirstModalOpen() {
return this.isMobileSelected ? this.$refs.mobileFirstModal?.getIsModalOpen() : false;
return this.selectableDatesMobile.days
? this.$refs.mobileFirstModal?.getIsModalOpen()
: false;
},
},
methods: {
@ -1121,30 +1147,7 @@ export default {
);
}
},
getTimeSlotInfo() {
// Get the current date
const currentDate = getTodayDate();
// Add 10 days to the current date
currentDate.setDate(currentDate.getDate() + 10);
// Format the date as yyyy-mm-dd
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const day = String(currentDate.getDate()).padStart(2, "0");
const formattedDate = `${year}-${month}-${day}`;
return {
date: formattedDate,
endTime: "12:00",
jobMaxMinutes: "180",
jobMinMinutes: "120",
routeCode: "3357I-03357-M-I*20988*AM",
startTime: "08:00",
};
},
onShopSelected(shopQuestionPopUpData) {
async onShopSelected(shopQuestionPopUpData) {
this.resetWaitlist();
this.preSelectedDate = null;
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
@ -1152,6 +1155,8 @@ export default {
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
});
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider?.providerNumber;
// No zipCodeData comes back if zip was not changed
if (shopQuestionPopUpData.zipCodeData) {
@ -1160,13 +1165,12 @@ export default {
selectedProvider
);
} else {
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider.providerNumber;
if (didProviderNumberChange) {
this.updateSelectedProvider(selectedProvider);
this.initializeDatePicker();
}
}
await this.initializeDatePicker();
this.setSelectedDateToFirstAvailable();
},
async getMoreScheduleData(startDate, endDate) {
// called only when "View more dates" is clicked; not on initial page load
@ -1174,6 +1178,9 @@ export default {
startDateString: startDate,
endDateString: endDate,
providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
@ -1187,12 +1194,12 @@ export default {
moreShopTimeSlots.mobileTimeSlotsData.days
);
this.showMobileFirstModal();
return moreShopTimeSlots;
},
async initializeDatePicker() {
this.isShowMobileFirstAppt && this.showLoadingModal();
this.selectedDate = null;
const includeMobileTimeSlots = this.isServiceableMobile;
@ -1204,6 +1211,9 @@ export default {
getSelectableDatesCallback: getScheduleApiResponse,
preSelectedDate: this.preSelectedDate,
providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode,
includeMobileTimeSlots: includeMobileTimeSlots,
includeInshopTimeSlots: includeInshopTimeSlots,
@ -1230,6 +1240,11 @@ export default {
this.setDisplayWaitList();
}
}
this.isShowMobileFirstAppt && this.hideLoadingModal();
},
getIsRecalAcknowledgedForSchedulingFromStore() {
return store.getters.order.isRecalAcknowledgedForScheduling;
},
getScheduleApiResponse,
getServiceZipCtuCodeFromStore() {
@ -1346,7 +1361,28 @@ export default {
showLoadingModal() {
this.$refs.loadingModal.showModal();
},
hideLoadingModal() {
this.$refs.loadingModal.hideModal();
},
async closeRecalAcknowledgementPopup() {
this.showRecalAcknowledgementPopup = false;
},
async forwardButtonAction() {
const showRecalAcknowledgementModal = this.showRecalAcknowledgementModal();
if (
showRecalAcknowledgementModal &&
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
) {
this.showRecalAcknowledgementPopup = true;
return;
}
this.dispatchStoreAction(
storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
this.isRecalAcknowledgedForScheduling,
false
);
this.navigatingForward = true;
var ctu = this.zipCodeCtu;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
@ -1648,6 +1684,8 @@ export default {
this.updateSelectedProvider();
},
updateSelectedProvider(newShopProvider) {
if (!this.appointmentTypeStrings) return;
if (newShopProvider) {
this.selectedProvider = newShopProvider;
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
@ -1668,7 +1706,27 @@ export default {
this.selectedProvider = this.shopProviderData.shopProviders[0];
}
},
handleZipCodeChange(newZipCode) {
async handleZipCodeChange(newZipCode) {
// if we were showing the recal ack modal and now are changing the zip, we need to navigate
// to service-zip so we can get call parts again to add the recal part if needed
if (this.shouldShowRecalAckModal()) {
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
zipCode: newZipCode.zipCode,
state: newZipCode.state,
zipCodeCtu: newZipCode.zipCodeCtu,
},
false
);
this.$router.navigateWithSaving(
this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
this.pageName
);
return;
}
this.resetMobileLocation();
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
@ -1678,7 +1736,9 @@ export default {
this.zipCode = newZipCode.zipCode;
this.zipCodeCtu = newZipCode.zipCodeCtu;
this.selectedDate = null;
this.initializeDatePicker();
this.initializeDatePicker().then(() => {
this.showMobileFirstModal();
});
});
},
updateSelectedProviderAndZipCode(newZipCode, newProvider) {
@ -1689,7 +1749,6 @@ export default {
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
this.selectedDate = null;
this.selectedProvider = newProvider;
this.initializeDatePicker();
},
handleAppointmentTypeChange(newAppointmentType) {
this.updateFooterButtonText();
@ -1716,7 +1775,6 @@ export default {
}
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.updateSelectedProvider();
this.showMobileFirstModal();
} else if (newAppointmentType) {
if (this.appointmentType != AppointmentTypeStrings.MOBILE) {
// Clear last shop selected if appointment type was changed in any manner other than from Mobile
@ -1731,7 +1789,11 @@ export default {
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
}
// make sure a selectedProvider exists
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
if (this.lastSelectedInshopOrDropoffProvider) {
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
} else {
this.updateSelectedProvider();
}
} else {
this.appointmentType = null;
}
@ -1741,12 +1803,34 @@ export default {
setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate();
},
showRecalAcknowledgementModal() {
if (this.shouldShowRecalAckModal()) {
this.$refs.navbar.removeLoader();
this.$refs.recalAckModal.openModal();
return true;
}
this.closeRecalAcknowledgementPopup();
return false;
},
shouldShowRecalAckModal() {
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
const order = this.$store.getters.order;
const containsRecalPart = containsRecalParts(order.lineItems);
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(order.lineItems);
if (
!containsRecalPart &&
hasPartWithRecalRequirement &&
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
) {
return true;
}
return false;
},
async showMobileFirstModal() {
if (this.appointmentType == AppointmentTypeStrings.MOBILE) {
const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
);
if (!this.selectedRouteCodeData?.routeCode) {
const pmMobileDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_PM_MOBILE_DAYS
);
@ -1801,32 +1885,40 @@ export default {
);
}
if (isShowMobileFirstAppt) {
if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) {
if (this.isShowMobileFirstAppt) {
if (pmMobileDays == 0) {
// Selects the first available time slot
preSelectedMobileAppointment =
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
? firstMobileAMAppt
: firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate <= pmMobileDays
) {
// Selects the first available PM time slot
preSelectedMobileAppointment = firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
preSelectedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt
: null;
!preSelectedMobileAppointment &&
(preSelectedMobileAppointment = firstMobilePMAppt);
: firstMobilePMAppt;
}
this.$refs.mobileFirstModal.setSelectedAppointment(
preSelectedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
if (preSelectedMobileAppointment) {
this.$refs.mobileFirstModal.setSelectedAppointment(
preSelectedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
}
}
}
}
},
async getFirstAvailableMobileApptByTOD(timeOfDay) {
if (!this.selectableDatesMobile.days) {
await this.initializeDatePicker();
}
if (!this.selectableDatesMobile?.days?.length) {
return null;
} else {
@ -1853,9 +1945,17 @@ export default {
}
},
updateTimeSlotandNavigateForward(timeSlotObj) {
this.selectedMobileFirstAppointment = true;
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.selectedDate = timeSlotObj.date;
this.updateSelectedProvider();
this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction();
},
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
},
},
watch: {
appointmentTypeFromAppointmentTypeQuestion: {
@ -1872,7 +1972,7 @@ export default {
const hasValueChanged = newValue !== oldValue;
const isDateDifferent = (newValue || oldValue) !== selectedDate;
if (hasValueChanged && isDateDifferent) {
if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) {
this.selectedTimeSlotInfo = {
timeSlot: {
date: null,
@ -1901,7 +2001,7 @@ export default {
textBlock,
timeSlotQuestion,
mobileFirstModal,
recalAckModal,
alert,
serviceZipModalQuestion,
appointmentTypeQuestion,

View file

@ -109,10 +109,10 @@ export default {
border-radius: $border-radius-pill;
a {
font-family: UrbanistSemibold;
display: inline-block;
text-decoration: none;
color: $white;
font-weight: $font-weight-600;
line-height: 1.5rem;
padding: 0.75rem 3rem;
}

View file

@ -548,9 +548,6 @@ export default {
}
},
},
unmounted() {
if (this.isModalOpened) this.closeModal();
},
watch: {
waitListRequested(newVal) {
if (newVal) {

View file

@ -198,6 +198,8 @@ export default {
false
);
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
// Navigate to the next page
this.navigateForward(matchedParts);
},

View file

@ -232,6 +232,21 @@ export default {
(part) => `${part.glassLocation}-${part.glassName}`
);
const format = (part) => `${part?.partNumber}-${part?.partType ? part.partType : ""}`;
const mapParts = (arr) => (arr ?? []).map(format);
const glassParts = mapParts(order?.lineItems?.glassParts);
const supportingItems = mapParts(order?.lineItems?.supportingItems);
const vaps = mapParts(order?.lineItems?.vaps);
const childParts = [].concat(
...(order?.lineItems?.glassParts ?? []).map((gp) => mapParts(gp?.childParts))
);
const allParts = [...glassParts, ...supportingItems, ...vaps, ...childParts];
if (applicationUser?.pageData?.quote?.servicePackageSelected) {
allParts.push(applicationUser.pageData.quote.servicePackageSelected);
}
var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`;
var sessionData = {};
@ -251,7 +266,7 @@ export default {
? "Insurance"
: "Cash";
sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace";
sessionData.productType = glassProducts;
sessionData.productType = allParts;
sessionData.eon = order?.eon;
sessionData.referralNumber = order?.referralNumber;
sessionData.referralSequenceNumber = order?.referralSequenceNumber;
@ -958,6 +973,8 @@ function getPageNameFromRouter() {
) {
return router.currentRoute.value.name;
}
return window.location.href.replace(/\/$/, "").split("/").pop();
}
function getValueToLog(value, valueToLogType) {

View file

@ -129,9 +129,9 @@ export default {
});
return lineItemsArray;
},
filterOutServicePackageDiscountPart(lineItems) {
filterOutQuotePageDiscountPart(lineItems) {
const filteredLineItems = lineItems?.filter((item) => {
return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT);
return !item?.partType?.includes(partTypeStrings.QUOTE_PAGE_DISCOUNT);
});
return filteredLineItems;
},

View file

@ -57,10 +57,12 @@ const navigationScenarios = {
// Schedule
CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION",
ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK",
// Payment
CLICKED_INSURANCE: "CLICKED_INSURANCE",
CLICKED_PAY_NOW: "CLICKED_PAY_NOW",
CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN",
CLICKED_PAY_LATER: "CLICKED_PAY_LATER",
PIA_ERROR: "PIA_ERROR",
PIA_CC_ERROR: "PIA_CC_ERROR",

View file

@ -89,6 +89,15 @@ export const routeData = {
name: "payment-pia-return",
path: "/payment-pia-return",
},
PAYMENT_ADYEN: {
name: "payment-adyen",
path: "/payment-adyen",
},
PAYMENT_ADYEN_RETURN: {
name: "adyen-return",
path: "/virtual/payment/return",
virtual: true,
},
CONFIRMATION: {
name: "confirmation",
path: "/confirmation",

View file

@ -486,6 +486,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
destinationPageData: routeData.MOBILE_DETAILS,
},
{
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
destinationPageData: routeData.SERVICE_ZIP,
},
],
},
{
@ -537,6 +541,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_PAY_NOW,
destinationPageData: routeData.PAYMENT,
},
{
scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN,
destinationPageData: routeData.PAYMENT_ADYEN,
},
{
scenario: navigationScenarios.CLICKED_INSURANCE,
destinationPageData: routeData.INSURANCE_COMPANY,
@ -563,6 +571,26 @@ const routingTable = function () {
},
],
},
{
pageName: routeData.PAYMENT_ADYEN.name,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationPageData: routeData.PAYMENT_METHOD,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationPageData: routeData.CONFIRMATION,
},
{
scenario: navigationScenarios.PIA_ERROR,
destinationPageData: routeData.PAYMENT_METHOD,
params: {
piaError: "true",
},
},
],
},
{
pageName: routeData.PAYMENT_PIA_RETURN.name,
maps: [

View file

@ -1,6 +1,10 @@
import analyticsMixin from "@/mixins/analytics-mixin";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
export async function afterEach(to, from) {
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
// digital consumer logging
analyticsMixin.methods.logDigitalConsumer();

View file

@ -3,16 +3,39 @@ import { queryStrings } from "@/constants/query-strings";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function consumeReferralQuerystrings() {
const referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
let referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
let parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
let correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
let referralDate = null;
let savedSessionId = null;
// 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.
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
if (didHeritageFunnelUpdateLast && !referralNumber) {
referralNumber = getFunnelCookie().ReferralNumber;
parentAccount = getFunnelCookie().ReferralParentAccountNumber;
correlationId = getFunnelCookie().ReferralCorrelationId;
referralDate = getFunnelCookie().ReferralDate;
savedSessionId = getFunnelCookie().SavedSessionId;
}
if (referralNumber) {
store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount);
store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId);
// log(" --update cookie");
if (referralDate) {
store.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
}
if (savedSessionId) {
store.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
}
updateOrCreateFunnelCookie();
}
}

View file

@ -2,11 +2,13 @@ import { queryStrings } from "@/constants/query-strings";
import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info";
import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper";
import { routeData } from "@/router/constants/routes";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function handleHeritageReturn(to, from) {
const fromHeritageFlag = to.query[queryStrings.FROM_HERITAGE];
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
if (fromHeritageFlag) {
if (fromHeritageFlag || didHeritageFunnelUpdateLast) {
// Get new referral info from querystring in case passed there.
await consumeReferralQuerystrings();

View file

@ -0,0 +1,164 @@
import { routeData } from "@/router/constants/routes";
import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper";
import { mapAdyenToFmgPaymentMethod, generateCcToken } from "@/helpers/adyen-helper";
import baseMixin from "@/mixins/base-mixin.js";
import { getAmountDue } from "@/helpers/pricing-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
export async function adyenReturnBeforeEnter(to, from) {
console.log(`In adyen return.`);
console.log(JSON.parse(JSON.stringify(to)));
const sessionId = to?.query?.sessionId;
const redirectResult = to?.query?.redirectResult;
if (sessionId && redirectResult) {
let result = null;
try {
result = await finalizeAdyenPayment(sessionId, redirectResult);
} catch (unexpectedResult) {
// If user cancelled payment, handle without error message
if (unexpectedResult.code === "Cancelled" || unexpectedResult.code === "CANCEL") {
return {
name: routeData.PAYMENT_ADYEN.name,
replace: true,
};
}
// Otherwise, failure scenario.
// Payment fails, so return user to payment-adyen screen to try again or pay later.
return {
name: routeData.PAYMENT_ADYEN.name,
query: {
piaFailure: true,
},
replace: true,
};
}
console.log(`Out of promise`);
console.log(result);
const sessionInfo = await getSessionInfo(sessionId, result.sessionResult);
console.log(`Got session info:`);
console.log(sessionInfo);
// TODO once afterpay info is returned, create cc token and submit order.
const paymentMethod = mapAdyenToFmgPaymentMethod(sessionInfo?.paymentMethod);
const amountDue = getAmountDue(store.getters.order.lineItems);
const ccToken = generateCcToken(sessionInfo);
ccToken.authCode = "831001";
ccToken.cardType = "VS";
ccToken.lastFour = "1111";
ccToken.expMonth = "03";
ccToken.expYear = "2030";
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethod,
false
);
await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
await baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
amountDue,
false
);
await baseMixin.methods.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
try {
await submitWorkOrder({
pageNameToLog: "adyen-return",
submitAfterSave: true,
});
} catch (error) {
console.log(error);
return {
name: routeData.PAYMENT_METHOD.name,
query: {
piaError: "true",
},
replace: true,
};
}
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
return {
name: routeData.CONFIRMATION.name,
replace: true,
};
}
return {
name: routeData.ERROR.name,
replace: true,
};
}
function finalizeAdyenPayment(sessionId, redirectResult) {
return new Promise((resolve, reject) => {
createAdyenCheckout({
session: {
id: sessionId,
},
handlers: {
onPaymentCompleted: (result, component) => {
console.log(`Payment successful`);
console.log(result);
resolve(result);
},
onPaymentFailed: (result, component) => {
console.log(`Payment failed`);
console.log(result);
if (result?.resultCode !== "Cancelled") {
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`;
global.$logger.logError(stringToLog);
}
reject({
code: result?.resultCode,
message: "Payment failed from Adyen",
});
},
onError: (error, component) => {
console.log(`Error occured`);
console.log(error);
if (error?.name !== "CANCEL") {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
global.$logger.logError(stringToLog);
}
reject({
code: error?.name,
message: "Error from Adyen",
});
},
},
options: {},
}).then(
(checkout) => {
checkout.submitDetails({
details: {
redirectResult: redirectResult,
},
});
},
(error) => {
reject(error);
}
);
});
}

View file

@ -6,6 +6,7 @@ import router from "@/router";
import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
import store from "@/store";
import { handleHardError } from "@/router/methods/error";
import analyticsMixin from "@/mixins/analytics-mixin";
export async function errorBeforeEnter(to, from) {
// Check if `hasAlreadyTriggeredErrror` is available.
@ -18,6 +19,7 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name,
};
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload);
return;
}
@ -31,6 +33,7 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name,
};
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload);
return;
} else {

View file

@ -13,6 +13,7 @@ import { restartBeforeEnter } from "@/router/methods/route-logic/restart";
import { paymentMethodBeforeEnter } from "@/router/methods/route-logic/payment-method";
import { paymentBeforeEnter } from "@/router/methods/route-logic/payment";
import { serviceLocationBeforeEnter } from "@/router/methods/route-logic/service-location";
import { adyenReturnBeforeEnter } from "@/router/methods/route-logic/adyen-return";
export const routes = [
// Non-virtual pages.
@ -36,6 +37,7 @@ export const routes = [
createRoute(routeData.PAYMENT_METHOD, paymentMethodBeforeEnter),
createRoute(routeData.PAYMENT, paymentBeforeEnter),
createRoute(routeData.PAYMENT_PIA_RETURN),
createRoute(routeData.PAYMENT_ADYEN),
createRoute(routeData.CONFIRMATION),
createRoute(routeData.RETURN_USER),
createRoute(routeData.MOBILE_DETAILS),
@ -47,4 +49,5 @@ export const routes = [
createVirtualRoute(routeData.AUTO_ROUTE, autoRouteBeforeEnter),
createVirtualRoute(routeData.ERROR, errorBeforeEnter),
createVirtualRoute(routeData.RESTART, restartBeforeEnter),
createVirtualRoute(routeData.PAYMENT_ADYEN_RETURN, adyenReturnBeforeEnter),
];

View file

@ -122,6 +122,7 @@ const getDefaultState = () => {
capabilityQuestionAnswers: null,
dateOfLoss: null,
damageCause: null,
installOemGlass: null,
},
lineItems: {
glassParts: null,
@ -189,6 +190,7 @@ const getDefaultState = () => {
lockToken: null,
settledTenderAmount: 0,
isRecalAckOptIn: false,
isRecalAcknowledgedForScheduling: "",
isMSRFeeApplicable: false,
},
applicationUser: {
@ -342,6 +344,12 @@ export const mutations = {
updateIsRecalAckOptIn(state, isRecalAckOptIn) {
state.order.isRecalAckOptIn = isRecalAckOptIn;
},
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
},
updateIsOemGlassSelected(state, isOemGlassSelected) {
state.order.damage.installOemGlass = isOemGlassSelected;
},
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable;
},
@ -712,6 +720,8 @@ export const mutations = {
state.order.damage.capabilityQuestionAnswers =
sessionInformation.order.damage.capabilityQuestionAnswers;
state.order.damage.installOemGlass = sessionInformation.order.damage.installOemGlass;
state.order.lineItems.glassParts = sessionInformation.order.lineItems.glassParts;
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? [];
@ -1765,7 +1775,8 @@ export const actions = {
const glassArray = damage.glassToReplace;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
const serviceType = order.serviceLocation?.appointmentType;
const serviceType = damage.isRepair ? "Repair" : "Install";
const parentAccountNumber = context.getters.payment.parentAccountNumber;
const referralSeqNumber = order.referralSequenceNumber;
// create a new array to avoid mutating state
@ -1781,6 +1792,7 @@ export const actions = {
vin: vin,
serviceType: serviceType,
referralSeqNumber: referralSeqNumber,
parentAccountNumber: parentAccountNumber,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
@ -1836,6 +1848,55 @@ export const actions = {
return response;
},
// Parts API Actions
async getOemAfterMarketParts(context, { pageNameToLog }) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const carId = vehicle.carId;
const glassArray = damage.glassToReplace;
const resultsArray = damage.partQuestionAnswers;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
const make = vehicle.make;
const serviceType = order.serviceLocation?.appointmentType;
const referralSeqNumber = order.referralSequenceNumber;
const parentAccountNumber = applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
const oemEndorsementFlag = order.damage.installOemGlass || false;
// create a new array to avoid mutating state
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
const resultsArrayForPayload = convertResultsForApi(resultsArray);
const response = await globalMethods.callHttpClient({
method: endpoints.GetOemAfterMarketParts.method,
endpoint: endpoints.GetOemAfterMarketParts.url,
payload: {
carId: carId,
glassPieces: glassArrayForPayload,
answerResults: resultsArrayForPayload,
zip: zipCode,
vin: vin,
serviceType: serviceType,
referralSeqNumber: referralSeqNumber,
make: make,
parentAccountNumber: parentAccountNumber.toString(),
oemEndorsementFlag: oemEndorsementFlag,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
});
// Flatten location and name properties
if (response?.data?.glassPieceParts) {
response.data.glassPieceParts = convertGlassPieceNamingFromApi(
response.data.glassPieceParts
);
}
return response;
},
async getWipers(context, { payload, pageNameToLog }) {
const carId = payload.carId;
const serviceZipCode = payload.serviceZipCode;
@ -1936,26 +1997,6 @@ export const actions = {
return response;
});
},
getServicePackageDiscountPart(context, { pageNameToLog }) {
const damage = context.getters.damage;
const damageType = damage.isRepair ? "Repair" : "Replace";
const glassPieces = convertGlassPieceNamingForApi(damage.glassToReplace);
return globalMethods
.callHttpClient({
method: endpoints.GetServicePackageDiscountPart.method,
endpoint: endpoints.GetServicePackageDiscountPart.url,
payload: {
glassPieces: glassPieces,
damageType: damageType,
},
logApiCall: true,
pageNameToLog: pageNameToLog,
})
.then((response) => {
return response;
});
},
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
@ -2363,9 +2404,17 @@ export const actions = {
//affiliate cookie save only when submitAfterSave
const affiliateCookies = submitAfterSave ? applicationUser.affiliateCookies : null;
let endPoint = endpoints.SaveSession.url;
if (createUnscheduledStatusWorkOrderForPIA) {
endPoint = endpoints.SubmitOrderForPia.url;
}
if (submitAfterSave) {
endPoint = endpoints.SubmitOrder.url;
}
return globalMethods.callHttpClient({
method: endpoints.SaveSession.method,
endpoint: endpoints.SaveSession.url,
endpoint: endPoint,
payload: {
submitAfterSave: submitAfterSave,
userAgent: navigator.userAgent,
@ -2413,6 +2462,7 @@ export const actions = {
partQuestionAnswers: order.damage.partQuestionAnswers,
moldingQuestionAnswers: order.damage.moldingQuestionAnswers,
capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers,
installOemGlass: order.damage.installOemGlass,
},
lineItems: {
glassParts: lineItems.glassParts,
@ -2919,6 +2969,17 @@ export const actions = {
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn);
},
saveIsRecalAcknowledgedForScheduling(context, isRecalAcknowledgedForScheduling) {
context.commit(
storeMutations.UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
isRecalAcknowledgedForScheduling
);
},
saveIsOemGlassSelected(context, isOemGlassSelected) {
context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected);
},
saveIsMSRFeeApplicable(context, isMSRFeeApplicable) {
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
},
@ -3085,40 +3146,24 @@ export const actions = {
promoCode: lineItem.promoCode ?? null,
}));
const pricedLineItemsFormattedForRequest =
buildQueryStringParameterFromArrayOfComplexObjects(
lineItemsWithOnlyPriceInfo,
"lineItems"
);
let queryString = "";
if (appointmentType == "Mobile") {
queryString =
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
`&BillToAccountNumber=${billToAccountNumber}` +
`&ProviderNumber=${providerNumber}` +
`&AppointmentType=${appointmentType}` +
`&ServiceLocation.City=${serviceLocationCity}` +
`&ServiceLocation.State=${serviceLocationState}` +
`&ServiceLocation.ZipCode=${serviceLocationZipCode}` +
`&${pricedLineItemsFormattedForRequest}`;
} else {
queryString =
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
`&BillToAccountNumber=${billToAccountNumber}` +
`&ProviderNumber=${providerNumber}` +
`&AppointmentType=${appointmentType}` +
`&${pricedLineItemsFormattedForRequest}`;
}
const lineItemServerData = context.getters.order.lineItems.serverData;
if (lineItemServerData) {
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
}
const order = context.getters.order;
const response = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method,
endpoint: `${endpoints.TaxOrderItems.url}?${queryString}`,
endpoint: endpoints.TaxOrderItems.url,
payload: {
parentAccountNumber: order.payment.parentAccountNumber,
billToAccountNumber: billToAccountNumber,
providerNumber: providerNumber,
appointmentType: appointmentType,
pricedLineItems: lineItemsWithOnlyPriceInfo,
serviceLocation: {
city: appointmentType == "Mobile" ? serviceLocationCity : null,
state: appointmentType == "Mobile" ? serviceLocationState : null,
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
},
serverData: order.lineItems.serverData ? order.lineItems.serverData : "",
},
logApiCall: true,
pageNameToLog: pageNameToLog,
});

View file

@ -27,6 +27,8 @@ $red-600: #ac160b;
$red-700: #840900;
$red-800: #5b0600;
$red-900: #330300;
$red-1000: #a80019;
$red-1100: #791710;
// Greens
$green-100: #e3f2ea;

View file

@ -63,13 +63,13 @@ export default {
<style lang="scss">
.btn {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
&.btn-primary {
position: relative;
background: $red;
border: none;
color: $white;
justify-content: center;
font-weight: 500;
@media (hover: hover) {
background: $red;
}
@ -92,7 +92,6 @@ export default {
background: $gray-200 !important;
background: $red !important;
color: $white !important;
font-weight: 400;
height: 48px;
border: none;
cursor: pointer;
@ -116,7 +115,6 @@ export default {
background: transparent;
border: 1px solid $blue;
color: $blue;
font-weight: 500;
transition: all 150ms linear;
height: 3rem;
&:hover {
@ -137,7 +135,6 @@ export default {
&:disabled {
background: transparent;
color: $red !important;
font-weight: 400;
height: 48px;
border: 1px solid $red;
cursor: pointer;
@ -157,7 +154,6 @@ export default {
background: $green-100;
border: 1px solid $green-400;
color: $black;
font-weight: 500;
transition: all 150ms linear;
height: 3rem;
&:hover {
@ -177,7 +173,6 @@ export default {
&:disabled {
background: transparent;
color: $gray-550 !important;
font-weight: 400;
height: 48px;
border: 1px solid $gray-550;
cursor: pointer;

View file

@ -15,7 +15,7 @@
<!-- prettier-ignore -->
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{ text }}<slot name="after-text"></slot></a>
<!-- prettier-ignore -->
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">{{ text }}<slot name="after-text"></slot></a>
<a v-else-if="linkType === 'text'" @click="handleClick" class="text-link" :href="href">{{ text }}<slot name="after-text"></slot></a>
</template>
<script>
@ -89,5 +89,8 @@ a {
&.new-window-link {
margin-bottom: 1.5rem;
}
&.text-link {
white-space: nowrap;
}
}
</style>