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_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30, ['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "", ['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
}; };
const keysOnly = Object.keys(environmentVariables); const keysOnly = Object.keys(environmentVariables);

41
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "Safelite", "name": "Safelite",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@adyen/adyen-web": "^6.27.0",
"@iframe-resizer/child": "^5.3.3", "@iframe-resizer/child": "^5.3.3",
"axios": "^0.30.2", "axios": "^0.30.2",
"bootstrap": "^5.3.3", "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": "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": { "node_modules/@ampproject/remapping": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -3173,6 +3186,12 @@
"node": ">=10.13.0" "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": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3328,6 +3347,12 @@
"@types/send": "*" "@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": { "node_modules/@types/graceful-fs": {
"version": "4.1.9", "version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -5789,6 +5814,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/clean-css": {
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@ -13681,6 +13712,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View file

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

View file

@ -3,7 +3,8 @@ import { PaymentMethod } from './localTypes/Enums'
export interface ITestData extends base { 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. // 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 paymentMethod: PaymentMethod,
isOptedInForTextMessages: boolean isOptedInForTextMessages: boolean,
totalAmount?: number totalAmount?: number,
isForcedOEM?: boolean
} }

View file

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

View file

@ -169,4 +169,20 @@ export class BasePage {
Soft.expect(actualProgressPercentage).toBe(progressPercentage); Soft.expect(actualProgressPercentage).toBe(progressPercentage);
console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${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); super(page);
this.page = 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.cusmodalPopup = this.page.locator('#Cusmodalpopup');
this.closePopupButton = this.page.getByRole('button', { name: '×' }); this.closePopupButton = this.page.getByRole('button', { name: '×' });
@ -47,7 +47,7 @@ export class HomePage extends BasePage {
//Zip Entry //Zip Entry
this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox'); 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' }); 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) { async letsGetStarted(zip: string, enterFunnelWithZip: boolean) {
if (enterFunnelWithZip) { if (enterFunnelWithZip) {
await this.letsGetStartedButton.evaluate((element, zip) => { await this.zipEntryLetsGetStartedButton.fill(zip);
/* await this.letsGetStartedButton.evaluate((element, zip) => {
const currentHref = element.getAttribute('href') || ''; const currentHref = element.getAttribute('href') || '';
element.setAttribute('href', `${currentHref}&zipCode=${zip}`); element.setAttribute('href', `${currentHref}?zipCode=${zip}`);
}, zip); }, zip);*/
} }
// Wait until modal pop up opens and close pop up // 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 // Map of expected messages by lookup type
const expectedMessages = { 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.", '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.", '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.", '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." '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 // Only proceed with validation if alertFlags is provided
if (alertFlags) { if (alertFlags) {
if (alertFlags.isUnserviceableZip) { if (alertFlags.isUnserviceableZip) {
await this.checkForUnserviceableZipAlertMessage(zip); await this.checkForUnserviceableZipAlertMessage(customerDetails!.address.postalCode!);
throw new TestSuccessAlert('Unserviceable ZIP validation successful.'); throw new TestSuccessAlert('Unserviceable ZIP validation successful.');
} else if (alertFlags.isInvalidZip) { } else if (alertFlags.isInvalidZip) {
await this.checkForInvalidZipAlertMessage(); await this.checkForInvalidZipAlertMessage();
throw new TestSuccessAlert('Invalid ZIP validation successful.'); throw new TestSuccessAlert('Invalid ZIP validation successful.');
} else if (alertFlags.isVinNotFound) { } else if (alertFlags.isVinNotFound) {
await this.checkForVinNotFoundAlertMessage(lookupType); await this.checkForVinNotFoundAlertMessage(vehicleDetails!.vehicleLookupType!);
throw new TestSuccessAlert('Vin not found validation successful.'); throw new TestSuccessAlert('Vin not found validation successful.');
} else { } else {

View file

@ -47,7 +47,7 @@ export class OrderConfirmationPage extends BasePage {
async validateOrderConfirmationPage(testData: Partial<ITestData>) { async validateOrderConfirmationPage(testData: Partial<ITestData>) {
// Destructure data we use // Destructure data we use
const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow, 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" }); await this.serviceText.waitFor({ state: "visible" });
Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase())); 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: ") @step("PaymentMethodPage >> Select Payment Method: ")
async handlePaymentMethodPage(testData: Partial<ITestData>) { async handlePaymentMethodPage(testData: Partial<ITestData>) {
const { servicePackage, isRecalVehicle, paymentDetails } = testData; const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData;
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage); await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
await this.validatePaymentDetailsPage(testData); await this.validatePaymentDetailsPage(testData);
await this.ValidateAfterPayBreakOutSection(); await this.ValidateAfterPayBreakOutSection();
if (isForcedOEM){
await this.validateOEMPart(isForcedOEM);
}
// Verify VAPS wipers on backend for standard and premium packages // Verify VAPS wipers on backend for standard and premium packages
if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) { if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) {
await this.verifyVAPS(); await this.verifyVAPS();

View file

@ -41,6 +41,8 @@ export class PaypalPage extends BasePage {
await this.paypalLoginButton.click(); await this.paypalLoginButton.click();
await this.completePurchaseButton.click(); await this.completePurchaseButton.click();
} else { } else {
await this.usernameTextBox.waitFor({ state: 'visible' });
await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true });
await this.usernameTextBox.fill(paymentDetails.username!); await this.usernameTextBox.fill(paymentDetails.username!);
await this.nextButton.click(); await this.nextButton.click();
await this.tryAnotherWayButton.click(); await this.tryAnotherWayButton.click();

View file

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

View file

@ -44,7 +44,7 @@ export class ServicePackagesPage extends BasePage {
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code'); this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' }); this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']'); 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'); 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: ") @step("ZipLookupPage >> Lookup by service ZIP: ")
async handleServiceZipPage(testData: Partial<ITestData>) { async handleServiceZipPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData; const { customerDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.ServiceZipPage); await this.validateProgressBar(ProgressBarPercentages.ServiceZipPage);
await this.enterZip(customerDetails!.address.postalCode!); 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: ") @step("VehicleLookupAddressPage >> Lookup by address: ")
async handleVehicleLookupAddressPage(testData: Partial<ITestData>) { async handleVehicleLookupAddressPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData; const { customerDetails, vehicleDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage); await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage);
await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!); 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: ") @step("VehicleLookupLicensePage >> Lookup by license plate: ")
async handleVehicleLookupLicensePage(testData: Partial<ITestData>) { async handleVehicleLookupLicensePage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData; const { customerDetails, vehicleDetails } = testData;
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage); await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage);
await this.enterPlateDetails(vehicleDetails!); await this.enterPlateDetails(vehicleDetails!);
await this.enterZip(customerDetails!.address.postalCode!); 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: ") @step("VinLookupPage >> Lookup by VIN: ")
async handleVehicleLookupVinPage(testData: Partial<ITestData>) { async handleVehicleLookupVinPage(testData: Partial<ITestData>) {
const { customerDetails, vehicleDetails, alertFlags } = testData; const { customerDetails, vehicleDetails } = testData;
let vin: string; let vin: string;
if (Array.isArray(vehicleDetails?.vin)) { if (Array.isArray(vehicleDetails?.vin)) {
vin = vehicleDetails.vin[0]; vin = vehicleDetails!.vin[0];
} else { } else {
vin = vehicleDetails?.vin || ''; vin = vehicleDetails?.vin || '';
} }
@ -36,6 +36,6 @@ export class VinLookupPage extends LookupPage {
await this.validateProgressBar(ProgressBarPercentages.VinLookupPage); await this.validateProgressBar(ProgressBarPercentages.VinLookupPage);
await this.enterVin(vin); await this.enterVin(vin);
await this.enterZip(customerDetails!.address.postalCode!); 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 new Promise(resolve => setTimeout(resolve, 1000));
await this.addressSuggestionList.click(); // Select the first suggestion await this.addressSuggestionList.click(); // Select the first suggestion
//await this.streetAddressTextBox.press('Tab'); // Move focus to ZIP code field //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 insuranceGeicoTests from "./InsuranceGeico";
import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState"; import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState";
import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive"; import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive";
import insuranceOEMAllstateTests from "./InsuranceOEMAllState";
import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay"; import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay";
import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal"; import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal";
import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff"; import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff";
@ -82,6 +83,7 @@ const allStandardTests = [
{name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests}, {name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests},
{name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests}, {name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests},
// {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests}, // {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests},
{name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests},
{name: "InsuranceUnverified", tests: insuranceUnverifiedTests}, {name: "InsuranceUnverified", tests: insuranceUnverifiedTests},
// {name: "InsuranceGeico", tests: insuranceGeicoTests}, // {name: "InsuranceGeico", tests: insuranceGeicoTests},
// {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests} // {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests}
@ -323,7 +325,7 @@ async function runWorkflow(page: Page, testCase: TestCase) {
} }
export async function handleInsuranceFlow(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 // Check if the insurance policy has endorsements
const hasEndorsements = endorsements && endorsements.length > 0; const hasEndorsements = endorsements && endorsements.length > 0;
@ -332,6 +334,13 @@ export async function handleInsuranceFlow(testCase: TestCase) {
let insuranceCompanyPage = testCase.pages.insuranceCompanyPage; let insuranceCompanyPage = testCase.pages.insuranceCompanyPage;
await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData); 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 // Handle ccPolicyInfoPage
let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage; let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage;
await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData); await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData);

View file

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

View file

@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
appointmentDetails: { appointmentDetails: {
serviceLocation: ServiceLocation.DropOff, serviceLocation: ServiceLocation.DropOff,
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate, appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
shopAddress: "6826 sawmill rd, Columbus, OH 43235" shopAddress: "1343 cameron ave, lewis center, oh 43035"
}, },
// Override vehicle details // 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: { customerDetails: {
address: { address: {
street: '4076 Spectacle Drive', street: '4219 Turpin Ln',
city: 'Columbus', city: 'Columbus',
state: 'OH', state: 'OH',
postalCode: '59261', postalCode: '43230',
country: 'United States' country: 'United States'
} }
}, },

View file

@ -114,7 +114,7 @@ body {
debug: true, debug: true,
}, },
{ {
name: 'Test', name: 'SysTest',
hostName: 'www-test2.safelite.com', hostName: 'www-test2.safelite.com',
apiHostname: 'digitalapi.test.safelite.io', apiHostname: 'digitalapi.test.safelite.io',
debug: true, debug: true,
@ -142,83 +142,178 @@ body {
return match; 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 () => { window.onload = async () => {
// =============== Check for session info: // =============== Check for session info:
const existingVuexDataJSON = window.localStorage.getItem('vuex'); const existingVuexDataJSON = window.localStorage.getItem('vuex');
const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null; const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null;
const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo'); const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo');
const existingBailoutInfo = existingBailoutInfoJSON ? JSON.parse(existingBailoutInfoJSON) : null; 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(`================ VUEX DATA`);
console.log(existingVuexData); console.log(existingVuexData);
console.log(`================ PRIOR BAILOUT DATA`); console.log(`================ PRIOR BAILOUT DATA`);
console.log(existingBailoutInfo); console.log(existingBailoutInfo);
console.log(`================ PRIOR HEADER DATA`);
console.log(existingHeaderInfo);
let bailoutInfo = null; let bailoutInfo = null;
let headerInfo = null;
if(existingVuexData) { if(existingVuexData) {
try { const results = extractDataFromVuex(existingVuexData);
// =============== Collect diagnostic data: bailoutInfo = results.bailoutInfo;
bailoutInfo = []; headerInfo = results.headerInfo;
// App & Site Name clearApplicationData();
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
// Not included (yet) from heritage: if(bailoutInfo) {
// - Site ID window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
// - 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');
} }
} else if(existingBailoutInfo) {
// Proceed with prior data. if(headerInfo) {
bailoutInfo = existingBailoutInfo; window.sessionStorage.setItem('headerInfo', JSON.stringify(headerInfo));
}
} else {
bailoutInfo = existingBailoutInfo ?? [];
headerInfo = existingHeaderInfo ?? [];
} }
const environmentInfo = getCurrentEnvironmentData(); const environmentInfo = getCurrentEnvironmentData();
@ -275,11 +370,11 @@ body {
); );
const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`; const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`;
const headers = generateRequestHeader(headerInfo);
const request = new Request(endpointUrl, { const request = new Request(endpointUrl, {
method: 'POST', method: 'POST',
headers: { headers: headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body: JSON.stringify({
entry: entryString, entry: entryString,
}), }),
@ -292,6 +387,38 @@ body {
console.error(`=== ERROR SENDING LOGGING`); console.error(`=== ERROR SENDING LOGGING`);
console.error(e); 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> </script>

View file

@ -509,7 +509,7 @@
'AccountService': { 'AccountService': {
baseUrl: apiUrl + '/account/api/v1', baseUrl: apiUrl + '/account/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
{ path: '/account/insurance-companies', method: 'GET' }, { path: '/account/insurance-companies', method: 'GET' },
{ path: '/account/account-details/373341', 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' } { path: '/account/bill-to-account-number?ParentAccountNumber=373341&ProviderNumber=01820&IsItac=false&TypeOfClaim=Personal&LineOfBusiness=Commercial', method: 'GET' }
@ -518,7 +518,7 @@
'LocationService': { 'LocationService': {
baseUrl: apiUrl + '/location/api/v1', baseUrl: apiUrl + '/location/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
{ path: '/location/zip/43015/Replace', method: 'GET' }, { path: '/location/zip/43015/Replace', method: 'GET' },
{ path: '/location/alert-reasons/03357', method: 'GET' }, { path: '/location/alert-reasons/03357', method: 'GET' },
{ path: '/location/providers/43015/Replace/100/167132/true/CR00046193/false', method: 'GET' }, { path: '/location/providers/43015/Replace/100/167132/true/CR00046193/false', method: 'GET' },
@ -528,7 +528,7 @@
'VehicleService': { 'VehicleService': {
baseUrl: apiUrl + '/vehicle/api/v1', baseUrl: apiUrl + '/vehicle/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
{ path: '/vehicle/years', method: 'GET' }, { path: '/vehicle/years', method: 'GET' },
{ path: '/vehicle/makes/1997', method: 'GET' }, { path: '/vehicle/makes/1997', method: 'GET' },
{ path: '/vehicle/Models/1997/Toyota', method: 'GET' }, { path: '/vehicle/Models/1997/Toyota', method: 'GET' },
@ -539,13 +539,12 @@
'PartService': { 'PartService': {
baseUrl: apiUrl + '/parts/api/v1', baseUrl: apiUrl + '/parts/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
{ path: '/parts/damage-options/CR00046193', method: 'GET' }, { path: '/parts/damage-options/CR00046193', method: 'GET' },
{ path: '/parts/rain-repel', 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/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/wipers/CR00046193/43015', method: 'GET' },
{ path: '/parts/mobile-premium-fee/Cash/Replace', 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' }, { 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': { 'PriceService': {
baseUrl: apiUrl + '/price/api/v1', baseUrl: apiUrl + '/price/api/v1',
endpoints: [ 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/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-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"} }, { 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': { 'ScheduleService': {
baseUrl: apiUrl + '/schedule/api/v1', baseUrl: apiUrl + '/schedule/api/v1',
endpoints: [ 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/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"} } { 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': { 'AnalyticsService': {
baseUrl: apiUrl + '/analytics/api/v1', baseUrl: apiUrl + '/analytics/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
] ]
}, },
'ExperimentService': { 'ExperimentService': {
baseUrl: apiUrl + '/experiments/api/v1', baseUrl: apiUrl + '/experiments/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
] ]
}, },
'OrderService': { 'OrderService': {
baseUrl: apiUrl + '/order/api/v1', baseUrl: apiUrl + '/order/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
] ]
}, },
'ContentService': { 'ContentService': {
baseUrl: apiUrl + '/content/api/v1', baseUrl: apiUrl + '/content/api/v1',
endpoints: [ endpoints: [
//{ path: '/healthcheck', method: 'GET' }, { path: '/healthcheck', method: 'GET' },
{ path: '/content/fmg/vehicle', method: 'GET' }, { path: '/content/fmg/vehicle', method: 'GET' },
] ]
} }
@ -674,7 +673,7 @@
} }
function updateAtAGlanceText() { 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; const totalCount = Object.values(healthData).length;
let text = 'Healthy'; let text = 'Healthy';
let className = 'stat-pill stat-pill-healthy'; let className = 'stat-pill stat-pill-healthy';
@ -763,14 +762,15 @@
// Determine overall service status // Determine overall service status
const endpoints = healthData[serviceName].endpoints; const endpoints = healthData[serviceName].endpoints;
const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length; const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length;
const unhealthyCount = endpoints.filter(ep => isUnhealthy(ep)).length;
const totalCount = endpoints.length; const totalCount = endpoints.length;
if (healthyCount === totalCount) { if (healthyCount === totalCount) {
healthData[serviceName].status = 'healthy'; healthData[serviceName].status = 'healthy';
} else if (healthyCount > 0) { } else if (unhealthyCount > 0) {
healthData[serviceName].status = 'degraded';
} else {
healthData[serviceName].status = 'unhealthy'; healthData[serviceName].status = 'unhealthy';
} else {
healthData[serviceName].status = 'degraded';
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -76,10 +76,6 @@ const endpoints = {
url: "/parts/api/v1/parts/pricing-by-day-part-number", url: "/parts/api/v1/parts/pricing-by-day-part-number",
method: "GET", method: "GET",
}, },
GetServicePackageDiscountPart: {
url: "/parts/api/v1/parts/service-package-discount",
method: "POST",
},
GetServiceabilityDetails: { GetServiceabilityDetails: {
url: "/location/api/v1/location/serviceability-details", url: "/location/api/v1/location/serviceability-details",
method: "GET", method: "GET",
@ -124,6 +120,14 @@ const endpoints = {
url: "/order/api/v1/order/save-session/fmg", url: "/order/api/v1/order/save-session/fmg",
method: "POST", 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: { LoadSession: {
url: "/order/api/v1/order/load-session", url: "/order/api/v1/order/load-session",
method: "POST", method: "POST",
@ -154,7 +158,7 @@ const endpoints = {
}, },
TaxOrderItems: { TaxOrderItems: {
url: "/price/api/v1/price/taxed-order-items", url: "/price/api/v1/price/taxed-order-items",
method: "GET", method: "POST",
}, },
LogExperimentExposureIfAssigned: { LogExperimentExposureIfAssigned: {
url: "/experiments/api/v1/experiments/log-exposure", url: "/experiments/api/v1/experiments/log-exposure",
@ -208,6 +212,18 @@ const endpoints = {
url: "/order/api/v1/order/add-donation-part", url: "/order/api/v1/order/add-donation-part",
method: "POST", 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 }; export { endpoints };

View file

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

View file

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

View file

@ -80,6 +80,10 @@ export const pageProgressMapper = {
percent: 98.5, percent: 98.5,
step: 4, step: 4,
}, },
"payment-adyen": {
percent: 98.5,
step: 4,
},
confirmation: { confirmation: {
percent: 100, percent: 100,
step: 4, 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 PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
const RECAL_ACK_YES = "Yes";
const RouteCodeFlags = { const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF", ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF", OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
@ -31,4 +33,5 @@ export {
PRICING_BY_DAY_PART_TYPE, PRICING_BY_DAY_PART_TYPE,
RouteCodeFlags, RouteCodeFlags,
cmsWidgetFieldMappings, cmsWidgetFieldMappings,
RECAL_ACK_YES,
}; };

View file

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

View file

@ -69,6 +69,8 @@ const storeMutations = {
LOCK_TOKEN: "updateLockToken", LOCK_TOKEN: "updateLockToken",
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", 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_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -54,10 +54,6 @@ export default {
mounted() { mounted() {
if (this.showSaveProgressPopup) this.modal.openModal(); 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 emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
props: { props: {
modelValue: Object, 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]; 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) { export function isRecalPartOrHasChildRecalPart(lineItem) {
if (lineItem.childParts && lineItem.childParts.length > 0) { if (lineItem.childParts && lineItem.childParts.length > 0) {
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts); return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);

View file

@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
return package_names[discountPackage] || null; 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) { function maxTier(tierA, tierB) {
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) { if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
return 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 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 flushSaveSessionQueue();
await this.removeQPDLineItemIfExists();
await this.dispatchStoreAction( await this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER, this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
this.parentAccountNumber.toString(), this.parentAccountNumber.toString(),
@ -193,6 +195,17 @@ export default {
handlePageShow() { handlePageShow() {
window.location.reload(); 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() { unmounted() {
window.removeEventListener("pageshow", this.handlePageShow); window.removeEventListener("pageshow", this.handlePageShow);

View file

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

View file

@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
//Assert //Assert
expect(spy).toHaveBeenNthCalledWith( expect(spy).toHaveBeenNthCalledWith(
1, 1,
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
false,
false
);
expect(spy).toHaveBeenNthCalledWith(
2,
"savePartQuestionAnswers", "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) // save to vuex store as order.damage.partQuestionAnswers (array)
// used in GET_PARTS call following this one // used in GET_PARTS call following this one
await this.dispatchStoreAction( 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 { debugLog } from "@/helpers/debug-log-helper";
import { ErrorMessage } from "vee-validate"; import { ErrorMessage } from "vee-validate";
import { Field } from "vee-validate"; import { Field } from "vee-validate";
import experimentMixin from "../../mixins/experiment-mixin";
defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED)); defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED)); defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
@ -609,10 +610,17 @@ export default {
} }
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_PAY_NOW, const shouldUseAdyen = experimentMixin.methods.hasSettingEqualTo(
this.pageName 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() { async evaluatePromosAndTaxItemsOnOrder() {
//Revalidate promos if there are any inactive, or active promos //Revalidate promos if there are any inactive, or active promos
@ -802,11 +810,51 @@ export default {
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE); const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
if (this.isInsurance) { if (this.isInsurance) {
return piaInsurance === "true" && this.currentDeductible > 0; return (
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
this.currentDeductible > 0
);
} else { } 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() { paymentMethod() {
if (!this.isPiaEnabled) { if (!this.isPiaEnabled) {
return paymentMethods.LATER; return paymentMethods.LATER;
@ -986,8 +1034,9 @@ export default {
} }
} }
} }
:deep(span.label) { :deep(span.label),
font-weight: 600; :deep(span) {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
} }
:deep(div.service-questions span) { :deep(div.service-questions span) {
color: $black; color: $black;

View file

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

View file

@ -1,10 +1,5 @@
<template> <template>
<div id="afterpay-banner" role="alert"> <div class="afterpay-modal-banner" role="alert">
<component
:is="'script'"
src="https://js.squarecdn.com/square-marketplace.js"
async></component>
<div> <div>
<span <span
class="callout" class="callout"
@ -22,22 +17,52 @@
<span v-else v-html="token"></span> <span v-else v-html="token"></span>
<span class="nbsp">&nbsp;</span> <span class="nbsp">&nbsp;</span>
</span> </span>
<a <textLink
id="afterpay-learnmore" linkType="text"
href="#" :text="bannerCta"
data-afterpay-modal="en_US" href="javascript:void(0)"
data-bind="click:afterpayLearnMore"> @click-event="openModal" />
{{ modalCopy }}
</a>
</div> </div>
</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> </template>
<script> <script>
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper"; import {
import baseMixin from "@/mixins/base-mixin.js"; doesCopyContainTextLink,
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper"; splitCopyOnCMSPlaceHolder,
import { getArrayOfAllLineItemsAndChildParts } from "@/store"; 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 INLINE_IMAGE_TOKEN = "custom:inlineImage";
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice"; const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
@ -48,20 +73,16 @@ function isInlineImageToken(token) {
function isAfterpayPriceToken(token) { function isAfterpayPriceToken(token) {
return token.includes(AFTERPAY_PRICE_TOKEN); return token.includes(AFTERPAY_PRICE_TOKEN);
} }
function getInlineAltText(token) { function getInlineAltText(token) {
const innerTokens = token.split(","); const innerTokens = token.split(",");
return innerTokens[1] ?? ""; return innerTokens[1] ?? "";
} }
export default { export default {
name: "afterpay-modal-banner", name: "afterpay-banner",
props: { props: {
cmsWidgetName: String, cmsWidgetName: String,
lineItems: Array, modalWidgetName: String,
isInsuranceSelected: Boolean,
afterpayExtendedPayOptionThreshold: Number,
}, },
data() { data() {
return {}; return {};
@ -70,6 +91,17 @@ export default {
isInlineImageToken, isInlineImageToken,
isAfterpayPriceToken, isAfterpayPriceToken,
getInlineAltText, getInlineAltText,
doesCopyContainTextLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
openModal() {
this.modal?.openModal();
},
closeModal() {
this.modal?.closeModal();
},
}, },
computed: { computed: {
imageUrl() { imageUrl() {
@ -84,39 +116,40 @@ export default {
afterpayCopyTokens() { afterpayCopyTokens() {
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText")); return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
}, },
modalCopy() { bannerCta() {
return this.getCmsContent(this.cmsWidgetName, "FooterText"); return this.getCmsContent(this.cmsWidgetName, "FooterText");
}, },
getTierOnePackagePrice() { modalHeaderText() {
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); return this.getCmsContent(this.modalWidgetName, "HeaderText");
if (this.lineItems.promos) { },
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT"); modalDisclaimerText() {
} return this.getCmsContent(this.modalWidgetName, "BodyText2");
},
let price = baseMixin.methods.getTierOnePackagePrice( modalName() {
baseMixin.methods.filterOutFees(allLineItems) return this.modalWidgetName;
); },
modal() {
if (this.lineItems.promos) { return this.$refs[this.modalName];
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems); },
const promos = getPromosThatMatchLineItemsOnOrder( afterpaySectionsContent() {
this.lineItems.promos, const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
allLineItems if (!sections) return [];
); sections.forEach((section) => {
promos.forEach((promo) => { const sectionContent = this.getCmsContent(section.Name, "BodyText");
price += baseMixin.methods.getTotalLineItemPrice(promo); section["content"] = sectionContent;
}); });
} return sections;
return price;
}, },
}, },
components: {}, components: {
textLink,
modal,
},
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
#afterpay-banner { .afterpay-modal-banner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: $blue-150; background-color: $blue-150;
@ -130,6 +163,7 @@ export default {
.callout { .callout {
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif; font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
white-space: nowrap;
} }
> div:first-of-type { > div:first-of-type {
@ -163,15 +197,120 @@ export default {
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
padding-left: 1rem; 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> </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) { filterOutFees(items) {
return null; return null;
}, },
filterOutServicePackageDiscountPart(items) { filterOutQuotePageDiscountPart(items) {
return null; return null;
}, },
isFormValid(form) { isFormValid(form) {
@ -118,6 +118,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) { if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
return false; return false;
} }
if (settingName === mockExperimentSettings.OFFER_OEM) {
return false;
}
return "test"; return "test";
}, },
}, },

View file

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

View file

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

View file

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

View file

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

View file

@ -50,7 +50,7 @@ export default {
data() { data() {
return { return {
isModalOpen: false, isModalOpen: false,
selectedAppontment: { selectedAppointment: {
estimatedServiceMinutes: { estimatedServiceMinutes: {
minimum: null, minimum: null,
maximum: null, maximum: null,
@ -58,6 +58,7 @@ export default {
timeSlot: null, timeSlot: null,
date: null, date: null,
}, },
confirmedAppointment: false,
}; };
}, },
methods: { methods: {
@ -67,21 +68,25 @@ export default {
this.modal.openModal(); this.modal.openModal();
}, },
onModalClosed() { onModalClosed() {
this.pushEvent();
this.$emit("close-mobile-first-modal"); this.$emit("close-mobile-first-modal");
this.confirmedAppointment = false;
}, },
closeModal() { closeModal() {
this.modal.closeModal(); this.modal.closeModal();
}, },
confirmAppointment() { confirmAppointment() {
let selectedTimeSlot = { let selectedTimeSlot = {
timeSlot: this.selectedAppontment.timeSlot, timeSlot: this.selectedAppointment.timeSlot,
routeCode: this.selectedAppontment.timeSlot.id, routeCode: this.selectedAppointment.timeSlot.id,
date: this.selectedAppointment.date,
}; };
this.confirmedAppointment = true;
this.$emit("confirm-appointment", selectedTimeSlot); this.$emit("confirm-appointment", selectedTimeSlot);
this.modal.closeModal(); this.modal.closeModal();
}, },
setSelectedAppointment(appointment) { setSelectedAppointment(appointment) {
this.selectedAppontment = appointment; this.selectedAppointment = appointment;
}, },
getIsModalOpen() { getIsModalOpen() {
return this.isModalOpen; return this.isModalOpen;
@ -89,6 +94,21 @@ export default {
setIsModalOpen(isOpen) { setIsModalOpen(isOpen) {
this.isModalOpen = 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: { computed: {
modalName() { modalName() {
@ -111,37 +131,37 @@ export default {
if (token.includes(INLINE_SERVICETYPE_TOKEN)) { if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
const serviceType = "<strong>" + this.getServiceType + "</strong>"; const serviceType = "<strong>" + this.getServiceType + "</strong>";
tokens.splice(tokens.indexOf(token), 1, serviceType); 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 = const day =
"<strong>" + "<strong>" +
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", { new Date(this.selectedAppointment.date).toLocaleDateString("en-US", {
weekday: "long", weekday: "long",
timeZone: "UTC", timeZone: "UTC",
}) + }) +
"</strong>"; "</strong>";
tokens.splice(tokens.indexOf(token), 1, day); tokens.splice(tokens.indexOf(token), 1, day);
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) { } else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppointment.date) {
const date = new Date(this.selectedAppontment.date).toLocaleDateString( const date = new Date(this.selectedAppointment.date).toLocaleDateString(
"en-US", "en-US",
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" } { timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
); );
tokens.splice(tokens.indexOf(token), 1, date); tokens.splice(tokens.indexOf(token), 1, date);
} else if ( } else if (
token.includes(INLINE_TIMESLOT_TOKEN) && token.includes(INLINE_TIMESLOT_TOKEN) &&
this.selectedAppontment.timeSlot this.selectedAppointment.timeSlot
) { ) {
const 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); tokens.splice(tokens.indexOf(token), 1, timeslot);
} else if ( } else if (
token.includes(INLINE_SERVICE_LENGTH_TOKEN) && token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
this.selectedAppontment.estimatedServiceMinutes this.selectedAppointment.estimatedServiceMinutes
) { ) {
const inshopDurationTime = getDisplayTextForDurationLength( const inshopDurationTime = getDisplayTextForDurationLength(
this.selectedAppontment.estimatedServiceMinutes.minimum, this.selectedAppointment.estimatedServiceMinutes.minimum,
this.selectedAppontment.estimatedServiceMinutes.maximum this.selectedAppointment.estimatedServiceMinutes.maximum
); );
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime); 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> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<loadingModal ref="loadingModal" /> <loadingModal notFullScreen ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<mobileFirstModal <mobileFirstModal
modalWidgetName="MobileFirstModalWidget" modalWidgetName="MobileFirstModalWidget"
ref="mobileFirstModal" ref="mobileFirstModal"
@confirm-appointment="updateTimeSlotandNavigateForward" /> @confirm-appointment="updateTimeSlotandNavigateForward" />
<recalAckModal
modalWidgetName="RecalAckModalWidget"
agreementWidgetName="RecalAgreementQuestionWidget"
groupName="recalAckGroup"
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
ref="recalAckModal" />
<div <div
class="container page-container-grouped-styles" class="container page-container-grouped-styles"
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']"> :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 timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.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 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 shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup"; import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
@ -216,6 +223,7 @@ import {
RouteCodeFlags, RouteCodeFlags,
PREMIUM_FEE_PART_TYPE, PREMIUM_FEE_PART_TYPE,
PRICING_BY_DAY_PART_TYPE, PRICING_BY_DAY_PART_TYPE,
RECAL_ACK_YES,
} from "@/constants/schedule-constants"; } from "@/constants/schedule-constants";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -242,6 +250,7 @@ import {
isDropOffRouteCode, isDropOffRouteCode,
getTodayDate, getTodayDate,
} from "@/layouts/schedule/helpers/schedule-helper"; } 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 { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
@ -287,6 +296,7 @@ const getScheduleApiResponse = async ({
startDateString, startDateString,
endDateString, endDateString,
providerNumber, providerNumber,
inshopProviderNumber,
zipCode, zipCode,
includeMobileTimeSlots = true, includeMobileTimeSlots = true,
includeInshopTimeSlots = true, includeInshopTimeSlots = true,
@ -329,7 +339,7 @@ const getScheduleApiResponse = async ({
startDate: apiStartDate, startDate: apiStartDate,
endDate: apiEndDate, endDate: apiEndDate,
shopAppointmentType: "InshopOrDropoff", shopAppointmentType: "InshopOrDropoff",
providerNumber: providerNumber, providerNumber: inshopProviderNumber,
}, },
}; };
if (apiStartDate < apiEndDate) { if (apiStartDate < apiEndDate) {
@ -429,6 +439,7 @@ export default {
name: "schedule", name: "schedule",
data() { data() {
return { return {
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
selectedDate: this.getSelectedDateFromStore(), selectedDate: this.getSelectedDateFromStore(),
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(), selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
@ -469,6 +480,11 @@ export default {
navigatingForward: false, navigatingForward: false,
shopListButton: shopListButton, shopListButton: shopListButton,
lastSelectedInshopOrDropoffProvider: null, lastSelectedInshopOrDropoffProvider: null,
selectedMobileFirstAppointment: false,
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
),
}; };
}, },
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -597,7 +613,9 @@ export default {
pricedMobileFeePart, pricedMobileFeePart,
shopProviderData.data shopProviderData.data
); );
vm.initializeDatePicker(); vm.initializeDatePicker().then(() => {
vm.showMobileFirstModal();
});
}); });
}, },
computed: { computed: {
@ -614,7 +632,10 @@ export default {
}, },
}, },
isMobileSelected() { isMobileSelected() {
return this.appointmentType == AppointmentTypeStrings.MOBILE; return (
this.appointmentType == AppointmentTypeStrings.MOBILE ||
(this.isServiceableMobile && !this.isServiceableInshop)
);
}, },
isServiceableMobile() { isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) { if (this.isRecalibrationServiceableMobile !== null) {
@ -626,6 +647,9 @@ export default {
return this.isGlassServiceableMobile; return this.isGlassServiceableMobile;
} }
}, },
closestInshopProviderNumber() {
return this.shopProviderData?.shopProviders[0]?.providerNumber;
},
showTimeSlotQuestion() { showTimeSlotQuestion() {
if ( if (
this.selectedDate && this.selectedDate &&
@ -858,7 +882,9 @@ export default {
); );
}, },
isMobileFirstModalOpen() { isMobileFirstModalOpen() {
return this.isMobileSelected ? this.$refs.mobileFirstModal?.getIsModalOpen() : false; return this.selectableDatesMobile.days
? this.$refs.mobileFirstModal?.getIsModalOpen()
: false;
}, },
}, },
methods: { methods: {
@ -1121,30 +1147,7 @@ export default {
); );
} }
}, },
getTimeSlotInfo() { async onShopSelected(shopQuestionPopUpData) {
// 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) {
this.resetWaitlist(); this.resetWaitlist();
this.preSelectedDate = null; this.preSelectedDate = null;
this.shopProviderData = shopQuestionPopUpData.shopProviderData; this.shopProviderData = shopQuestionPopUpData.shopProviderData;
@ -1152,6 +1155,8 @@ export default {
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => { const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber; return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
}); });
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider?.providerNumber;
// No zipCodeData comes back if zip was not changed // No zipCodeData comes back if zip was not changed
if (shopQuestionPopUpData.zipCodeData) { if (shopQuestionPopUpData.zipCodeData) {
@ -1160,13 +1165,12 @@ export default {
selectedProvider selectedProvider
); );
} else { } else {
const didProviderNumberChange =
oldProvider?.providerNumber !== selectedProvider.providerNumber;
if (didProviderNumberChange) { if (didProviderNumberChange) {
this.updateSelectedProvider(selectedProvider); this.updateSelectedProvider(selectedProvider);
this.initializeDatePicker();
} }
} }
await this.initializeDatePicker();
this.setSelectedDateToFirstAvailable();
}, },
async getMoreScheduleData(startDate, endDate) { async getMoreScheduleData(startDate, endDate) {
// called only when "View more dates" is clicked; not on initial page load // called only when "View more dates" is clicked; not on initial page load
@ -1174,6 +1178,9 @@ export default {
startDateString: startDate, startDateString: startDate,
endDateString: endDate, endDateString: endDate,
providerNumber: this.selectedProvider?.providerNumber, providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode, zipCode: this.zipCode,
includeMobileTimeSlots: this.isServiceableMobile, includeMobileTimeSlots: this.isServiceableMobile,
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff, includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
@ -1187,12 +1194,12 @@ export default {
moreShopTimeSlots.mobileTimeSlotsData.days moreShopTimeSlots.mobileTimeSlotsData.days
); );
this.showMobileFirstModal();
return moreShopTimeSlots; return moreShopTimeSlots;
}, },
async initializeDatePicker() { async initializeDatePicker() {
this.isShowMobileFirstAppt && this.showLoadingModal();
this.selectedDate = null; this.selectedDate = null;
const includeMobileTimeSlots = this.isServiceableMobile; const includeMobileTimeSlots = this.isServiceableMobile;
@ -1204,6 +1211,9 @@ export default {
getSelectableDatesCallback: getScheduleApiResponse, getSelectableDatesCallback: getScheduleApiResponse,
preSelectedDate: this.preSelectedDate, preSelectedDate: this.preSelectedDate,
providerNumber: this.selectedProvider?.providerNumber, providerNumber: this.selectedProvider?.providerNumber,
inshopProviderNumber: this.isMobileSelected
? this.closestInshopProviderNumber
: this.selectedProvider?.providerNumber,
zipCode: this.zipCode, zipCode: this.zipCode,
includeMobileTimeSlots: includeMobileTimeSlots, includeMobileTimeSlots: includeMobileTimeSlots,
includeInshopTimeSlots: includeInshopTimeSlots, includeInshopTimeSlots: includeInshopTimeSlots,
@ -1230,6 +1240,11 @@ export default {
this.setDisplayWaitList(); this.setDisplayWaitList();
} }
} }
this.isShowMobileFirstAppt && this.hideLoadingModal();
},
getIsRecalAcknowledgedForSchedulingFromStore() {
return store.getters.order.isRecalAcknowledgedForScheduling;
}, },
getScheduleApiResponse, getScheduleApiResponse,
getServiceZipCtuCodeFromStore() { getServiceZipCtuCodeFromStore() {
@ -1346,7 +1361,28 @@ export default {
showLoadingModal() { showLoadingModal() {
this.$refs.loadingModal.showModal(); this.$refs.loadingModal.showModal();
}, },
hideLoadingModal() {
this.$refs.loadingModal.hideModal();
},
async closeRecalAcknowledgementPopup() {
this.showRecalAcknowledgementPopup = false;
},
async forwardButtonAction() { 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; this.navigatingForward = true;
var ctu = this.zipCodeCtu; var ctu = this.zipCodeCtu;
if (this.appointmentType === AppointmentTypeStrings.MOBILE) { if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
@ -1648,6 +1684,8 @@ export default {
this.updateSelectedProvider(); this.updateSelectedProvider();
}, },
updateSelectedProvider(newShopProvider) { updateSelectedProvider(newShopProvider) {
if (!this.appointmentTypeStrings) return;
if (newShopProvider) { if (newShopProvider) {
this.selectedProvider = newShopProvider; this.selectedProvider = newShopProvider;
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) { } else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
@ -1668,7 +1706,27 @@ export default {
this.selectedProvider = this.shopProviderData.shopProviders[0]; 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(); this.resetMobileLocation();
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => { getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
@ -1678,7 +1736,9 @@ export default {
this.zipCode = newZipCode.zipCode; this.zipCode = newZipCode.zipCode;
this.zipCodeCtu = newZipCode.zipCodeCtu; this.zipCodeCtu = newZipCode.zipCodeCtu;
this.selectedDate = null; this.selectedDate = null;
this.initializeDatePicker(); this.initializeDatePicker().then(() => {
this.showMobileFirstModal();
});
}); });
}, },
updateSelectedProviderAndZipCode(newZipCode, newProvider) { updateSelectedProviderAndZipCode(newZipCode, newProvider) {
@ -1689,7 +1749,6 @@ export default {
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase; this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
this.selectedDate = null; this.selectedDate = null;
this.selectedProvider = newProvider; this.selectedProvider = newProvider;
this.initializeDatePicker();
}, },
handleAppointmentTypeChange(newAppointmentType) { handleAppointmentTypeChange(newAppointmentType) {
this.updateFooterButtonText(); this.updateFooterButtonText();
@ -1716,7 +1775,6 @@ export default {
} }
this.appointmentType = AppointmentTypeStrings.MOBILE; this.appointmentType = AppointmentTypeStrings.MOBILE;
this.updateSelectedProvider(); this.updateSelectedProvider();
this.showMobileFirstModal();
} else if (newAppointmentType) { } else if (newAppointmentType) {
if (this.appointmentType != AppointmentTypeStrings.MOBILE) { if (this.appointmentType != AppointmentTypeStrings.MOBILE) {
// Clear last shop selected if appointment type was changed in any manner other than from 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; this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
} }
// make sure a selectedProvider exists // make sure a selectedProvider exists
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider); if (this.lastSelectedInshopOrDropoffProvider) {
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
} else {
this.updateSelectedProvider();
}
} else { } else {
this.appointmentType = null; this.appointmentType = null;
} }
@ -1741,12 +1803,34 @@ export default {
setSelectedDateToFirstAvailable() { setSelectedDateToFirstAvailable() {
this.selectedDate = this.getFirstAvailableDate(); 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() { async showMobileFirstModal() {
if (this.appointmentType == AppointmentTypeStrings.MOBILE) { if (!this.selectedRouteCodeData?.routeCode) {
const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SHOW_MOBILE_FIRST_APPT,
"true"
);
const pmMobileDays = experimentMixin.methods.getSettingValue( const pmMobileDays = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_PM_MOBILE_DAYS experimentSettings.SHOW_PM_MOBILE_DAYS
); );
@ -1801,32 +1885,40 @@ export default {
); );
} }
if (isShowMobileFirstAppt) { if (this.isShowMobileFirstAppt) {
if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) { 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; preSelectedMobileAppointment = firstMobilePMAppt;
} else if ( } else if (
firstMobilePMDate && firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) { ) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
preSelectedMobileAppointment = firstMobileAMAppt preSelectedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt ? firstMobileAMAppt
: null; : firstMobilePMAppt;
!preSelectedMobileAppointment &&
(preSelectedMobileAppointment = firstMobilePMAppt);
} }
this.$refs.mobileFirstModal.setSelectedAppointment( if (preSelectedMobileAppointment) {
preSelectedMobileAppointment this.$refs.mobileFirstModal.setSelectedAppointment(
); preSelectedMobileAppointment
this.$refs.mobileFirstModal.openModal(); );
this.$refs.mobileFirstModal.openModal();
}
} }
} }
} }
}, },
async getFirstAvailableMobileApptByTOD(timeOfDay) { async getFirstAvailableMobileApptByTOD(timeOfDay) {
if (!this.selectableDatesMobile.days) {
await this.initializeDatePicker();
}
if (!this.selectableDatesMobile?.days?.length) { if (!this.selectableDatesMobile?.days?.length) {
return null; return null;
} else { } else {
@ -1853,9 +1945,17 @@ export default {
} }
}, },
updateTimeSlotandNavigateForward(timeSlotObj) { updateTimeSlotandNavigateForward(timeSlotObj) {
this.selectedMobileFirstAppointment = true;
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.selectedDate = timeSlotObj.date;
this.updateSelectedProvider();
this.updateTimeSlot(timeSlotObj); this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction(); this.forwardButtonAction();
}, },
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
},
}, },
watch: { watch: {
appointmentTypeFromAppointmentTypeQuestion: { appointmentTypeFromAppointmentTypeQuestion: {
@ -1872,7 +1972,7 @@ export default {
const hasValueChanged = newValue !== oldValue; const hasValueChanged = newValue !== oldValue;
const isDateDifferent = (newValue || oldValue) !== selectedDate; const isDateDifferent = (newValue || oldValue) !== selectedDate;
if (hasValueChanged && isDateDifferent) { if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) {
this.selectedTimeSlotInfo = { this.selectedTimeSlotInfo = {
timeSlot: { timeSlot: {
date: null, date: null,
@ -1901,7 +2001,7 @@ export default {
textBlock, textBlock,
timeSlotQuestion, timeSlotQuestion,
mobileFirstModal, mobileFirstModal,
recalAckModal,
alert, alert,
serviceZipModalQuestion, serviceZipModalQuestion,
appointmentTypeQuestion, appointmentTypeQuestion,

View file

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

View file

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

View file

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

View file

@ -232,6 +232,21 @@ export default {
(part) => `${part.glassLocation}-${part.glassName}` (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 appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`;
var sessionData = {}; var sessionData = {};
@ -251,7 +266,7 @@ export default {
? "Insurance" ? "Insurance"
: "Cash"; : "Cash";
sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace"; sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace";
sessionData.productType = glassProducts; sessionData.productType = allParts;
sessionData.eon = order?.eon; sessionData.eon = order?.eon;
sessionData.referralNumber = order?.referralNumber; sessionData.referralNumber = order?.referralNumber;
sessionData.referralSequenceNumber = order?.referralSequenceNumber; sessionData.referralSequenceNumber = order?.referralSequenceNumber;
@ -958,6 +973,8 @@ function getPageNameFromRouter() {
) { ) {
return router.currentRoute.value.name; return router.currentRoute.value.name;
} }
return window.location.href.replace(/\/$/, "").split("/").pop();
} }
function getValueToLog(value, valueToLogType) { function getValueToLog(value, valueToLogType) {

View file

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

View file

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

View file

@ -89,6 +89,15 @@ export const routeData = {
name: "payment-pia-return", name: "payment-pia-return",
path: "/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: { CONFIRMATION: {
name: "confirmation", name: "confirmation",
path: "/confirmation", path: "/confirmation",

View file

@ -486,6 +486,10 @@ const routingTable = function () {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE, scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
destinationPageData: routeData.MOBILE_DETAILS, 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, scenario: navigationScenarios.CLICKED_PAY_NOW,
destinationPageData: routeData.PAYMENT, destinationPageData: routeData.PAYMENT,
}, },
{
scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN,
destinationPageData: routeData.PAYMENT_ADYEN,
},
{ {
scenario: navigationScenarios.CLICKED_INSURANCE, scenario: navigationScenarios.CLICKED_INSURANCE,
destinationPageData: routeData.INSURANCE_COMPANY, 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, pageName: routeData.PAYMENT_PIA_RETURN.name,
maps: [ maps: [

View file

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

View file

@ -3,16 +3,39 @@ import { queryStrings } from "@/constants/query-strings";
import store from "@/store"; import store from "@/store";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function consumeReferralQuerystrings() { export async function consumeReferralQuerystrings() {
const referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER); let referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT); let parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID); 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) { if (referralNumber) {
store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount); store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount);
store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId); 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(); updateOrCreateFunnelCookie();
} }
} }

View file

@ -2,11 +2,13 @@ import { queryStrings } from "@/constants/query-strings";
import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info"; import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info";
import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper"; import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper";
import { routeData } from "@/router/constants/routes"; import { routeData } from "@/router/constants/routes";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
export async function handleHeritageReturn(to, from) { export async function handleHeritageReturn(to, from) {
const fromHeritageFlag = to.query[queryStrings.FROM_HERITAGE]; 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. // Get new referral info from querystring in case passed there.
await consumeReferralQuerystrings(); 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 { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
import store from "@/store"; import store from "@/store";
import { handleHardError } from "@/router/methods/error"; import { handleHardError } from "@/router/methods/error";
import analyticsMixin from "@/mixins/analytics-mixin";
export async function errorBeforeEnter(to, from) { export async function errorBeforeEnter(to, from) {
// Check if `hasAlreadyTriggeredErrror` is available. // Check if `hasAlreadyTriggeredErrror` is available.
@ -18,6 +19,7 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name, nextPage: to?.name,
}; };
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload); await handleHardError(errorPayload);
return; return;
} }
@ -31,6 +33,7 @@ export async function errorBeforeEnter(to, from) {
nextPage: to?.name, nextPage: to?.name,
}; };
analyticsMixin.methods.logDigitalConsumer();
await handleHardError(errorPayload); await handleHardError(errorPayload);
return; return;
} else { } else {

View file

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

View file

@ -122,6 +122,7 @@ const getDefaultState = () => {
capabilityQuestionAnswers: null, capabilityQuestionAnswers: null,
dateOfLoss: null, dateOfLoss: null,
damageCause: null, damageCause: null,
installOemGlass: null,
}, },
lineItems: { lineItems: {
glassParts: null, glassParts: null,
@ -189,6 +190,7 @@ const getDefaultState = () => {
lockToken: null, lockToken: null,
settledTenderAmount: 0, settledTenderAmount: 0,
isRecalAckOptIn: false, isRecalAckOptIn: false,
isRecalAcknowledgedForScheduling: "",
isMSRFeeApplicable: false, isMSRFeeApplicable: false,
}, },
applicationUser: { applicationUser: {
@ -342,6 +344,12 @@ export const mutations = {
updateIsRecalAckOptIn(state, isRecalAckOptIn) { updateIsRecalAckOptIn(state, isRecalAckOptIn) {
state.order.isRecalAckOptIn = isRecalAckOptIn; state.order.isRecalAckOptIn = isRecalAckOptIn;
}, },
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
},
updateIsOemGlassSelected(state, isOemGlassSelected) {
state.order.damage.installOemGlass = isOemGlassSelected;
},
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
state.order.isMSRFeeApplicable = isMSRFeeApplicable; state.order.isMSRFeeApplicable = isMSRFeeApplicable;
}, },
@ -712,6 +720,8 @@ export const mutations = {
state.order.damage.capabilityQuestionAnswers = state.order.damage.capabilityQuestionAnswers =
sessionInformation.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.glassParts = sessionInformation.order.lineItems.glassParts;
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems; state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? []; state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? [];
@ -1765,7 +1775,8 @@ export const actions = {
const glassArray = damage.glassToReplace; const glassArray = damage.glassToReplace;
const zipCode = order.serviceLocation.zipCode; const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin; 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; const referralSeqNumber = order.referralSequenceNumber;
// create a new array to avoid mutating state // create a new array to avoid mutating state
@ -1781,6 +1792,7 @@ export const actions = {
vin: vin, vin: vin,
serviceType: serviceType, serviceType: serviceType,
referralSeqNumber: referralSeqNumber, referralSeqNumber: referralSeqNumber,
parentAccountNumber: parentAccountNumber,
}, },
logApiCall: true, logApiCall: true,
pageNameToLog: pageNameToLog, pageNameToLog: pageNameToLog,
@ -1836,6 +1848,55 @@ export const actions = {
return response; 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 }) { async getWipers(context, { payload, pageNameToLog }) {
const carId = payload.carId; const carId = payload.carId;
const serviceZipCode = payload.serviceZipCode; const serviceZipCode = payload.serviceZipCode;
@ -1936,26 +1997,6 @@ export const actions = {
return response; 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 }) { getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
const escapeRecalibrationType = (rt) => rt.split("&").join("%26"); const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
@ -2363,9 +2404,17 @@ export const actions = {
//affiliate cookie save only when submitAfterSave //affiliate cookie save only when submitAfterSave
const affiliateCookies = submitAfterSave ? applicationUser.affiliateCookies : null; 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({ return globalMethods.callHttpClient({
method: endpoints.SaveSession.method, method: endpoints.SaveSession.method,
endpoint: endpoints.SaveSession.url, endpoint: endPoint,
payload: { payload: {
submitAfterSave: submitAfterSave, submitAfterSave: submitAfterSave,
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
@ -2413,6 +2462,7 @@ export const actions = {
partQuestionAnswers: order.damage.partQuestionAnswers, partQuestionAnswers: order.damage.partQuestionAnswers,
moldingQuestionAnswers: order.damage.moldingQuestionAnswers, moldingQuestionAnswers: order.damage.moldingQuestionAnswers,
capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers, capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers,
installOemGlass: order.damage.installOemGlass,
}, },
lineItems: { lineItems: {
glassParts: lineItems.glassParts, glassParts: lineItems.glassParts,
@ -2919,6 +2969,17 @@ export const actions = {
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn); 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) { saveIsMSRFeeApplicable(context, isMSRFeeApplicable) {
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable); context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
}, },
@ -3085,40 +3146,24 @@ export const actions = {
promoCode: lineItem.promoCode ?? null, promoCode: lineItem.promoCode ?? null,
})); }));
const pricedLineItemsFormattedForRequest = const order = context.getters.order;
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 response = await globalMethods.callHttpClient({ const response = await globalMethods.callHttpClient({
method: endpoints.TaxOrderItems.method, 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, logApiCall: true,
pageNameToLog: pageNameToLog, pageNameToLog: pageNameToLog,
}); });

View file

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

View file

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

View file

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