Merge remote-tracking branch 'origin/develop' into feature/CASH-1527
This commit is contained in:
commit
7db73ef10e
84 changed files with 3033 additions and 546 deletions
|
|
@ -13,6 +13,8 @@ const environmentVariables = {
|
|||
['__VUE_APP_SAFELITE_HOP__']: "https://sv2-safelitehop-dev.safelite.com/fmgCheckoutShared.aspx",
|
||||
['__VUE_APP_SESSION_TIMEOUT_MINUTES__']: 30,
|
||||
['__VUE_APP_SOLARWINDS_MONITORING_SCRIPT__']: "",
|
||||
['__VUE_APP_ADYEN_ENVIRONMENT__']: "test",
|
||||
['__VUE_APP_ADYEN_CLIENT_KEY__']: "test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD",
|
||||
};
|
||||
|
||||
const keysOnly = Object.keys(environmentVariables);
|
||||
|
|
|
|||
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"name": "Safelite",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@adyen/adyen-web": "^6.27.0",
|
||||
"@iframe-resizer/child": "^5.3.3",
|
||||
"axios": "^0.30.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
|
|
@ -59,6 +60,18 @@
|
|||
"node": "8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22"
|
||||
}
|
||||
},
|
||||
"node_modules/@adyen/adyen-web": {
|
||||
"version": "6.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.27.0.tgz",
|
||||
"integrity": "sha512-6E6hqEkMkoY/FevjO53zhgeCWMt+i/PDc11MX6eVM7Fvd+Sdgc9cIwTBz+NKIXs4ZerG6n2gd+ZIZEywqF/aBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/applepayjs": "14.0.9",
|
||||
"@types/googlepay": "0.7.8",
|
||||
"classnames": "2.5.1",
|
||||
"preact": "10.22.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
|
|
@ -3173,6 +3186,12 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/applepayjs": {
|
||||
"version": "14.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.9.tgz",
|
||||
"integrity": "sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -3328,6 +3347,12 @@
|
|||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/googlepay": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.8.tgz",
|
||||
"integrity": "sha512-EHl7G7jIeit/Px/fi/Du/19SZDvqrRhy0DjJX40Vzs/97m3sro9mzVyAYNC0pfzpWGDY+zPjtTLEfhV+OYWUcQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
|
|
@ -5789,6 +5814,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||
|
|
@ -13681,6 +13712,16 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.22.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz",
|
||||
"integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adyen/adyen-web": "^6.27.0",
|
||||
"@iframe-resizer/child": "^5.3.3",
|
||||
"axios": "^0.30.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { PaymentMethod } from './localTypes/Enums'
|
|||
|
||||
export interface ITestData extends base {
|
||||
// Put any project-specific data here. Anything useful to other Safelite projects should be submitted as a pull request to safelite-playwright-core.
|
||||
paymentMethod: PaymentMethod
|
||||
isOptedInForTextMessages: boolean
|
||||
totalAmount?: number
|
||||
paymentMethod: PaymentMethod,
|
||||
isOptedInForTextMessages: boolean,
|
||||
totalAmount?: number,
|
||||
isForcedOEM?: boolean
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import { CCPolicyInfoPage } from "../pages/CCPolicyInfoPage"
|
|||
import { DuplicateCheckPage } from "../pages/DuplicateCheckPage"
|
||||
import { PolicyVehiclesPage } from "../pages/PolicyVehiclesPage"
|
||||
import { PolicyInfoSubmittedPage } from "../pages/PolicyInfoSubmittedPage"
|
||||
import { HeritageProblemGlassQuestionsPage } from "../pages/HeritageProblemGlassQuestionsPage"
|
||||
import RecalibrationInfoPage from "../pages/RecalibrationInfoPage"
|
||||
import { CoverageStatementPage } from "../pages/CoverageStatementPage"
|
||||
import { VerifyDetailsPage } from "../pages/VerifyDetailsPage"
|
||||
|
|
@ -48,6 +49,7 @@ export interface ITestPages {
|
|||
paymentMethodPage: PaymentMethodPage,
|
||||
policyInfoSubmittedPage: PolicyInfoSubmittedPage,
|
||||
policyVehiclesPage: PolicyVehiclesPage,
|
||||
heritageProblemGlassQuestionsPage: HeritageProblemGlassQuestionsPage,
|
||||
recalibrationInfoPage: RecalibrationInfoPage,
|
||||
schedulePage: SchedulePage,
|
||||
mobileDetailsPage: MobileDetailsPage,
|
||||
|
|
@ -83,6 +85,7 @@ export const createTestPages: TestPagesFactory<ITestPages> = (page: Page) => {
|
|||
paymentMethodPage: new PaymentMethodPage(page),
|
||||
policyInfoSubmittedPage: new PolicyInfoSubmittedPage(page),
|
||||
policyVehiclesPage: new PolicyVehiclesPage(page),
|
||||
heritageProblemGlassQuestionsPage: new HeritageProblemGlassQuestionsPage(page),
|
||||
recalibrationInfoPage: new RecalibrationInfoPage(page),
|
||||
schedulePage: new SchedulePage(page),
|
||||
mobileDetailsPage: new MobileDetailsPage(page),
|
||||
|
|
|
|||
|
|
@ -169,4 +169,20 @@ export class BasePage {
|
|||
Soft.expect(actualProgressPercentage).toBe(progressPercentage);
|
||||
console.log(`Progress Bar Percentage: Actual - ${actualProgressPercentage} vs Expected - ${progressPercentage}`);
|
||||
}
|
||||
|
||||
async validateOEMPart( isOEM: boolean): Promise<void> {
|
||||
if (isOEM!) {
|
||||
// Get Vuex state from localStorage
|
||||
const vuexState = JSON.parse(await this.page.evaluate('localStorage.getItem(\'vuex\')'));
|
||||
|
||||
// Validate the presence of an OEM part
|
||||
if (vuexState.order?.lineItems?.glassParts?.length > 0) {
|
||||
const firstGlassPartNumber = vuexState.order.lineItems.glassParts[0].partNumber;
|
||||
|
||||
await expect(firstGlassPartNumber.includes("OEM")).toBe(true);
|
||||
} else {
|
||||
throw new Error("No glass parts found in the order");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
playwright-tests/pages/HeritageProblemGlassQuestionsPage.ts
Normal file
42
playwright-tests/pages/HeritageProblemGlassQuestionsPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ export class HomePage extends BasePage {
|
|||
super(page);
|
||||
|
||||
this.page = page;
|
||||
this.letsGetStartedButton = this.page.locator('.hero-content #zipCodeTextboxButton');
|
||||
this.letsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
|
||||
this.cusmodalPopup = this.page.locator('#Cusmodalpopup');
|
||||
this.closePopupButton = this.page.getByRole('button', { name: '×' });
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export class HomePage extends BasePage {
|
|||
|
||||
//Zip Entry
|
||||
this.enterServiceZipTextBox = this.page.locator('#zipCodeTextbox');
|
||||
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextboxButton');
|
||||
this.zipEntryLetsGetStartedButton = this.page.locator('#zipCodeTextbox');
|
||||
this.getQuoteAndScheduleButton = this.page.getByLabel('main').getByRole('link', { name: 'Get quote + schedule' });
|
||||
}
|
||||
|
||||
|
|
@ -61,10 +61,11 @@ export class HomePage extends BasePage {
|
|||
|
||||
async letsGetStarted(zip: string, enterFunnelWithZip: boolean) {
|
||||
if (enterFunnelWithZip) {
|
||||
await this.letsGetStartedButton.evaluate((element, zip) => {
|
||||
await this.zipEntryLetsGetStartedButton.fill(zip);
|
||||
/* await this.letsGetStartedButton.evaluate((element, zip) => {
|
||||
const currentHref = element.getAttribute('href') || '';
|
||||
element.setAttribute('href', `${currentHref}&zipCode=${zip}`);
|
||||
}, zip);
|
||||
element.setAttribute('href', `${currentHref}?zipCode=${zip}`);
|
||||
}, zip);*/
|
||||
}
|
||||
|
||||
// Wait until modal pop up opens and close pop up
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@ export class LookupPage extends BasePage {
|
|||
|
||||
// Map of expected messages by lookup type
|
||||
const expectedMessages = {
|
||||
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
|
||||
'license plate': "Your license plate didn’t return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
|
||||
'VIN': "Your VIN didn’t return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
|
||||
'address': "Your address didn't return a VIN match.Please re-enter the information below or provide your VIN in a different way.",
|
||||
'license plate': "Your license plate didn’t return a VIN match.Please re-enter the information above or provide your VIN in a different way.",
|
||||
'VIN': "Your VIN didn’t return a vehicle matchPlease re-enter your VIN or we can look up your VIN for you.",
|
||||
'ZIP code': "Your ZIP code didn't return a match.Please re-enter the information above or provide your VIN in a different way."
|
||||
};
|
||||
|
||||
|
|
@ -106,19 +106,19 @@ export class LookupPage extends BasePage {
|
|||
}
|
||||
}
|
||||
|
||||
async handleZipValidation(testData: Partial<ITestData>, zip: string, lookupType: VehicleLookupType, alertFlags?: IAlertFlags): Promise<boolean> {
|
||||
async handleZipValidation(testData: Partial<ITestData>): Promise<boolean> {
|
||||
|
||||
let { vehicleDamage } = testData;
|
||||
let { customerDetails,vehicleDamage, vehicleDetails, alertFlags } = testData;
|
||||
// Only proceed with validation if alertFlags is provided
|
||||
if (alertFlags) {
|
||||
if (alertFlags.isUnserviceableZip) {
|
||||
await this.checkForUnserviceableZipAlertMessage(zip);
|
||||
await this.checkForUnserviceableZipAlertMessage(customerDetails!.address.postalCode!);
|
||||
throw new TestSuccessAlert('Unserviceable ZIP validation successful.');
|
||||
} else if (alertFlags.isInvalidZip) {
|
||||
await this.checkForInvalidZipAlertMessage();
|
||||
throw new TestSuccessAlert('Invalid ZIP validation successful.');
|
||||
} else if (alertFlags.isVinNotFound) {
|
||||
await this.checkForVinNotFoundAlertMessage(lookupType);
|
||||
await this.checkForVinNotFoundAlertMessage(vehicleDetails!.vehicleLookupType!);
|
||||
throw new TestSuccessAlert('Vin not found validation successful.');
|
||||
} else {
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export class OrderConfirmationPage extends BasePage {
|
|||
async validateOrderConfirmationPage(testData: Partial<ITestData>) {
|
||||
// Destructure data we use
|
||||
const { vehicleDetails, customerDetails, servicePackage, isCashInsuranceFlow,
|
||||
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified } = testData;
|
||||
isPolicyFound, claimDetails, paymentDetails, isUseVehicleOnPolicy, paymentMethod, isPolicyUnverified} = testData;
|
||||
await this.serviceText.waitFor({ state: "visible" });
|
||||
|
||||
Soft.expect((await this.getActualAppointmentSummary()).map(item => item.toLowerCase())).toEqual((await this.getExpectedAppointmentSummary(testData)).map(item => item.toLowerCase()));
|
||||
|
|
|
|||
|
|
@ -499,12 +499,16 @@ export class PaymentMethodPage extends BasePage {
|
|||
|
||||
@step("PaymentMethodPage >> Select Payment Method: ")
|
||||
async handlePaymentMethodPage(testData: Partial<ITestData>) {
|
||||
const { servicePackage, isRecalVehicle, paymentDetails } = testData;
|
||||
const { servicePackage, isRecalVehicle, paymentDetails, isForcedOEM } = testData;
|
||||
|
||||
await this.validateProgressBar(ProgressBarPercentages.PaymentMethodPage);
|
||||
await this.validatePaymentDetailsPage(testData);
|
||||
await this.ValidateAfterPayBreakOutSection();
|
||||
|
||||
if (isForcedOEM){
|
||||
await this.validateOEMPart(isForcedOEM);
|
||||
}
|
||||
|
||||
// Verify VAPS wipers on backend for standard and premium packages
|
||||
if (servicePackage === ServicePackage.Standard || servicePackage === ServicePackage.Premium) {
|
||||
await this.verifyVAPS();
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ export class PaypalPage extends BasePage {
|
|||
await this.paypalLoginButton.click();
|
||||
await this.completePurchaseButton.click();
|
||||
} else {
|
||||
await this.usernameTextBox.waitFor({ state: 'visible' });
|
||||
await this.page.screenshot({ path: `test-results\\ortoni-data\\paypal-username-${Date.now()}.png`, fullPage: true });
|
||||
await this.usernameTextBox.fill(paymentDetails.username!);
|
||||
await this.nextButton.click();
|
||||
await this.tryAnotherWayButton.click();
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export class SchedulePage extends BasePage {
|
|||
readonly timeSlots: Locator;
|
||||
readonly allDayDropOffButton: Locator;
|
||||
readonly pickATimeButton: Locator;
|
||||
readonly mobileFirstModalCloseButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
|
|
@ -65,6 +66,7 @@ export class SchedulePage extends BasePage {
|
|||
this.viewMoreDatesLink = this.page.getByText(/View more dates/).first();
|
||||
this.appointmentDuration = this.page.locator('.duration-text-block');
|
||||
this.timeSlots = this.page.locator('fieldset:has(>legend#chooseTimeSlot) label').filter({ visible: true});
|
||||
this.mobileFirstModalCloseButton = this.page.getByRole('dialog').getByRole('button', { name: 'Close' });
|
||||
}
|
||||
|
||||
async selectLocation(testData: Partial<ITestData>) {
|
||||
|
|
@ -229,6 +231,9 @@ export class SchedulePage extends BasePage {
|
|||
async handleSchedulePage(testData: Partial<ITestData>) {
|
||||
const { appointmentDetails } = testData;
|
||||
await this.validateProgressBar(ProgressBarPercentages.SchedulePage);
|
||||
if (await this.mobileFirstModalCloseButton.isVisible()) {
|
||||
await this.mobileFirstModalCloseButton.click();
|
||||
}
|
||||
await this.selectLocation(testData);
|
||||
await this.scheduleFirstAppointment(testData);
|
||||
await this.nextPage();
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class ServicePackagesPage extends BasePage {
|
|||
this.promoCodeTextbox = this.page.getByLabel('Enter a promo code');
|
||||
this.applyPromoButton = this.page.getByRole('button', { name: 'Apply promo code' });
|
||||
this.repeatedClicksModalCloseButton = this.page.locator('.QSISlider').locator('img[src*=\'close\']');
|
||||
this.afterPayBanner = this.page.locator('#afterpay-banner');
|
||||
this.afterPayBanner = this.page.locator('div.afterpay-modal-banner');
|
||||
this.glassOnlypackagePrice = this.page.locator('fieldset:has(legend#ServicePackageQuestion) label').filter({ hasText: 'Glass service only' }).locator('.pricing-info');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ export class ServiceZipPage extends LookupPage {
|
|||
|
||||
@step("ZipLookupPage >> Lookup by service ZIP: ")
|
||||
async handleServiceZipPage(testData: Partial<ITestData>) {
|
||||
const { customerDetails, vehicleDetails, alertFlags } = testData;
|
||||
const { customerDetails } = testData;
|
||||
await this.validateProgressBar(ProgressBarPercentages.ServiceZipPage);
|
||||
await this.enterZip(customerDetails!.address.postalCode!);
|
||||
await this.handleZipValidation(testData, customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
|
||||
await this.handleZipValidation(testData);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,10 +29,10 @@ export class VehicleLookupAddressPage extends LookupPage {
|
|||
|
||||
@step("VehicleLookupAddressPage >> Lookup by address: ")
|
||||
async handleVehicleLookupAddressPage(testData: Partial<ITestData>) {
|
||||
const { customerDetails, vehicleDetails, alertFlags } = testData;
|
||||
const { customerDetails, vehicleDetails } = testData;
|
||||
|
||||
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupAddressPage);
|
||||
await this.lookupVehicleByAddress(customerDetails!, vehicleDetails!);
|
||||
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
|
||||
await this.handleZipValidation(testData);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,11 +34,11 @@ export class VehicleLookupLicensePage extends LookupPage {
|
|||
|
||||
@step("VehicleLookupLicensePage >> Lookup by license plate: ")
|
||||
async handleVehicleLookupLicensePage(testData: Partial<ITestData>) {
|
||||
const { customerDetails, vehicleDetails, alertFlags } = testData;
|
||||
const { customerDetails, vehicleDetails } = testData;
|
||||
|
||||
await this.validateProgressBar(ProgressBarPercentages.VehicleLookupLicensePage);
|
||||
await this.enterPlateDetails(vehicleDetails!);
|
||||
await this.enterZip(customerDetails!.address.postalCode!);
|
||||
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
|
||||
await this.handleZipValidation(testData);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,11 +24,11 @@ export class VinLookupPage extends LookupPage {
|
|||
|
||||
@step("VinLookupPage >> Lookup by VIN: ")
|
||||
async handleVehicleLookupVinPage(testData: Partial<ITestData>) {
|
||||
const { customerDetails, vehicleDetails, alertFlags } = testData;
|
||||
const { customerDetails, vehicleDetails } = testData;
|
||||
let vin: string;
|
||||
|
||||
if (Array.isArray(vehicleDetails?.vin)) {
|
||||
vin = vehicleDetails.vin[0];
|
||||
vin = vehicleDetails!.vin[0];
|
||||
} else {
|
||||
vin = vehicleDetails?.vin || '';
|
||||
}
|
||||
|
|
@ -36,6 +36,6 @@ export class VinLookupPage extends LookupPage {
|
|||
await this.validateProgressBar(ProgressBarPercentages.VinLookupPage);
|
||||
await this.enterVin(vin);
|
||||
await this.enterZip(customerDetails!.address.postalCode!);
|
||||
await this.handleZipValidation(customerDetails!.address.postalCode!, vehicleDetails!.vehicleLookupType!, alertFlags!);
|
||||
await this.handleZipValidation(testData);
|
||||
}
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ export class AddressForm extends BasePage {
|
|||
);
|
||||
});
|
||||
|
||||
await this.page.hover(".pac-container .pac-item");
|
||||
await this.addressSuggestionList.hover();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await this.addressSuggestionList.click(); // Select the first suggestion
|
||||
//await this.streetAddressTextBox.press('Tab'); // Move focus to ZIP code field
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import insuranceITAC21stCenturyTests from "./InsuranceITAC21stCentury";
|
|||
import insuranceGeicoTests from "./InsuranceGeico";
|
||||
import insuranceITACOptimizedPriceValidationAllStateTests from "./InsuranceITACOptimizedPriceValidationAllState";
|
||||
import insuranceNoCompProgressiveTests from "./InsuranceNoCompProgressive";
|
||||
import insuranceOEMAllstateTests from "./InsuranceOEMAllState";
|
||||
import cashRepairInShopAfterPayTests from "./CashRepairInShopAfterPay";
|
||||
import cashRepairInShopPayPalTests from "./CashRepairInShopPayPal";
|
||||
import cashReplaceMultiSlidingGlassDropoffTests from "./CashReplaceMultiSlidingGlassDropoff";
|
||||
|
|
@ -82,6 +83,7 @@ const allStandardTests = [
|
|||
{name: "InsuranceITAC21stCentury", tests: insuranceITAC21stCenturyTests},
|
||||
{name: "InsuranceNoCompProgressive", tests: insuranceNoCompProgressiveTests},
|
||||
// {name: "InsuranceBigTruckVerified", tests: insuranceBigTruckVerifiedTests},
|
||||
{name: "InsuranceOEMAllstate", tests: insuranceOEMAllstateTests},
|
||||
{name: "InsuranceUnverified", tests: insuranceUnverifiedTests},
|
||||
// {name: "InsuranceGeico", tests: insuranceGeicoTests},
|
||||
// {name: "InsuranceITACOptimizedPriceValidationAllState", tests: insuranceITACOptimizedPriceValidationAllStateTests}
|
||||
|
|
@ -323,7 +325,7 @@ async function runWorkflow(page: Page, testCase: TestCase) {
|
|||
}
|
||||
|
||||
export async function handleInsuranceFlow(testCase: TestCase) {
|
||||
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, flow } = testCase.testData;
|
||||
const { isPolicyFound, isPolicyDriver, endorsements, isRecalVehicle, isCashInsuranceFlow, isForcedOEM, flow } = testCase.testData;
|
||||
|
||||
// Check if the insurance policy has endorsements
|
||||
const hasEndorsements = endorsements && endorsements.length > 0;
|
||||
|
|
@ -332,6 +334,13 @@ export async function handleInsuranceFlow(testCase: TestCase) {
|
|||
let insuranceCompanyPage = testCase.pages.insuranceCompanyPage;
|
||||
await insuranceCompanyPage.handleInsuranceCompanyPage(testCase.testData);
|
||||
|
||||
// Handle OEM scenario
|
||||
if (isForcedOEM) {
|
||||
// User should be on heritage problem glass questions page after selecting Allstate on insurance company page
|
||||
let HeritageProblemGlassQuestionsPage = testCase.pages.heritageProblemGlassQuestionsPage;
|
||||
await HeritageProblemGlassQuestionsPage.handleHeritageProblemGlassQuestionsPage(testCase.testData);
|
||||
}
|
||||
|
||||
// Handle ccPolicyInfoPage
|
||||
let ccPolicyInfoPage = testCase.pages.ccPolicyInfoPage;
|
||||
await ccPolicyInfoPage.handleCCPolicyInfoPage(testCase.testData);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//Imports here
|
||||
import { ITestData } from 'framework/TestData'
|
||||
import { ServiceLocation, PaymentType } from 'safelite-playwright-core';
|
||||
import { ServiceLocation, ServicePackage, PaymentType } from 'safelite-playwright-core';
|
||||
import { ITestCase } from '../framework/Typedefs'
|
||||
import { VehicleLookupType } from 'safelite-playwright-core';
|
||||
import { getDefaultTestData, setFakerSeedFromTestName } from 'safelite-playwright-core';
|
||||
|
|
@ -29,7 +29,8 @@ const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
|
|||
postalCode: '21237'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
servicePackage: ServicePackage.Premium,
|
||||
// Override vehicle details
|
||||
vehicleDetails: {
|
||||
...getDefaultTestData().vehicleDetails!,
|
||||
|
|
@ -59,7 +60,8 @@ const cashReplaceDynamicRecalMobileData: Partial<ITestData> = {
|
|||
|
||||
// Override payment details
|
||||
paymentDetails: {
|
||||
paymentType: PaymentType.PayAtService
|
||||
paymentType: PaymentType.PayAtService,
|
||||
promoCode: 'digitalrain'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const cashReplaceMultiSlidingGlassDropoffData: Partial<ITestData> = {
|
|||
appointmentDetails: {
|
||||
serviceLocation: ServiceLocation.DropOff,
|
||||
appointmentDate: getDefaultTestData().appointmentDetails?.appointmentDate,
|
||||
shopAddress: "6826 sawmill rd, Columbus, OH 43235"
|
||||
shopAddress: "1343 cameron ave, lewis center, oh 43035"
|
||||
},
|
||||
|
||||
// Override vehicle details
|
||||
|
|
|
|||
88
playwright-tests/tests/InsuranceOEMAllState.ts
Normal file
88
playwright-tests/tests/InsuranceOEMAllState.ts
Normal 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;
|
||||
|
|
@ -75,10 +75,10 @@ const lookupTypesToTest: LookupTestCase[] = [
|
|||
},
|
||||
customerDetails: {
|
||||
address: {
|
||||
street: '4076 Spectacle Drive',
|
||||
street: '4219 Turpin Ln',
|
||||
city: 'Columbus',
|
||||
state: 'OH',
|
||||
postalCode: '59261',
|
||||
postalCode: '43230',
|
||||
country: 'United States'
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ body {
|
|||
debug: true,
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
name: 'SysTest',
|
||||
hostName: 'www-test2.safelite.com',
|
||||
apiHostname: 'digitalapi.test.safelite.io',
|
||||
debug: true,
|
||||
|
|
@ -142,83 +142,178 @@ body {
|
|||
return match;
|
||||
}
|
||||
|
||||
function getExperimentSettingsFromVuex(vuexData) {
|
||||
const experiments = vuexData?.applicationUser?.experiments;
|
||||
|
||||
if(experiments) {
|
||||
return experiments
|
||||
.filter((e) => !!e.isActive)
|
||||
.map((e) => e.settings)
|
||||
.reduce((prev, next) => Object.assign(prev, next), {}) ?? {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function extractDataFromVuex(existingVuexData) {
|
||||
let bailoutInfo = [];
|
||||
let headerInfo = [];
|
||||
|
||||
try {
|
||||
// =============== Collect diagnostic data:
|
||||
bailoutInfo = [];
|
||||
|
||||
// App & Site Name
|
||||
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
|
||||
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
|
||||
|
||||
// Not included (yet) from heritage:
|
||||
// - Site ID
|
||||
// - Code
|
||||
// - Module
|
||||
// - Page
|
||||
|
||||
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
|
||||
|
||||
// - URL
|
||||
// - Server
|
||||
|
||||
const sessionId = getCookieValueByName('sid');
|
||||
bailoutInfo.push({ name: 'Session Id', value: sessionId });
|
||||
|
||||
const userAgent = window.navigator.userAgent;
|
||||
bailoutInfo.push({ name: 'User Agent', value: userAgent });
|
||||
|
||||
// - IP
|
||||
// - Session Log Sequence Number
|
||||
|
||||
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
|
||||
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
|
||||
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
|
||||
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
|
||||
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
|
||||
|
||||
// - Referral Insured Zip
|
||||
// - Referral Insured Service Zip
|
||||
|
||||
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
|
||||
|
||||
const vehicle = existingVuexData.order?.vehicle;
|
||||
const vehicleString = vehicle?.year
|
||||
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
|
||||
: null;
|
||||
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
|
||||
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
|
||||
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
|
||||
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
|
||||
|
||||
const environmentData = getCurrentEnvironmentData();
|
||||
const userIdCookieName = `FunnelUserId-${environmentData?.name}`;
|
||||
bailoutInfo.push({ name: 'User Id', value: getCookieValueByName(userIdCookieName) });
|
||||
|
||||
// - Parent Account Name
|
||||
// - Client GUID
|
||||
|
||||
// =============== Collect header data:
|
||||
headerInfo = [];
|
||||
|
||||
headerInfo.push({ name: 'X-Experiment-Data', value: JSON.stringify(getExperimentSettingsFromVuex(existingVuexData)) });
|
||||
headerInfo.push({ name: 'X-Application-Name', value: 'FixMyGlass' });
|
||||
|
||||
const sessionKeyCookieName = `FunnelSessionKey-${environmentData?.name}`;
|
||||
|
||||
headerInfo.push({ name: 'X-Session-Sequence-Number', value: getCookieValueByName(sessionKeyCookieName) });
|
||||
headerInfo.push({ name: 'X-Referral-Sequence-Number', value: existingVuexData?.order?.referralSequenceNumber });
|
||||
headerInfo.push({ name: 'X-Enterprise-Order-Number', value: existingVuexData?.order?.eon });
|
||||
headerInfo.push({ name: 'log-enabled', value: existingVuexData?.applicationUser?.loggingOption ?? false });
|
||||
} finally {
|
||||
return {
|
||||
headerInfo: headerInfo,
|
||||
bailoutInfo: bailoutInfo
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function generateRequestHeader(headerInfo) {
|
||||
let headers = {};
|
||||
|
||||
headerInfo.forEach(
|
||||
keyPair => {
|
||||
headers[keyPair.name] = keyPair.value;
|
||||
}
|
||||
);
|
||||
|
||||
headers['X-Transaction-Id'] = crypto.randomUUID();
|
||||
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function getFieldWithName(info, name) {
|
||||
const match = info?.find(
|
||||
(row) => row.name === name
|
||||
);
|
||||
|
||||
return match?.value;
|
||||
}
|
||||
|
||||
function clearApplicationData() {
|
||||
window.localStorage.removeItem('vuex');
|
||||
|
||||
const environmentData = getCurrentEnvironmentData();
|
||||
const cookieNames = [
|
||||
`FunnelUserId-${environmentData?.name}`,
|
||||
`FunnelSessionKey-${environmentData?.name}`,
|
||||
`FunnelSessionInfo-${environmentData?.name}`,
|
||||
`sid`,
|
||||
`dxdev`,
|
||||
];
|
||||
|
||||
cookieNames.forEach(
|
||||
(cookieName) => {
|
||||
let cookieToAdd = `${cookieName}=undefined; path=/; domain=${environmentData.hostName}; max-age=0`;
|
||||
document.cookie = cookieToAdd;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
// =============== Check for session info:
|
||||
const existingVuexDataJSON = window.localStorage.getItem('vuex');
|
||||
const existingVuexData = existingVuexDataJSON ? JSON.parse(existingVuexDataJSON) : null;
|
||||
const existingBailoutInfoJSON = window.sessionStorage.getItem('bailoutInfo');
|
||||
const existingBailoutInfo = existingBailoutInfoJSON ? JSON.parse(existingBailoutInfoJSON) : null;
|
||||
const existingHeaderInfoJSON = window.sessionStorage.getItem('headerInfo');
|
||||
const existingHeaderInfo = existingHeaderInfoJSON ? JSON.parse(existingHeaderInfoJSON) : null;
|
||||
|
||||
console.log(`================ VUEX DATA`);
|
||||
console.log(existingVuexData);
|
||||
console.log(`================ PRIOR BAILOUT DATA`);
|
||||
console.log(existingBailoutInfo);
|
||||
console.log(`================ PRIOR HEADER DATA`);
|
||||
console.log(existingHeaderInfo);
|
||||
|
||||
let bailoutInfo = null;
|
||||
let headerInfo = null;
|
||||
|
||||
if(existingVuexData) {
|
||||
try {
|
||||
// =============== Collect diagnostic data:
|
||||
bailoutInfo = [];
|
||||
const results = extractDataFromVuex(existingVuexData);
|
||||
bailoutInfo = results.bailoutInfo;
|
||||
headerInfo = results.headerInfo;
|
||||
|
||||
// App & Site Name
|
||||
bailoutInfo.push({ name: 'App Name', value: 'FixMyGlass' });
|
||||
bailoutInfo.push({ name: 'Site Name', value: 'SafeliteDotcom' });
|
||||
clearApplicationData();
|
||||
|
||||
// Not included (yet) from heritage:
|
||||
// - Site ID
|
||||
// - Code
|
||||
// - Module
|
||||
// - Page
|
||||
|
||||
bailoutInfo.push({ name: 'Timestamp', value: `${new Date()}` });
|
||||
|
||||
// - URL
|
||||
// - Server
|
||||
|
||||
const sessionId = getCookieValueByName('sid');
|
||||
bailoutInfo.push({ name: 'Session Id', value: sessionId });
|
||||
|
||||
const userAgent = window.navigator.userAgent;
|
||||
bailoutInfo.push({ name: 'User Agent', value: userAgent });
|
||||
|
||||
// - IP
|
||||
// - Session Log Sequence Number
|
||||
|
||||
bailoutInfo.push({ name: 'Referral Seq Number', value: existingVuexData.order?.referralSequenceNumber });
|
||||
bailoutInfo.push({ name: 'Referral Number', value: existingVuexData.order?.referralNumber });
|
||||
bailoutInfo.push({ name: 'Referral Date', value: existingVuexData.order?.referralDate });
|
||||
bailoutInfo.push({ name: 'Referral Provider Number', value: existingVuexData.order?.serviceLocation?.provider?.providerNumber });
|
||||
bailoutInfo.push({ name: 'Referral CTU', value: existingVuexData.order?.serviceLocation?.provider?.address?.zipCodeCtu });
|
||||
|
||||
// - Referral Insured Zip
|
||||
// - Referral Insured Service Zip
|
||||
|
||||
bailoutInfo.push({ name: 'Referral CarID', value: existingVuexData.order?.vehicle?.carId });
|
||||
|
||||
const vehicle = existingVuexData.order?.vehicle;
|
||||
const vehicleString = vehicle?.year
|
||||
? `${vehicle.year} ${vehicle.make} ${vehicle.model} - ${vehicle.style}`
|
||||
: null;
|
||||
bailoutInfo.push({ name: 'Referral Vehicle', value: vehicleString });
|
||||
bailoutInfo.push({ name: 'Work Order Number', value: existingVuexData.order?.workOrderNumber });
|
||||
bailoutInfo.push({ name: 'Work Order ID', value: existingVuexData.order?.workOrderId });
|
||||
bailoutInfo.push({ name: 'Parent Account Number', value: existingVuexData.order?.payment?.parentAccountNumber });
|
||||
|
||||
// - Parent Account Name
|
||||
// - Client GUID
|
||||
} finally {
|
||||
// If any information gathered, write to session storage.
|
||||
if(bailoutInfo) {
|
||||
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
|
||||
}
|
||||
|
||||
// Then *always* clear vuex data.
|
||||
window.localStorage.removeItem('vuex');
|
||||
if(bailoutInfo) {
|
||||
window.sessionStorage.setItem('bailoutInfo', JSON.stringify(bailoutInfo));
|
||||
}
|
||||
} else if(existingBailoutInfo) {
|
||||
// Proceed with prior data.
|
||||
bailoutInfo = existingBailoutInfo;
|
||||
|
||||
if(headerInfo) {
|
||||
window.sessionStorage.setItem('headerInfo', JSON.stringify(headerInfo));
|
||||
}
|
||||
} else {
|
||||
bailoutInfo = existingBailoutInfo ?? [];
|
||||
headerInfo = existingHeaderInfo ?? [];
|
||||
}
|
||||
|
||||
const environmentInfo = getCurrentEnvironmentData();
|
||||
|
|
@ -275,11 +370,11 @@ body {
|
|||
);
|
||||
const entryString = `User encountered bailout page.\n${new Date()}\n${infoString ?? 'No information recoverable'}`;
|
||||
|
||||
const headers = generateRequestHeader(headerInfo);
|
||||
|
||||
const request = new Request(endpointUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
entry: entryString,
|
||||
}),
|
||||
|
|
@ -292,6 +387,38 @@ body {
|
|||
console.error(`=== ERROR SENDING LOGGING`);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
const endpointUrl = `https://${apiHostname}/analytics/api/v1/analytics/log-page-view`;
|
||||
|
||||
const headers = generateRequestHeader(headerInfo);
|
||||
|
||||
const payload = {
|
||||
AppName: "FixMyGlass",
|
||||
action: "",
|
||||
applicationName: "FixMyGlassNextGen",
|
||||
event: "ENTRY",
|
||||
experimentsForUser: [],
|
||||
pageName: "static-error",
|
||||
parentAccountNumber: getFieldWithName(bailoutInfo, 'Parent Account Number') ?? 0,
|
||||
referralSequenceNumber: getFieldWithName(bailoutInfo, 'Referral Seq Number'),
|
||||
sessionId: getFieldWithName(bailoutInfo, 'Session Id'),
|
||||
sessionKey: getFieldWithName(headerInfo, 'X-Session-Sequence-Number'),
|
||||
shouldUseSessionId: false,
|
||||
userId: getFieldWithName(bailoutInfo, 'User Id'),
|
||||
};
|
||||
|
||||
const request = new Request(endpointUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const response = await fetch(request);
|
||||
} catch(e) {
|
||||
console.error(`=== ERROR SENDING PAGEVIEW`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@
|
|||
'AccountService': {
|
||||
baseUrl: apiUrl + '/account/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/account/insurance-companies', method: 'GET' },
|
||||
{ path: '/account/account-details/373341', method: 'GET' },
|
||||
{ path: '/account/bill-to-account-number?ParentAccountNumber=373341&ProviderNumber=01820&IsItac=false&TypeOfClaim=Personal&LineOfBusiness=Commercial', method: 'GET' }
|
||||
|
|
@ -518,7 +518,7 @@
|
|||
'LocationService': {
|
||||
baseUrl: apiUrl + '/location/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/location/zip/43015/Replace', method: 'GET' },
|
||||
{ path: '/location/alert-reasons/03357', method: 'GET' },
|
||||
{ path: '/location/providers/43015/Replace/100/167132/true/CR00046193/false', method: 'GET' },
|
||||
|
|
@ -528,7 +528,7 @@
|
|||
'VehicleService': {
|
||||
baseUrl: apiUrl + '/vehicle/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/vehicle/years', method: 'GET' },
|
||||
{ path: '/vehicle/makes/1997', method: 'GET' },
|
||||
{ path: '/vehicle/Models/1997/Toyota', method: 'GET' },
|
||||
|
|
@ -539,13 +539,12 @@
|
|||
'PartService': {
|
||||
baseUrl: apiUrl + '/parts/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/parts/damage-options/CR00046193', method: 'GET' },
|
||||
{ path: '/parts/rain-repel', method: 'GET' },
|
||||
{ path: '/parts/parts-or-questions', method: 'POST', body: {"carId":"CR00046193","glassPieces":[{"location":"Windshield","name":"Single"}],"zip":"43015","vin":null,"serviceType":null,"referralSeqNumber":environmentContext.referralNumber,"AppName":"FixMyGlass"} },
|
||||
{ path: '/parts/wipers/CR00046193/43015', method: 'GET' },
|
||||
{ path: '/parts/mobile-premium-fee/Cash/Replace', method: 'GET' },
|
||||
{ path: '/parts/service-package-discount', method: 'POST', body: {"glassPieces":[{"location":"Windshield","name":"Single"}],"damageType":"Replace","AppName":"FixMyGlass"} },
|
||||
{ path: '/parts/mobile-fee/?serviceType=Install&facilityType=Mobile&parentAccountNumber=167132&billToAccountNumber=87291&providerNumber=03357&isItacOptimized=false&zipCode=43015', method: 'GET' },
|
||||
]
|
||||
},
|
||||
|
|
@ -553,7 +552,7 @@
|
|||
'PriceService': {
|
||||
baseUrl: apiUrl + '/price/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/price/taxed-order-items?ParentAccountNumber=167132&BillToAccountNumber=87291&ProviderNumber=003357&AppointmentType=Mobile&ServiceLocation.City=asdf&ServiceLocation.State=OH&ServiceLocation.ZipCode=43015&lineItems[0].partNumber=FW02018GBNN&lineItems[0].laborAmount=60&lineItems[0].kitPrice=0&lineItems[0].sellingPrice=297.33&lineItems[1].partNumber=GGG%202018&lineItems[1].laborAmount=0&lineItems[1].kitPrice=0&lineItems[1].sellingPrice=13.4&lineItems[2].partNumber=RECYCLE%20FEE&lineItems[2].laborAmount=39.99&lineItems[2].kitPrice=0&lineItems[2].sellingPrice=0&ServerData=' + encodeURIComponent(environmentContext.serverData), method: 'GET' },
|
||||
{ path: '/price/order-items', method: 'POST', body: { "parentAccountNumber": 167132, "billToAccountNumber": "87291", "providerNumber": "03357", "carId": "CR00046193", "year": 1997, "make": "Toyota", "model": "Camry", "eon": environmentContext.eon, "state": "OH", "referralNumber": environmentContext.referralNumber, "referralSequenceNumber": environmentContext.referralNumber, "zipCode": "43015", "serviceZipCode": "43015", "referralDate": "2025-10-29T10:55:27.18", "isReplacement": true, "deductible": 0, "coverageStatus": null, "lineItems": [ { "partNumber": "RECYCLE FEE" }, { "partNumber": "FW02018GBNN" }, { "partNumber": "GGG 2018" }, { "partNumber": "SBB19" }, { "partNumber": "SBB21" } ], "serverData": "", "AppName": "FixMyGlass" } },
|
||||
{ path: '/price/order-promo', method: 'POST', body: {"promoCode":"WEBSL45","addableVaps":[{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":null}],"order":{"appointmentType":"Mobile","carId":"CR00046193","correlationId":"b0261e48-1111-44fc-abc9-34b41380687c","eon":environmentContext.eon,"isInsurance":false,"isRepair":false,"glassToReplace":[{"location":"Windshield","name":"Single"}],"lineItemsOnOrder":[{"partNumber":"FW02018GBNN","basePartNumber":"FW02018","description":"solar","color":"Green Tint, Blue Shade","partType":"WINDSHIELD","canSafeliteRecalibrate":false,"requiresRecalibration":false,"requiresCapabilityQuestions":false,"recalibrationType":null,"childParts":[{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94}],"kitPrice":0,"sellingPrice":368.94,"laborAmount":60,"id":"4326cee6-b39e-41ff-bb29-df9220e32298","isChildPart":false,"salesTax":30.02},{"partNumber":"GGG 2018","safelitePartNumber":"GGG 2018","id":"329cf0ec-8106-4e62-ab66-de0da8f18a1d","isChildPart":true,"laborAmount":0,"sellingPrice":13.4,"kitPrice":0,"salesTax":0.94},{"partNumber":"RECYCLE FEE","description":null,"partType":"REPLACE FEE","isInsurable":null,"isChildPart":false,"laborAmount":39.99,"sellingPrice":0,"kitPrice":0,"salesTax":2.8,"id":"3771aed5-0de1-4f0e-b1ad-394f73e08f38","cartItemType":"REPLACE FEE"},{"partNumber":"MOBILE FEE","description":null,"partType":"MOBILE FEE","isInsurable":true,"isChildPart":false,"laborAmount":0,"sellingPrice":0,"kitPrice":0,"salesTax":0,"id":"2bc936ec-ef5e-4fe2-95fb-e946792b1ae5"},{"partNumber":"SBB19","description":"SAFELITE BEAM BLADE 19","partType":"FRONT WIPER","isInsurable":null,"id":"4db19a8b-739f-4f7b-983e-f6308c5f5901","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"SBB21","description":"SAFELITE BEAM BLADE 21","partType":"FRONT WIPER","isInsurable":null,"id":"e27ae20a-3a60-4fdf-a738-4b56d2e761cb","isChildPart":false,"laborAmount":0,"sellingPrice":34.99,"kitPrice":0,"salesTax":2.45,"cartItemType":"FRONT WIPERS"},{"partNumber":"RAIN REPEL","description":null,"partType":"RAIN REPEL","isInsurable":null,"id":"ccb0865d-1c0a-4be7-8605-bc7b9cb3d221","isChildPart":false,"laborAmount":0,"sellingPrice":44.99,"kitPrice":0,"salesTax":3.15,"cartItemType":"RAIN REPEL"}],"parentAccountNumber":"167132","referralSequenceNumber":environmentContext.referralNumber,"serviceState":"OH","serverData":environmentContext.serverData,"vehicleYear":"1997","zipCodeOrProviderCtu":"03357"},"AppName":"FixMyGlass"} },
|
||||
|
|
@ -563,7 +562,7 @@
|
|||
'ScheduleService': {
|
||||
baseUrl: apiUrl + '/schedule/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/schedule/mobile-time-slots', method: 'POST', body: {"startDate":startDate,"endDate":endDate,"applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"SBB19","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"SBB21","partType":"FRONT WIPER","isGlassPart":false},{"partNumber":"RAIN REPEL","partType":"RAIN REPEL","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"zipCode":"43015","AppName":"FixMyGlass"} },
|
||||
{ path: '/schedule/shop-time-slots', method: 'POST', body: {"providerNumber":"003417","startDate":startDate,"endDate":endDate,"shopAppointmentType":"InshopOrDropoff","applicationName":"FixMyGlass","parentAccountNumber":"167132","carId":"CR00046193","lineItems":[{"partNumber":"RECYCLE FEE","partType":"REPLACE FEE","isGlassPart":false},{"partNumber":"FW02018GBNN","partType":"WINDSHIELD","isGlassPart":true},{"partNumber":"GGG 2018","isGlassPart":false}],"glassPieces":[{"location":"Windshield","name":"Single"}],"eon":environmentContext.eon,"billToAccountNumber":"87291","coverage":{"status":null,"deductible":0,"additionalAuthFlag":null},"partSelection":{"hasAnsweredPartQuestions":false,"hasAnsweredMoldingQuestions":false,"hasAnsweredCapabilityQuestions":false,"hasManuallySelectedParts":false},"vehicle":{"year":1997,"make":"Toyota","model":"Camry","style":"4 door sedan","vin":""},"AppName":"FixMyGlass"} }
|
||||
]
|
||||
|
|
@ -571,25 +570,25 @@
|
|||
'AnalyticsService': {
|
||||
baseUrl: apiUrl + '/analytics/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
]
|
||||
},
|
||||
'ExperimentService': {
|
||||
baseUrl: apiUrl + '/experiments/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
]
|
||||
},
|
||||
'OrderService': {
|
||||
baseUrl: apiUrl + '/order/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
]
|
||||
},
|
||||
'ContentService': {
|
||||
baseUrl: apiUrl + '/content/api/v1',
|
||||
endpoints: [
|
||||
//{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/healthcheck', method: 'GET' },
|
||||
{ path: '/content/fmg/vehicle', method: 'GET' },
|
||||
]
|
||||
}
|
||||
|
|
@ -674,7 +673,7 @@
|
|||
}
|
||||
|
||||
function updateAtAGlanceText() {
|
||||
const healthyCount = Object.values(healthData).filter(service => service.status === 'healthy').length;
|
||||
const healthyCount = Object.values(healthData).filter(service => (service.status === 'healthy' || service.status === 'degraded')).length;
|
||||
const totalCount = Object.values(healthData).length;
|
||||
let text = 'Healthy';
|
||||
let className = 'stat-pill stat-pill-healthy';
|
||||
|
|
@ -763,14 +762,15 @@
|
|||
// Determine overall service status
|
||||
const endpoints = healthData[serviceName].endpoints;
|
||||
const healthyCount = endpoints.filter(ep => !isDegraded(ep) && !isUnhealthy(ep)).length;
|
||||
const unhealthyCount = endpoints.filter(ep => isUnhealthy(ep)).length;
|
||||
const totalCount = endpoints.length;
|
||||
|
||||
if (healthyCount === totalCount) {
|
||||
healthData[serviceName].status = 'healthy';
|
||||
} else if (healthyCount > 0) {
|
||||
healthData[serviceName].status = 'degraded';
|
||||
} else {
|
||||
} else if (unhealthyCount > 0) {
|
||||
healthData[serviceName].status = 'unhealthy';
|
||||
} else {
|
||||
healthData[serviceName].status = 'degraded';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ const GaCategories = {
|
|||
EVOX: "Evox",
|
||||
APPOINTMENT: "Appointment",
|
||||
SERVICE_LOCATION: "service-location",
|
||||
CONFIRMATION_CLICKED: "confirmation clicked",
|
||||
RECAL_DISCLAIMER: "Recalibration disclaimer",
|
||||
};
|
||||
|
||||
const GaActions = {
|
||||
|
|
@ -26,6 +28,9 @@ const GaActions = {
|
|||
MOBILE_AVAILABLE: "mobile_available",
|
||||
SHOPS_FIRST_DISPLAYED: "shops_first_displayed",
|
||||
MORE_LOCATIONS_CLICKED: "more_locations_clicked",
|
||||
YES: "yes",
|
||||
NO: "no",
|
||||
ACKNOWLEDGED: "Acknowledged",
|
||||
};
|
||||
|
||||
const GaLabels = {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const applicationConfig = {
|
|||
PIA_ERROR_URL:
|
||||
location.protocol + "//" + location.host + "/fmg/payment-pia-return?src=concept-funnel",
|
||||
CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/confirmation",
|
||||
PIA_ADYEN_RETURN_URL: location.protocol + "//" + location.host + "/fmg/virtual/payment/return",
|
||||
GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE",
|
||||
YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60",
|
||||
OUTLOOK_CALENDAR:
|
||||
|
|
@ -31,6 +32,8 @@ const applicationConfig = {
|
|||
COOKIE_NAME_LENGTH_MAX_LIMIT: 44,
|
||||
SESSION_TIMEOUT_MINUTES: process.env.__VUE_APP_SESSION_TIMEOUT_MINUTES__,
|
||||
RETURN_USER_PAGE: "/fmg/return-user",
|
||||
ADYEN_CLIENT_KEY: process.env.__VUE_APP_ADYEN_CLIENT_KEY__,
|
||||
ADYEN_ENVIRONMENT: process.env.__VUE_APP_ADYEN_ENVIRONMENT__,
|
||||
};
|
||||
|
||||
export { applicationConfig };
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const cartItemCategories = {
|
|||
GLASS_PARTS: "glassParts",
|
||||
SUPPORTING_ITEMS: "supportingItems",
|
||||
PROMOS: "promos",
|
||||
SERVICE_PACKAGE_DISCOUNT: "servicePackageDiscount",
|
||||
QUOTE_PAGE_DISCOUNT: "quotePageDiscount",
|
||||
};
|
||||
|
||||
export { cartItemCategories };
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const cartItemTypes = {
|
|||
PROMOS: "PROMOS",
|
||||
EARLY_BIRD: "EARLY BIRD",
|
||||
SUPPLIES_REPAIR: "SUPPLIES-REPAIR",
|
||||
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
|
||||
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
|
||||
DONATION: "DONATION",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -76,10 +76,6 @@ const endpoints = {
|
|||
url: "/parts/api/v1/parts/pricing-by-day-part-number",
|
||||
method: "GET",
|
||||
},
|
||||
GetServicePackageDiscountPart: {
|
||||
url: "/parts/api/v1/parts/service-package-discount",
|
||||
method: "POST",
|
||||
},
|
||||
GetServiceabilityDetails: {
|
||||
url: "/location/api/v1/location/serviceability-details",
|
||||
method: "GET",
|
||||
|
|
@ -124,6 +120,14 @@ const endpoints = {
|
|||
url: "/order/api/v1/order/save-session/fmg",
|
||||
method: "POST",
|
||||
},
|
||||
SubmitOrder: {
|
||||
url: "/order/api/v1/order/submit-order",
|
||||
method: "POST",
|
||||
},
|
||||
SubmitOrderForPia: {
|
||||
url: "/order/api/v1/order/submit-order-for-pia",
|
||||
method: "POST",
|
||||
},
|
||||
LoadSession: {
|
||||
url: "/order/api/v1/order/load-session",
|
||||
method: "POST",
|
||||
|
|
@ -154,7 +158,7 @@ const endpoints = {
|
|||
},
|
||||
TaxOrderItems: {
|
||||
url: "/price/api/v1/price/taxed-order-items",
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
},
|
||||
LogExperimentExposureIfAssigned: {
|
||||
url: "/experiments/api/v1/experiments/log-exposure",
|
||||
|
|
@ -208,6 +212,18 @@ const endpoints = {
|
|||
url: "/order/api/v1/order/add-donation-part",
|
||||
method: "POST",
|
||||
},
|
||||
GetOemAfterMarketParts: {
|
||||
url: "/parts/api/v1/parts/oem-after-market-parts",
|
||||
method: "POST",
|
||||
},
|
||||
InitializeAdyenPayment: {
|
||||
url: "/payment/api/v1/payment/payment/session/initialize-session",
|
||||
method: "POST",
|
||||
},
|
||||
GetAdyenSessionResult: {
|
||||
url: "/payment/api/v1/payment/payment/session/session-result",
|
||||
method: "POST",
|
||||
},
|
||||
};
|
||||
|
||||
export { endpoints };
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ const experimentSettings = {
|
|||
SUPPRESS_VIN_CAPTURE: "SuppressVinCapture",
|
||||
DISPLAY_AVAILABILITY_INDICATORS: "DisplayAvailabilityIndicators",
|
||||
PIA_EXPERIENCE: "PIA Experience",
|
||||
CONDITIONAL_PROVIDER_NUMBERS: "ConditionalProviderNumbers",
|
||||
CONDITIONAL_PROVIDER_NUMBERS_2: "ConditionalProviderNumbers2",
|
||||
CONDITIONAL_PROVIDER_SHUTOFF_DATE: "ConditionalProviderShutoffDate",
|
||||
PIA_INSURANCE: "DisplayPIAInsurance",
|
||||
SUBMIT_ORDER_ENABLE_PIA: "SubmitOrder_Enable_PIA",
|
||||
IS_EMAIL_OPTIONAL: "isEmailOptional",
|
||||
|
|
@ -33,6 +36,8 @@ const experimentSettings = {
|
|||
SHOW_PM_MOBILE_DAYS: "Show_PMmobileDays",
|
||||
SHOW_NO_PM_MOBILE_DAYS: "Show_NoPMmobileDays",
|
||||
SHOW_NO_MOBILE_AVAILABLE_DAYS: "Show_NoMobileAvailableDays",
|
||||
OFFER_OEM: "OfferOem",
|
||||
USE_ADYEN_PAYMENT: "UseAdyenPayment",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ const partTypeStrings = {
|
|||
REPAIR_FEE: "REPAIR FEE",
|
||||
EARLY_BIRD: "EARLY BIRD",
|
||||
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
|
||||
QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT",
|
||||
DONATION: "DONATION",
|
||||
WINDSHIELD: "WINDSHIELD",
|
||||
};
|
||||
|
||||
export { partTypeStrings };
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ export const pageProgressMapper = {
|
|||
percent: 98.5,
|
||||
step: 4,
|
||||
},
|
||||
"payment-adyen": {
|
||||
percent: 98.5,
|
||||
step: 4,
|
||||
},
|
||||
confirmation: {
|
||||
percent: 100,
|
||||
step: 4,
|
||||
|
|
|
|||
80
src/constants/quote-page-discounts.js
Normal file
80
src/constants/quote-page-discounts.js
Normal 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,
|
||||
},
|
||||
];
|
||||
|
|
@ -11,6 +11,8 @@ const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
|
|||
|
||||
const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
|
||||
|
||||
const RECAL_ACK_YES = "Yes";
|
||||
|
||||
const RouteCodeFlags = {
|
||||
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
|
||||
OVERNIGHT_DROP_OFF: "OVERNIGHT DROP OFF",
|
||||
|
|
@ -31,4 +33,5 @@ export {
|
|||
PRICING_BY_DAY_PART_TYPE,
|
||||
RouteCodeFlags,
|
||||
cmsWidgetFieldMappings,
|
||||
RECAL_ACK_YES,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const storeActions = {
|
|||
GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu",
|
||||
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
|
||||
GET_PARTS: "getParts",
|
||||
GET_OEM_AFTERMARKET_PARTS: "getOemAfterMarketParts",
|
||||
GET_WIPERS: "getWipers",
|
||||
GET_RAIN_REPEL: "getRainRepel",
|
||||
GET_SUPPORTING_ITEMS: "getSupportingItems",
|
||||
|
|
@ -32,7 +33,6 @@ const storeActions = {
|
|||
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
|
||||
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
||||
GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
|
||||
GET_SERVICE_PACKAGE_DISCOUNT_PART: "getServicePackageDiscountPart",
|
||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||
GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
|
||||
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
|
||||
|
|
@ -111,6 +111,8 @@ const storeActions = {
|
|||
SAVE_PAYPAL_TOKEN: "savePaypalToken",
|
||||
SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount",
|
||||
SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn",
|
||||
SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling",
|
||||
SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected",
|
||||
SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable",
|
||||
|
||||
CREATE_SUBMITTED_STATE: "createSubmittedState",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ const storeMutations = {
|
|||
LOCK_TOKEN: "updateLockToken",
|
||||
UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount",
|
||||
UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn",
|
||||
UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "updateIsRecalAcknowledgedForScheduling",
|
||||
UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected",
|
||||
UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable",
|
||||
UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal",
|
||||
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ export default {
|
|||
const response = config.getSelectableDatesCallback({
|
||||
startDateString: initialViewStartDate,
|
||||
endDateString: initialViewEndDate,
|
||||
providerNumber: config.providerNumber,
|
||||
inshopProviderNumber: config.inshopProviderNumber,
|
||||
zipCode: config.zipCode,
|
||||
includeMobileTimeSlots: config.includeMobileTimeSlots,
|
||||
includeInshopTimeSlots: config.includeInshopTimeSlots,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
<div class="modal-footer">
|
||||
<slot name="modal-footer-slot"></slot>
|
||||
<modalButtonMain
|
||||
v-if="!isFooterButtonSuppressed"
|
||||
:isPrimary="isFooterButtonPrimary"
|
||||
class="w-100 modal-footer-button"
|
||||
:id="modalId + '-modalbtn'"
|
||||
|
|
@ -67,6 +68,7 @@ export default {
|
|||
staticBackdrop: Boolean,
|
||||
footerButtonDisabled: Boolean,
|
||||
isFooterButtonPrimary: Boolean,
|
||||
isFooterButtonSuppressed: Boolean,
|
||||
onModalOpenedCallback: {
|
||||
type: Function,
|
||||
},
|
||||
|
|
@ -158,6 +160,10 @@ export default {
|
|||
return this.staticBackdrop ? "static" : "true";
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||
this.closeModal();
|
||||
},
|
||||
components: {
|
||||
modalButtonMain,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -145,9 +145,7 @@ export default {
|
|||
return "$" + decimalPrice.toFixed(2);
|
||||
},
|
||||
hasPackageDiscount() {
|
||||
return (
|
||||
this.additionalButtonData.servicePackageDiscount && this.additionalButtonData.Text
|
||||
);
|
||||
return this.additionalButtonData.hasDiscount && this.additionalButtonData.Text;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
class="package-label pricing-by-day-pkg-lbl"
|
||||
:class="[
|
||||
this.buttonLabelSubCopy ? 'has-subheader' : '',
|
||||
!this.additionalButtonData.servicePackageDiscount ? 'adjust-top' : '',
|
||||
!this.additionalButtonData.hasDiscount ? 'adjust-top' : '',
|
||||
]"
|
||||
for="testradio">
|
||||
<div class="package-specs">
|
||||
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
|
||||
<div v-if="this.additionalButtonData.hasDiscount" class="row">
|
||||
<div class="col md-6">
|
||||
<p class="m-0">
|
||||
<span v-html="this.buttonLabel"></span>
|
||||
|
|
@ -84,10 +84,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
this.additionalButtonData.servicePackageDiscount &&
|
||||
this.additionalButtonData.Text
|
||||
"
|
||||
v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
|
||||
class="special-save-box">
|
||||
<span v-html="this.additionalButtonData.Text"></span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ import { cartItemCategories } from "@/constants/cart-item-categories";
|
|||
import { cartItemTypes } from "@/constants/cart-item-types";
|
||||
import { coverageStatus, cartItemTypesCoveredByInsurance } from "@/constants/insurance";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { quotePageDiscountTable } from "@/constants/quote-page-discounts";
|
||||
|
||||
export default {
|
||||
name: "cart",
|
||||
|
|
@ -256,11 +257,11 @@ export default {
|
|||
|
||||
return subTotal;
|
||||
},
|
||||
getServicePackageDiscount() {
|
||||
const servicePackageDiscountLineItems = this.servicePackageDiscountCartItem;
|
||||
getQuotePageDiscount() {
|
||||
const quotePageDiscountLineItems = this.quotePageDiscountCartItem;
|
||||
let subTotal = 0;
|
||||
|
||||
subTotal += servicePackageDiscountLineItems?.subTotal ?? 0;
|
||||
subTotal += quotePageDiscountLineItems?.subTotal ?? 0;
|
||||
return subTotal;
|
||||
},
|
||||
getVapsCartItemsForSelectedPackage(packageName) {
|
||||
|
|
@ -303,7 +304,8 @@ export default {
|
|||
|
||||
getLineItemAmount(amount, useVerifyingText, category) {
|
||||
if (useVerifyingText) return this.verifyingCoverageText;
|
||||
return category == "promos" || category == "servicePackageDiscount"
|
||||
return category == cartItemCategories.PROMOS ||
|
||||
category == cartItemCategories.QUOTE_PAGE_DISCOUNT
|
||||
? "(" + this.currencyFormatter.format(amount * -1) + ")"
|
||||
: this.currencyFormatter.format(amount);
|
||||
},
|
||||
|
|
@ -316,16 +318,21 @@ export default {
|
|||
if (category == cartItemCategories.VAPS || category == cartItemCategories.PROMOS) {
|
||||
this.saveVaps(this.lineItems);
|
||||
// Check if service package discount should be removed after vaps change
|
||||
if (
|
||||
this.servicePackageDiscountCartItem &&
|
||||
this.discountPackageNames != this.packageLevel
|
||||
) {
|
||||
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
|
||||
(lineItemsToKeep) =>
|
||||
lineItemsToKeep.cartItemType !=
|
||||
this.servicePackageDiscountCartItem.cartItemType
|
||||
if (this.quotePageDiscountCartItem) {
|
||||
const existingLineItem = this.quotePageDiscountCartItem.lineItems.at(0);
|
||||
const discountInfo = quotePageDiscountTable.find(
|
||||
(info) => info.partNumber === existingLineItem?.partNumber
|
||||
);
|
||||
shouldSaveSupportingItems = true;
|
||||
const packageLevelForDiscount = discountInfo?.packageLevel;
|
||||
|
||||
if (this.packageLevel !== packageLevelForDiscount) {
|
||||
this.lineItems.supportingItems = this.lineItems.supportingItems.filter(
|
||||
(lineItemsToKeep) =>
|
||||
lineItemsToKeep.cartItemType !=
|
||||
this.quotePageDiscountCartItem.cartItemType
|
||||
);
|
||||
shouldSaveSupportingItems = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldSaveSupportingItems) {
|
||||
|
|
@ -483,8 +490,8 @@ export default {
|
|||
cartItems.push(this.mobileFeeCartItem);
|
||||
}
|
||||
|
||||
if (this.servicePackageDiscountCartItem) {
|
||||
cartItems.push(this.servicePackageDiscountCartItem);
|
||||
if (this.quotePageDiscountCartItem) {
|
||||
cartItems.push(this.quotePageDiscountCartItem);
|
||||
}
|
||||
|
||||
if (this.premiumAppointmentDiscountCartItem) {
|
||||
|
|
@ -500,12 +507,6 @@ export default {
|
|||
return cartItems;
|
||||
},
|
||||
},
|
||||
discountPackageNames() {
|
||||
const discountServicePackage = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.PROMO_ON_PACKAGE
|
||||
);
|
||||
return getDiscountedPackageName(discountServicePackage);
|
||||
},
|
||||
servicePackageTitleWidget() {
|
||||
const servicePackageNames = this.getCmsContent(
|
||||
this.servicePackageOptionsCmsName,
|
||||
|
|
@ -550,8 +551,8 @@ export default {
|
|||
}
|
||||
|
||||
packagePrice += this.getVapsPrice(this.packageLevel);
|
||||
if (this.servicePackageDiscountCartItem) {
|
||||
packagePrice -= this.getServicePackageDiscount();
|
||||
if (this.quotePageDiscountCartItem) {
|
||||
packagePrice -= this.getQuotePageDiscount();
|
||||
}
|
||||
|
||||
return packagePrice;
|
||||
|
|
@ -928,25 +929,25 @@ export default {
|
|||
}
|
||||
return cartItem;
|
||||
},
|
||||
servicePackageDiscountCartItemName() {
|
||||
quotePageDiscountCartItemName() {
|
||||
return this.getCmsContent("ServicePackageDiscountTextWidget", "Text");
|
||||
},
|
||||
servicePackageDiscountCartItem() {
|
||||
quotePageDiscountCartItem() {
|
||||
let cartItem = null;
|
||||
|
||||
const servicePackageDiscountLineItem = this.supportingItems.find(
|
||||
(lineItem) => lineItem.partType == partTypeStrings.SERVICE_PACKAGE_DISCOUNT
|
||||
const quotePageDiscountLineItem = this.supportingItems.find(
|
||||
(lineItem) => lineItem.partType == partTypeStrings.QUOTE_PAGE_DISCOUNT
|
||||
);
|
||||
|
||||
if (servicePackageDiscountLineItem) {
|
||||
if (quotePageDiscountLineItem) {
|
||||
cartItem = {
|
||||
name:
|
||||
this.servicePackageDiscountCartItemName +
|
||||
this.quotePageDiscountCartItemName +
|
||||
Math.abs(
|
||||
baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem)
|
||||
baseMixin.methods.getTotalLineItemPrice(quotePageDiscountLineItem)
|
||||
),
|
||||
category: cartItemCategories.SERVICE_PACKAGE_DISCOUNT,
|
||||
cartItemType: cartItemTypes.SERVICE_PACKAGE_DISCOUNT,
|
||||
category: cartItemCategories.QUOTE_PAGE_DISCOUNT,
|
||||
cartItemType: cartItemTypes.QUOTE_PAGE_DISCOUNT,
|
||||
isDisplayed: true,
|
||||
isRemovable: false,
|
||||
subTotal: 0,
|
||||
|
|
@ -955,15 +956,15 @@ export default {
|
|||
isCoveredByInsurance: false,
|
||||
};
|
||||
|
||||
servicePackageDiscountLineItem.cartItemType = cartItem.cartItemType;
|
||||
cartItem.lineItems.push(servicePackageDiscountLineItem);
|
||||
quotePageDiscountLineItem.cartItemType = cartItem.cartItemType;
|
||||
cartItem.lineItems.push(quotePageDiscountLineItem);
|
||||
|
||||
cartItem.subTotal +=
|
||||
(servicePackageDiscountLineItem.kitPrice ?? 0) +
|
||||
(servicePackageDiscountLineItem.laborAmount ?? 0) +
|
||||
(servicePackageDiscountLineItem.sellingPrice ?? 0);
|
||||
(quotePageDiscountLineItem.kitPrice ?? 0) +
|
||||
(quotePageDiscountLineItem.laborAmount ?? 0) +
|
||||
(quotePageDiscountLineItem.sellingPrice ?? 0);
|
||||
|
||||
cartItem.salesTax += servicePackageDiscountLineItem.salesTax ?? 0;
|
||||
cartItem.salesTax += quotePageDiscountLineItem.salesTax ?? 0;
|
||||
}
|
||||
|
||||
return cartItem;
|
||||
|
|
@ -972,7 +973,7 @@ export default {
|
|||
let cartItem = null;
|
||||
|
||||
const otherSupportingItems = this.supportingItems.filter((item) => {
|
||||
return item.partType != partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
|
||||
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
|
||||
});
|
||||
// Don't include fees they are part of the package total
|
||||
let otherSupportingItemsLineItems =
|
||||
|
|
|
|||
|
|
@ -75,17 +75,12 @@ export default {
|
|||
|
||||
:deep(a) {
|
||||
text-decoration: none;
|
||||
&.new-window-link {
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
margin: 0 0.75rem;
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
&.new-window-link,
|
||||
&.navigation-link {
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-family: UrbanistRegular, Arial, Helvetica, sans-serif;
|
||||
margin: 0 0.75rem;
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
font-weight: initial;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,6 @@ export default {
|
|||
modalWidgetName: String,
|
||||
pageName: String,
|
||||
},
|
||||
unmounted() {
|
||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||
this.modal?.closeModal();
|
||||
},
|
||||
computed: {
|
||||
buttonText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "SubheaderText");
|
||||
|
|
|
|||
|
|
@ -54,10 +54,6 @@ export default {
|
|||
mounted() {
|
||||
if (this.showSaveProgressPopup) this.modal.openModal();
|
||||
},
|
||||
unmounted() {
|
||||
// remove any modal effects before leaving page (e.g. user hits browser back button)
|
||||
this.modal.closeModal();
|
||||
},
|
||||
emits: ["close-save-progress-popup", "save-progress"], // <--- should remove oodles of warnings in dev tools
|
||||
props: {
|
||||
modelValue: Object,
|
||||
|
|
|
|||
168
src/helpers/adyen-helper.js
Normal file
168
src/helpers/adyen-helper.js
Normal 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";
|
||||
}
|
||||
|
|
@ -3,6 +3,14 @@ import { partTypeStrings } from "@/constants/part-type-strings";
|
|||
|
||||
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION];
|
||||
|
||||
export function anyPartWithRequiresRecalFlag(lineItems) {
|
||||
if (!lineItems) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lineItems.glassParts?.some((li) => li.requiresRecalibration === true);
|
||||
}
|
||||
|
||||
export function isRecalPartOrHasChildRecalPart(lineItem) {
|
||||
if (lineItem.childParts && lineItem.childParts.length > 0) {
|
||||
return isRecalPart(lineItem) || containsRecalParts(lineItem.childParts);
|
||||
|
|
|
|||
|
|
@ -254,6 +254,19 @@ export function getDiscountedPackageName(discountPackage) {
|
|||
return package_names[discountPackage] || null;
|
||||
}
|
||||
|
||||
export function getPackageLabel(packageName) {
|
||||
switch (packageName) {
|
||||
case packageNames.TIER_ONE:
|
||||
return externalParamPackageLabels.GLASS_ONLY;
|
||||
case packageNames.TIER_TWO:
|
||||
return externalParamPackageLabels.STANDARD;
|
||||
case packageNames.TIER_THREE:
|
||||
return externalParamPackageLabels.PREMIUM;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function maxTier(tierA, tierB) {
|
||||
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
|
||||
return packageNames.TIER_THREE;
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ export default {
|
|||
// await any pending save-sessions. if you don't, we will save the new parent account into vuex and then a pending async save-session response overwrites it
|
||||
await flushSaveSessionQueue();
|
||||
|
||||
await this.removeQPDLineItemIfExists();
|
||||
|
||||
await this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
|
||||
this.parentAccountNumber.toString(),
|
||||
|
|
@ -193,6 +195,17 @@ export default {
|
|||
handlePageShow() {
|
||||
window.location.reload();
|
||||
},
|
||||
async removeQPDLineItemIfExists() {
|
||||
const lineItems = deepClone(store.getters.order.lineItems);
|
||||
lineItems.supportingItems = baseMixin.methods.filterOutQuotePageDiscountPart(
|
||||
lineItems.supportingItems
|
||||
);
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
|
||||
lineItems.supportingItems,
|
||||
false
|
||||
);
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener("pageshow", this.handlePageShow);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@
|
|||
cmsWidgetName="PleaseProvideAddressWidget"
|
||||
class="provide-address-header" />
|
||||
<textBlock
|
||||
v-if="isMSRFeeApplicable()"
|
||||
cmsWidgetName="VehicleKeysMSRMessageWidget"
|
||||
class="keys-message" />
|
||||
<textBlock
|
||||
v-else
|
||||
cmsWidgetName="VehicleKeysMessageWidget"
|
||||
class="keys-message" />
|
||||
<mobileAddressQuestions
|
||||
|
|
@ -143,6 +148,9 @@ export default {
|
|||
getIsVehicleProtectedFromStore() {
|
||||
return store.getters.order.serviceLocation.isVehicleProtected;
|
||||
},
|
||||
isMSRFeeApplicable() {
|
||||
return store.getters.order.isMSRFeeApplicable;
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
await this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_MOBILE_DETAILS,
|
||||
|
|
|
|||
|
|
@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => {
|
|||
//Assert
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
storeActions.SAVE_IS_OEM_GLASS_SELECTED,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"savePartQuestionAnswers",
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -188,6 +188,8 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
|
||||
|
||||
// save to vuex store as order.damage.partQuestionAnswers (array)
|
||||
// used in GET_PARTS call following this one
|
||||
await this.dispatchStoreAction(
|
||||
|
|
|
|||
686
src/layouts/payment-adyen/payment-adyen.vue
Normal file
686
src/layouts/payment-adyen/payment-adyen.vue
Normal 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>
|
||||
|
|
@ -170,6 +170,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas
|
|||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
import { ErrorMessage } from "vee-validate";
|
||||
import { Field } from "vee-validate";
|
||||
import experimentMixin from "../../mixins/experiment-mixin";
|
||||
|
||||
defineRule("payment-method-required", required(errorMessages.OPTION_REQUIRED));
|
||||
defineRule("recal-ack-required", required(errorMessages.RECAL_ACK_REQUIRED));
|
||||
|
|
@ -609,10 +610,17 @@ export default {
|
|||
}
|
||||
|
||||
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.CLICKED_PAY_NOW,
|
||||
this.pageName
|
||||
|
||||
const shouldUseAdyen = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.USE_ADYEN_PAYMENT,
|
||||
"true"
|
||||
);
|
||||
|
||||
const scenarioName = shouldUseAdyen
|
||||
? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN
|
||||
: this.navigationScenarios.CLICKED_PAY_NOW;
|
||||
|
||||
this.$router.navigateWithoutSaving(scenarioName, this.pageName);
|
||||
},
|
||||
async evaluatePromosAndTaxItemsOnOrder() {
|
||||
//Revalidate promos if there are any inactive, or active promos
|
||||
|
|
@ -802,11 +810,51 @@ export default {
|
|||
const piaInsurance = this.getSettingValue(experimentSettings.PIA_INSURANCE);
|
||||
|
||||
if (this.isInsurance) {
|
||||
return piaInsurance === "true" && this.currentDeductible > 0;
|
||||
return (
|
||||
(piaInsurance === "true" || this.isConditionalPIAEnabled) &&
|
||||
this.currentDeductible > 0
|
||||
);
|
||||
} else {
|
||||
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
|
||||
if (piaExperience?.includes("PIA Disabled")) {
|
||||
return this.isConditionalPIAEnabled;
|
||||
} else {
|
||||
return piaExperience === "PIA Optional" || piaExperience === "PIA Required";
|
||||
}
|
||||
}
|
||||
},
|
||||
isConditionalPIAEnabled() {
|
||||
const conditionalProviderNumbers = this.getSettingValue(
|
||||
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS
|
||||
);
|
||||
const conditionalProviderNumbers2 = this.getSettingValue(
|
||||
experimentSettings.CONDITIONAL_PROVIDER_NUMBERS_2
|
||||
);
|
||||
const conditionalProviderShutoffDate = this.getSettingValue(
|
||||
experimentSettings.CONDITIONAL_PROVIDER_SHUTOFF_DATE
|
||||
);
|
||||
|
||||
const allConditionalProviderNumbers = [
|
||||
conditionalProviderNumbers,
|
||||
conditionalProviderNumbers2,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(",");
|
||||
const providerNumber =
|
||||
this.$store.getters.order.serviceLocation.provider.providerNumber;
|
||||
const appointmentDate = new Date(this.$store.getters.order.schedule.date);
|
||||
const shutoffDate = new Date(conditionalProviderShutoffDate);
|
||||
if (allConditionalProviderNumbers && conditionalProviderShutoffDate) {
|
||||
const providerNumbersArray = allConditionalProviderNumbers
|
||||
.split(",")
|
||||
.map((num) => num.trim());
|
||||
if (!providerNumbersArray.includes(providerNumber)) {
|
||||
return false;
|
||||
} else {
|
||||
return appointmentDate <= shutoffDate;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
paymentMethod() {
|
||||
if (!this.isPiaEnabled) {
|
||||
return paymentMethods.LATER;
|
||||
|
|
@ -986,8 +1034,9 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
:deep(span.label) {
|
||||
font-weight: 600;
|
||||
:deep(span.label),
|
||||
:deep(span) {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
:deep(div.service-questions span) {
|
||||
color: $black;
|
||||
|
|
|
|||
|
|
@ -156,6 +156,9 @@ export default {
|
|||
lastFour: lastFour,
|
||||
};
|
||||
|
||||
console.log(`CCToken =`);
|
||||
console.log(ccToken);
|
||||
|
||||
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false);
|
||||
baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<template>
|
||||
<div id="afterpay-banner" role="alert">
|
||||
<component
|
||||
:is="'script'"
|
||||
src="https://js.squarecdn.com/square-marketplace.js"
|
||||
async></component>
|
||||
|
||||
<div class="afterpay-modal-banner" role="alert">
|
||||
<div>
|
||||
<span
|
||||
class="callout"
|
||||
|
|
@ -22,22 +17,52 @@
|
|||
<span v-else v-html="token"></span>
|
||||
<span class="nbsp"> </span>
|
||||
</span>
|
||||
<a
|
||||
id="afterpay-learnmore"
|
||||
href="#"
|
||||
data-afterpay-modal="en_US"
|
||||
data-bind="click:afterpayLearnMore">
|
||||
{{ modalCopy }}
|
||||
</a>
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="bannerCta"
|
||||
href="javascript:void(0)"
|
||||
@click-event="openModal" />
|
||||
</div>
|
||||
</div>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
class="afterpay-modal"
|
||||
:headerText="modalHeaderText"
|
||||
:isFooterButtonSuppressed="true">
|
||||
<template v-slot:modal-header-slot>
|
||||
<span>{{ modalSubHeaderText }}</span>
|
||||
</template>
|
||||
<template v-slot>
|
||||
<span class="afterpay-sections">
|
||||
<span
|
||||
v-for="section in afterpaySectionsContent"
|
||||
:key="section"
|
||||
class="afterpay-section">
|
||||
<span class="section-header">
|
||||
<img v-if="section.AnswerImageUrl" :src="section.AnswerImageUrl" />
|
||||
<h3 v-if="section.Text" v-html="section.Text"></h3>
|
||||
</span>
|
||||
<span v-html="section.content"></span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:modal-footer-slot>
|
||||
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { splitCopyOnCMSPlaceHolder, splitCMSCopyOnBR } from "@/helpers/cms-content-helper";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { getPromosThatMatchLineItemsOnOrder } from "@/helpers/promotions-helper";
|
||||
import { getArrayOfAllLineItemsAndChildParts } from "@/store";
|
||||
import {
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getExternalLink,
|
||||
splitCMSCopyOnBR,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
|
||||
const INLINE_IMAGE_TOKEN = "custom:inlineImage";
|
||||
const AFTERPAY_PRICE_TOKEN = "custom:afterpayPrice";
|
||||
|
|
@ -48,20 +73,16 @@ function isInlineImageToken(token) {
|
|||
function isAfterpayPriceToken(token) {
|
||||
return token.includes(AFTERPAY_PRICE_TOKEN);
|
||||
}
|
||||
|
||||
function getInlineAltText(token) {
|
||||
const innerTokens = token.split(",");
|
||||
|
||||
return innerTokens[1] ?? "";
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "afterpay-modal-banner",
|
||||
name: "afterpay-banner",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
lineItems: Array,
|
||||
isInsuranceSelected: Boolean,
|
||||
afterpayExtendedPayOptionThreshold: Number,
|
||||
modalWidgetName: String,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
|
@ -70,6 +91,17 @@ export default {
|
|||
isInlineImageToken,
|
||||
isAfterpayPriceToken,
|
||||
getInlineAltText,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
getExternalLink,
|
||||
openModal() {
|
||||
this.modal?.openModal();
|
||||
},
|
||||
closeModal() {
|
||||
this.modal?.closeModal();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
|
|
@ -84,39 +116,40 @@ export default {
|
|||
afterpayCopyTokens() {
|
||||
return splitCopyOnCMSPlaceHolder(this.getCmsContent(this.cmsWidgetName, "BodyText"));
|
||||
},
|
||||
modalCopy() {
|
||||
bannerCta() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
getTierOnePackagePrice() {
|
||||
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
|
||||
if (this.lineItems.promos) {
|
||||
allLineItems = allLineItems?.filter((item) => item.partType !== "PROMO_DISCOUNT");
|
||||
}
|
||||
|
||||
let price = baseMixin.methods.getTierOnePackagePrice(
|
||||
baseMixin.methods.filterOutFees(allLineItems)
|
||||
);
|
||||
|
||||
if (this.lineItems.promos) {
|
||||
let allLineItems = getArrayOfAllLineItemsAndChildParts(this.lineItems);
|
||||
const promos = getPromosThatMatchLineItemsOnOrder(
|
||||
this.lineItems.promos,
|
||||
allLineItems
|
||||
);
|
||||
promos.forEach((promo) => {
|
||||
price += baseMixin.methods.getTotalLineItemPrice(promo);
|
||||
});
|
||||
}
|
||||
|
||||
return price;
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
modalDisclaimerText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "BodyText2");
|
||||
},
|
||||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
afterpaySectionsContent() {
|
||||
const sections = this.getCmsContent("AfterpayModalSectionsWidget", "Answers");
|
||||
if (!sections) return [];
|
||||
sections.forEach((section) => {
|
||||
const sectionContent = this.getCmsContent(section.Name, "BodyText");
|
||||
section["content"] = sectionContent;
|
||||
});
|
||||
return sections;
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
components: {
|
||||
textLink,
|
||||
modal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#afterpay-banner {
|
||||
.afterpay-modal-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $blue-150;
|
||||
|
|
@ -130,6 +163,7 @@ export default {
|
|||
|
||||
.callout {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> div:first-of-type {
|
||||
|
|
@ -163,15 +197,120 @@ export default {
|
|||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-left: 1rem;
|
||||
max-width: 32rem;
|
||||
max-width: 36rem;
|
||||
}
|
||||
}
|
||||
& .alert-heading {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.afterpay-modal {
|
||||
&.modal.modal-component {
|
||||
:deep(.modal-dialog) {
|
||||
margin: 0 1rem;
|
||||
top: 0;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 100%;
|
||||
max-width: 840px;
|
||||
top: 3.4rem;
|
||||
right: 0;
|
||||
left: 0;
|
||||
transform: none;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.modal-content {
|
||||
border-radius: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
|
||||
.modal-header.mt-6 {
|
||||
background-color: $blue-100;
|
||||
margin-top: 0;
|
||||
padding: 1.5rem 2rem 1.5rem 1.5rem;
|
||||
font-family: UrbanistSemibold;
|
||||
|
||||
.modal-title.justify-content-center {
|
||||
// OVERRIDE
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.btn-close {
|
||||
top: 1.25rem;
|
||||
right: 1.5rem;
|
||||
}
|
||||
}
|
||||
.afterpay-section {
|
||||
display: block;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
h3 {
|
||||
font-family: UrbanistBold;
|
||||
font-size: 1rem;
|
||||
margin: 0 0 0 0.5rem;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
strong {
|
||||
font-family: UrbanistSemibold;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
|
||||
.afterpay-sections {
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
column-gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
.afterpay-section {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 1 33%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.modal-dialog-centered {
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.modal-footer) {
|
||||
position: relative;
|
||||
background: $gray-100;
|
||||
|
||||
.modal-disclaimer {
|
||||
font-style: italic;
|
||||
font-size: 0.75rem;
|
||||
a {
|
||||
padding: 0;
|
||||
line-height: 1.625;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#afterpay-learnmore {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
167
src/layouts/quote/oem-glass-question/oem-glass-question.vue
Normal file
167
src/layouts/quote/oem-glass-question/oem-glass-question.vue
Normal 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>
|
||||
|
|
@ -82,7 +82,7 @@ jest.mock("@/mixins/base-mixin", () => ({
|
|||
filterOutFees(items) {
|
||||
return null;
|
||||
},
|
||||
filterOutServicePackageDiscountPart(items) {
|
||||
filterOutQuotePageDiscountPart(items) {
|
||||
return null;
|
||||
},
|
||||
isFormValid(form) {
|
||||
|
|
@ -118,6 +118,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
|
|||
if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) {
|
||||
return false;
|
||||
}
|
||||
if (settingName === mockExperimentSettings.OFFER_OEM) {
|
||||
return false;
|
||||
}
|
||||
return "test";
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@
|
|||
:isRecalibrationOnOrder="isRecalibrationOnOrder"
|
||||
:shouldHideRecalibration="shouldHideRecalibration"
|
||||
@vapsItemsSelected="vapsItemsSelectedAction"
|
||||
@servicePackageDiscountSelected="servicePackageDiscountSelectedAction"
|
||||
@quotePageDiscountSelected="quotePageDiscountSelectedAction"
|
||||
@currentNumberOfPackages="currentNumberOfPackagesAction"
|
||||
@servicePackageSelected="servicePackageSelectedAction"
|
||||
:servicePackage="servicePackage"
|
||||
:activePromos="lineItems.promos"
|
||||
:availableQuotePageDiscounts="quoteDiscountPartsInfo"
|
||||
v-on="{ 'buttonEvent.openModal': openModalAction }"
|
||||
validationRules="option-required"
|
||||
isRequired />
|
||||
|
|
@ -45,10 +47,8 @@
|
|||
<!-- If Cash AND vehicle does not require recal OR if Cash AND State requires showing recal, then show Afterpay banner -->
|
||||
<afterpayModalBanner
|
||||
v-if="showAfterpayBanner"
|
||||
cmsWidgetName="AfterpayModalWidget"
|
||||
:afterpayExtendedPayOptionThreshold="afterpayExtendedPayOptionThreshold"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
:lineItems="lineItems" />
|
||||
cmsWidgetName="AfterpayBannerWidget"
|
||||
modalWidgetName="AfterpayModalWidget" />
|
||||
|
||||
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
|
||||
<recalDisclaimer
|
||||
|
|
@ -86,6 +86,13 @@
|
|||
isInsuranceSelected ? isInsuranceContinueButtonText : ''
|
||||
" />
|
||||
|
||||
<oemGlassQuestion
|
||||
cmsWidgetName="OemGlassQuestionWidget"
|
||||
v-if="offerOem && !isInsuranceSelected"
|
||||
v-model="isOemGlassSelected"
|
||||
:oemGlassPartPrice="oemGlassPartPrice"
|
||||
:aftermarketGlassPartPrice="aftermarketGlassPartPrice" />
|
||||
|
||||
<saveProgressModalQuestion
|
||||
modalWidgetName="SaveProgressModalWidget"
|
||||
modalName="SaveProgressModal"
|
||||
|
|
@ -129,6 +136,7 @@ import afterpayModalBanner from "@/layouts/quote/afterpay-modal-banner/afterpay-
|
|||
import recalDisclaimer from "@/layouts/quote/recal-disclaimer/recal-disclaimer.vue";
|
||||
import saveProgressModalQuestion from "@/fmg-components/save-progress-modal-question/save-progress-modal-question";
|
||||
import saveProgressPopupQuestion from "@/fmg-components/save-progress-popup-question/save-progress-popup-question";
|
||||
import oemGlassQuestion from "./oem-glass-question/oem-glass-question.vue";
|
||||
|
||||
// Supporting files
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
|
@ -176,6 +184,7 @@ import { consumeQueryFromStash } from "@/router/methods/helpers/querystring-stas
|
|||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||
import { quotePageDiscountTable } from "../../constants/quote-page-discounts";
|
||||
|
||||
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
|
|
@ -186,6 +195,14 @@ const ACCEPTED_QUOTE_TYPES = {
|
|||
INSURANCE: "insurance",
|
||||
};
|
||||
|
||||
function getAvailableQuotePageDiscounts() {
|
||||
const activeDiscounts = quotePageDiscountTable?.filter((discountEntry) =>
|
||||
experimentMixin?.methods?.hasSettingEqualTo(discountEntry?.experimentCode, "true")
|
||||
);
|
||||
|
||||
return activeDiscounts ?? [];
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "quote",
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -209,6 +226,15 @@ export default {
|
|||
null,
|
||||
"quote"
|
||||
);
|
||||
const experimentAllowsOfferOem = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.OFFER_OEM,
|
||||
"true"
|
||||
);
|
||||
const oemGlassCheckPromise = baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_OEM_AFTERMARKET_PARTS,
|
||||
null,
|
||||
"quote"
|
||||
);
|
||||
|
||||
const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder;
|
||||
const skipQuoteExperiment = store.getters.applicationUser.experiments.find(
|
||||
|
|
@ -282,6 +308,10 @@ export default {
|
|||
resultKey: "supportingItems",
|
||||
promise: supportingItemsPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "oemGlassCheck",
|
||||
promise: oemGlassCheckPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
|
@ -302,31 +332,48 @@ export default {
|
|||
const lineItems = deepClone(store.getters.order.lineItems);
|
||||
lineItems.vaps = lineItems.vaps ?? [];
|
||||
const nullSafeGlassParts = lineItems.glassParts ?? [];
|
||||
const servicePackageDiscountSettingValue = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SERVICE_PACKAGE_DISCOUNT
|
||||
);
|
||||
const isServicePackageDiscount = servicePackageDiscountSettingValue === "True";
|
||||
//Service package cash discount api call when experiment is active
|
||||
var servicePackageDiscountPart = [];
|
||||
if (isServicePackageDiscount) {
|
||||
const servicePackageDiscountPartResponse =
|
||||
await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.GET_SERVICE_PACKAGE_DISCOUNT_PART,
|
||||
null,
|
||||
"quote"
|
||||
);
|
||||
if (servicePackageDiscountPartResponse.data) {
|
||||
servicePackageDiscountPart.push(servicePackageDiscountPartResponse.data);
|
||||
}
|
||||
}
|
||||
|
||||
const oemGlassCheck = resultMap.oemGlassCheck;
|
||||
const offerOem = experimentAllowsOfferOem ? oemGlassCheck?.offerOem : false;
|
||||
const oemGlassPieceParts = oemGlassCheck?.glassPieceParts;
|
||||
const oemGlassParts =
|
||||
oemGlassPieceParts && oemGlassPieceParts.length > 0
|
||||
? oemGlassPieceParts[0].parts
|
||||
: null;
|
||||
const isOemGlassSelected = store.getters.order.damage?.installOemGlass ?? false;
|
||||
|
||||
let alternateGlassPartsForOem;
|
||||
if (oemGlassParts) alternateGlassPartsForOem = oemGlassParts;
|
||||
|
||||
const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts();
|
||||
|
||||
const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({
|
||||
partNumber: partInfo.partNumber,
|
||||
partType: partInfo.partType,
|
||||
description: partInfo.description,
|
||||
isInsurable: null,
|
||||
}));
|
||||
|
||||
const availableLineItems = [
|
||||
resultMap.rainRepel,
|
||||
...resultMap.supportingItems,
|
||||
...resultMap.wipers,
|
||||
...nullSafeGlassParts,
|
||||
...servicePackageDiscountPart,
|
||||
...quotePageDiscountParts,
|
||||
];
|
||||
|
||||
let pricedAlternateGlassParts = null;
|
||||
if (alternateGlassPartsForOem && alternateGlassPartsForOem.length > 0) {
|
||||
pricedAlternateGlassParts = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||
{
|
||||
availableLineItems: alternateGlassPartsForOem,
|
||||
},
|
||||
"quote",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards
|
||||
var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber;
|
||||
var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber;
|
||||
|
|
@ -351,7 +398,7 @@ export default {
|
|||
false
|
||||
);
|
||||
|
||||
// This will remove any servicePackageDiscount item from lineItems.supportingItems
|
||||
// This will remove any discount item from lineItems.supportingItems
|
||||
baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_SUPPORTING_ITEMS,
|
||||
resultMap.supportingItems,
|
||||
|
|
@ -388,6 +435,41 @@ export default {
|
|||
vm.holdParentAccountNumber = holdParentAccountNumber;
|
||||
vm.holdBillToAccountNumber = holdBillToAccountNumber;
|
||||
vm.skipToInsurance = shouldSkipToInsurance;
|
||||
vm.offerOem = offerOem;
|
||||
vm.alternateGlassPartsForOem = pricedAlternateGlassParts; // oemParts
|
||||
vm.isOemGlassSelected = isOemGlassSelected;
|
||||
|
||||
const getOEMGlassPartPrice = () => {
|
||||
let price = 0;
|
||||
if (vm.alternateGlassPartsForOem) {
|
||||
let oemGlassPart = vm.isOemGlassSelected
|
||||
? vm.lineItems.glassParts
|
||||
: vm.alternateGlassPartsForOem;
|
||||
if (oemGlassPart) {
|
||||
price =
|
||||
oemGlassPart.find(
|
||||
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
|
||||
)?.sellingPrice || 0;
|
||||
}
|
||||
}
|
||||
return price;
|
||||
};
|
||||
const getAftermarketGlassPartPrice = () => {
|
||||
let price = 0;
|
||||
if (vm.alternateGlassPartsForOem) {
|
||||
let aftermarketGlassPart = vm.isOemGlassSelected
|
||||
? vm.alternateGlassPartsForOem
|
||||
: vm.lineItems.glassParts;
|
||||
if (aftermarketGlassPart) {
|
||||
price = aftermarketGlassPart.find(
|
||||
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
|
||||
)?.sellingPrice;
|
||||
}
|
||||
}
|
||||
return price;
|
||||
};
|
||||
vm.oemGlassPartPrice = getOEMGlassPartPrice();
|
||||
vm.aftermarketGlassPartPrice = getAftermarketGlassPartPrice();
|
||||
|
||||
if (revalidatePromoResponse) {
|
||||
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
|
||||
|
|
@ -453,10 +535,9 @@ export default {
|
|||
)
|
||||
: baseMixin.methods.filterOutFees(availableLineItems);
|
||||
|
||||
const lineItemsNoDiscount =
|
||||
baseMixin.methods.filterOutServicePackageDiscountPart(
|
||||
lineItemsForCalculatingPrice
|
||||
);
|
||||
const lineItemsNoDiscount = baseMixin.methods.filterOutQuotePageDiscountPart(
|
||||
lineItemsForCalculatingPrice
|
||||
);
|
||||
|
||||
if (isInsuranceFromQueryString != null) {
|
||||
if (isInsuranceFromQueryString.toLowerCase() === "true") {
|
||||
|
|
@ -556,6 +637,8 @@ export default {
|
|||
availableLineItems: null,
|
||||
lineItems: [],
|
||||
addableVaps: [],
|
||||
offerOem: null,
|
||||
alternateGlassPartsForOem: null,
|
||||
servicePackage: null,
|
||||
holdParentAccountNumber: null,
|
||||
holdBillToAccountNumber: null,
|
||||
|
|
@ -564,6 +647,8 @@ export default {
|
|||
isSaveProgressComplete: null,
|
||||
packageNumber: null,
|
||||
skipToInsurance: null,
|
||||
servicePackageSelected: null,
|
||||
isOemGlassSelected: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
|
|
@ -580,17 +665,8 @@ export default {
|
|||
isRecalibrationOnOrder() {
|
||||
return store.getters.isRecalibrationOnOrder;
|
||||
},
|
||||
isServicePackageDiscountOnOrder() {
|
||||
return containsLineItemWithPartType(
|
||||
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
|
||||
this.availableLineItems
|
||||
);
|
||||
},
|
||||
servicePackageDiscountParts() {
|
||||
return findLineItemsWithPartType(
|
||||
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
|
||||
this.availableLineItems
|
||||
);
|
||||
isQuotePageDiscountOnOrder() {
|
||||
return this.quoteDiscountPartsInfo?.length > 0;
|
||||
},
|
||||
shouldHideRecalibration() {
|
||||
return (
|
||||
|
|
@ -619,6 +695,23 @@ export default {
|
|||
thresholdAmount && (thresholdAmount = parseInt(thresholdAmount));
|
||||
return thresholdAmount;
|
||||
},
|
||||
quoteDiscountPartsInfo() {
|
||||
const defs = getAvailableQuotePageDiscounts();
|
||||
const withPrices = defs
|
||||
.map((def) => {
|
||||
const match = this.availableLineItems?.find?.(
|
||||
(pricedItem) => pricedItem.partNumber === def.partNumber
|
||||
);
|
||||
|
||||
return {
|
||||
packageLevel: def.packageLevel,
|
||||
item: match,
|
||||
};
|
||||
})
|
||||
.filter((result) => result.item);
|
||||
|
||||
return withPrices;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getColCount() {
|
||||
|
|
@ -662,7 +755,10 @@ export default {
|
|||
vapsItemsSelectedAction(vapsItemsSelected) {
|
||||
this.lineItems.vaps = vapsItemsSelected;
|
||||
},
|
||||
servicePackageDiscountSelectedAction(supportingItemsSelected) {
|
||||
servicePackageSelectedAction(newValue) {
|
||||
this.servicePackageSelected = newValue;
|
||||
},
|
||||
quotePageDiscountSelectedAction(supportingItemsSelected) {
|
||||
this.lineItems.supportingItems = supportingItemsSelected;
|
||||
},
|
||||
backButtonAction() {
|
||||
|
|
@ -717,6 +813,20 @@ export default {
|
|||
this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems);
|
||||
}
|
||||
|
||||
if (
|
||||
this.lineItems.glassParts &&
|
||||
this.alternateGlassPartsForOem &&
|
||||
this.alternateGlassPartsForOem.length > 0
|
||||
) {
|
||||
let isOEMGlassOnOrder = this.isOEMGlassOnOrder(this.lineItems.glassParts);
|
||||
if (
|
||||
(this.isOemGlassSelected && !isOEMGlassOnOrder) ||
|
||||
(!this.isOemGlassSelected && isOEMGlassOnOrder)
|
||||
) {
|
||||
this.swapOEMGlassPartsOnOrder();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.lineItems.glassParts?.length > 0) {
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING,
|
||||
|
|
@ -724,10 +834,10 @@ export default {
|
|||
false
|
||||
);
|
||||
}
|
||||
if (this.isInsuranceSelected && this.isServicePackageDiscountOnOrder) {
|
||||
if (this.isInsuranceSelected && this.isQuotePageDiscountOnOrder) {
|
||||
let supportingItems = this.lineItems.supportingItems;
|
||||
supportingItems = supportingItems.filter((item) => {
|
||||
return item.partType != this.servicePackageDiscountParts[0].partType;
|
||||
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
|
||||
});
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SUPPORTING_ITEMS,
|
||||
|
|
@ -749,8 +859,20 @@ export default {
|
|||
},
|
||||
false
|
||||
);
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_IS_OEM_GLASS_SELECTED,
|
||||
this.isOemGlassSelected,
|
||||
false
|
||||
);
|
||||
|
||||
await savePageData(this.pageName, { saveProgressPopupSkipped: true }, true);
|
||||
await savePageData(
|
||||
this.pageName,
|
||||
{
|
||||
saveProgressPopupSkipped: true,
|
||||
servicePackageSelected: this.servicePackageSelected,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const payment = this.$store.getters.payment;
|
||||
if (payment.isInsurance) {
|
||||
|
|
@ -813,19 +935,23 @@ export default {
|
|||
lineItemsToPrice =
|
||||
baseMixin.methods.filterOutRecalibration(lineItemsToPrice);
|
||||
}
|
||||
if (this.isServicePackageDiscountOnOrder) {
|
||||
if (this.isQuotePageDiscountOnOrder) {
|
||||
lineItemsToPrice = lineItemsToPrice?.filter((item) => {
|
||||
return (
|
||||
item.partType != this.servicePackageDiscountParts[0].partType
|
||||
);
|
||||
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
|
||||
});
|
||||
}
|
||||
let price = 0;
|
||||
if (
|
||||
tier.additionalButtonData?.servicePackageDiscount &&
|
||||
this.isServicePackageDiscountOnOrder
|
||||
tier.additionalButtonData?.hasDiscount &&
|
||||
this.isQuotePageDiscountOnOrder
|
||||
) {
|
||||
lineItemsToPrice.push(...this.servicePackageDiscountParts);
|
||||
const parts = this.quoteDiscountPartsInfo;
|
||||
const matchPart = parts.find(
|
||||
(discountInfo) => discountInfo.packageLevel === tier
|
||||
);
|
||||
if (matchPart) {
|
||||
lineItemsToPrice.push(matchPart);
|
||||
}
|
||||
}
|
||||
price += baseMixin.methods.getTierOnePackagePrice(
|
||||
baseMixin.methods.filterOutFees(lineItemsToPrice)
|
||||
|
|
@ -856,6 +982,26 @@ export default {
|
|||
const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode);
|
||||
this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade);
|
||||
},
|
||||
isOEMGlassOnOrder(glassParts) {
|
||||
if (glassParts && glassParts.length > 0) {
|
||||
return glassParts.some((part) => part.partNumber.endsWith("OEM"));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
swapOEMGlassPartsOnOrder() {
|
||||
this.lineItems.glassParts.forEach((part, index) => {
|
||||
if (part.partType === partTypeStrings.WINDSHIELD) {
|
||||
let altPartToUse = this.alternateGlassPartsForOem.find(
|
||||
(altPart) => altPart.partType === partTypeStrings.WINDSHIELD
|
||||
);
|
||||
if (altPartToUse) {
|
||||
let childParts = this.lineItems.glassParts[index].childParts;
|
||||
altPartToUse.childParts = childParts;
|
||||
this.lineItems.glassParts[index] = altPartToUse;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
|
|
@ -872,6 +1018,7 @@ export default {
|
|||
recalDisclaimer,
|
||||
saveProgressModalQuestion,
|
||||
saveProgressPopupQuestion,
|
||||
oemGlassQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { experimentSettings } from "@/constants/experiments";
|
|||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
import { nextTick } from "vue";
|
||||
import { packageNames } from "../../../constants/package-names";
|
||||
|
||||
jest.mock("@/store", () => ({
|
||||
commit: jest.fn(),
|
||||
|
|
@ -73,9 +74,9 @@ describe("service-package-question.vue", () => {
|
|||
price: 35.5,
|
||||
},
|
||||
{
|
||||
partNumber: "DISC CASHSAVE70",
|
||||
partNumber: "PRM RCL CSH PKG",
|
||||
description: null,
|
||||
partType: "SERVICE PACKAGE DISCOUNT",
|
||||
partType: "QUOTE PAGE DISCOUNT",
|
||||
price: -70,
|
||||
},
|
||||
],
|
||||
|
|
@ -105,13 +106,27 @@ describe("service-package-question.vue", () => {
|
|||
description: null,
|
||||
kitPrice: 0,
|
||||
laborAmount: 0,
|
||||
partNumber: "DISC CASHSAVE70",
|
||||
partType: "SERVICE PACKAGE DISCOUNT",
|
||||
partNumber: "PRM RCL CSH PKG",
|
||||
partType: "QUOTE PAGE DISCOUNT",
|
||||
salesTax: null,
|
||||
sellingPrice: -70,
|
||||
},
|
||||
],
|
||||
},
|
||||
availableQuotePageDiscounts: [
|
||||
{
|
||||
packageLevel: packageNames.TIER_THREE,
|
||||
item: {
|
||||
description: null,
|
||||
kitPrice: 0,
|
||||
laborAmount: 0,
|
||||
partNumber: "PRM RCL CSH PKG",
|
||||
partType: "QUOTE PAGE DISCOUNT",
|
||||
salesTax: null,
|
||||
sellingPrice: -70,
|
||||
},
|
||||
},
|
||||
],
|
||||
activePromos: [],
|
||||
};
|
||||
const packageNameKey = "05_01_CSR_Quote_Standard_Repair";
|
||||
|
|
@ -136,41 +151,17 @@ describe("service-package-question.vue", () => {
|
|||
});
|
||||
it("should return price when there is discount", () => {
|
||||
const wrapper = setupMocks({});
|
||||
const partType = partTypeStrings.SERVICE_PACKAGE_DISCOUNT;
|
||||
|
||||
wrapper.vm.isServicePackageDiscountOnOrder = containsLineItemWithPartType(
|
||||
partType,
|
||||
mockProps.lineItems.supportingItems
|
||||
);
|
||||
const servicePackageDiscountLineItem = findLineItemsWithPartType(
|
||||
partType,
|
||||
mockProps.lineItems.supportingItems
|
||||
);
|
||||
wrapper.vm.getServicePackageDiscountPrice();
|
||||
const price = baseMixin.methods.getTotalLineItemPrice(servicePackageDiscountLineItem[0]);
|
||||
const price = wrapper.vm.getDiscountPrice(packageNames.TIER_THREE);
|
||||
|
||||
expect(Math.abs(price)).toEqual(70);
|
||||
});
|
||||
it("isServicePackageDiscountOnOrder returns true if it contains service package discount lineItems", () => {
|
||||
it("isDiscountOnOrder returns true if it contains quote package discount lineItems", () => {
|
||||
// Arrange
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isServicePackageDiscountOnOrder).toBe(true);
|
||||
});
|
||||
it("servicePackageDiscountParts should returns service package discount lineItems", () => {
|
||||
// Arrange
|
||||
const wrapper = setupMocks({});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.servicePackageDiscountParts).toEqual([
|
||||
{
|
||||
partNumber: "DISC CASHSAVE70",
|
||||
description: null,
|
||||
partType: "SERVICE PACKAGE DISCOUNT",
|
||||
price: -70,
|
||||
},
|
||||
]);
|
||||
expect(wrapper.vm.isDiscountOnOrder).toBe(true);
|
||||
});
|
||||
it("should have the correct insurance pricing text when insurance is selected", () => {
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
findLineItemsWithPartType,
|
||||
getPackageNameByType,
|
||||
getDiscountedPackageName,
|
||||
getPackageLabel,
|
||||
} from "@/helpers/service-package-helper";
|
||||
import {
|
||||
getPromosThatMatchLineItemsOnOrder,
|
||||
|
|
@ -45,6 +46,7 @@ import { containsRecalParts, getItemsWithoutRecalParts } from "@/helpers/recal-h
|
|||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||
import { debugLog } from "@/helpers/debug-log-helper";
|
||||
|
||||
export default {
|
||||
name: "servicePackageQuestion",
|
||||
|
|
@ -60,6 +62,7 @@ export default {
|
|||
servicePackage: null,
|
||||
isRecalibrationOnOrder: Boolean,
|
||||
shouldHideRecalibration: Boolean,
|
||||
availableQuotePageDiscounts: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -83,29 +86,26 @@ export default {
|
|||
if (this.activePromos?.length) this.selectDefaultPackage();
|
||||
},
|
||||
selectedPackageName(newValue) {
|
||||
debugLog("selectedPackageName:", newValue);
|
||||
const packageLabel = getPackageLabel(newValue);
|
||||
|
||||
debugLog("getPackageLabel:", packageLabel);
|
||||
this.$emit("servicePackageSelected", `ServicePackage-${packageLabel}`);
|
||||
|
||||
const VapsProductsInSelectedPackage = this.getVapsLineItemsForSelectedPackage(newValue);
|
||||
this.$emit("vapsItemsSelected", VapsProductsInSelectedPackage);
|
||||
const servicePackageDiscountPartInSelectedPackage =
|
||||
this.getServicePackageDiscountPartForSelectedPackage(newValue);
|
||||
const discountPartInSelectedPackage = this.getDiscountPartForSelectedPackage(newValue);
|
||||
let supportingItems = this.supportingLineItems;
|
||||
if (
|
||||
containsLineItemWithPartType(
|
||||
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
|
||||
supportingItems
|
||||
)
|
||||
) {
|
||||
supportingItems = supportingItems.filter(
|
||||
(item) => item.partType !== partTypeStrings.SERVICE_PACKAGE_DISCOUNT
|
||||
);
|
||||
}
|
||||
if (
|
||||
servicePackageDiscountPartInSelectedPackage?.length > 0 &&
|
||||
supportingItems != null
|
||||
) {
|
||||
supportingItems.push(servicePackageDiscountPartInSelectedPackage[0]);
|
||||
|
||||
supportingItems = supportingItems.filter(
|
||||
(item) => item.partType !== partTypeStrings.QUOTE_PAGE_DISCOUNT
|
||||
);
|
||||
|
||||
if (discountPartInSelectedPackage) {
|
||||
supportingItems.push(discountPartInSelectedPackage);
|
||||
}
|
||||
|
||||
this.$emit("servicePackageDiscountSelected", supportingItems);
|
||||
this.$emit("quotePageDiscountSelected", supportingItems);
|
||||
},
|
||||
servicePackage(newValue) {
|
||||
if (newValue !== null) {
|
||||
|
|
@ -118,7 +118,7 @@ export default {
|
|||
return this.availableLineItems ?? [];
|
||||
},
|
||||
supportingLineItems() {
|
||||
return this.$store.getters.lineItems?.supportingItems;
|
||||
return this.$store.getters.lineItems?.supportingItems ?? [];
|
||||
},
|
||||
servicePackageAnswers() {
|
||||
const cmsWidgetName = this.isInsuranceSelected
|
||||
|
|
@ -149,18 +149,15 @@ export default {
|
|||
? this.getDiscountedPackagePriceString(answer.Name, false)
|
||||
: this.getDiscountedPackagePriceString(
|
||||
answer.Name,
|
||||
this.isServicePackageDiscountOnOrder &&
|
||||
this.discountPackageNames == answer.Name
|
||||
this.hasDiscountForPackage(answer.Name)
|
||||
),
|
||||
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
|
||||
additionalButtonData: {
|
||||
strikeThroughPrice: this.getPackagePriceString(answer.Name),
|
||||
servicePackageDiscount:
|
||||
this.isServicePackageDiscountOnOrder &&
|
||||
this.discountPackageNames == answer.Name,
|
||||
hasDiscount: this.hasDiscountForPackage(answer.Name),
|
||||
Text: this.isInsuranceSelected
|
||||
? false
|
||||
: "Special: Save $" + this.getServicePackageDiscountPrice(),
|
||||
: "Special: Save $" + this.getDiscountPrice(answer.Name),
|
||||
isInsuranceSelected: this.isInsuranceSelected,
|
||||
},
|
||||
}));
|
||||
|
|
@ -168,17 +165,8 @@ export default {
|
|||
this.$emit("currentNumberOfPackages", modifiedAnswers.length);
|
||||
return modifiedAnswers;
|
||||
},
|
||||
isServicePackageDiscountOnOrder() {
|
||||
return containsLineItemWithPartType(
|
||||
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
|
||||
this.nullSafeAvailableLineItems
|
||||
);
|
||||
},
|
||||
discountPackageNames() {
|
||||
const discountServicePackage = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.PROMO_ON_PACKAGE
|
||||
);
|
||||
return getDiscountedPackageName(discountServicePackage);
|
||||
isDiscountOnOrder() {
|
||||
return this.availableQuotePageDiscounts?.length > 0;
|
||||
},
|
||||
frontWipersApplicableForTierTwo() {
|
||||
return shouldFrontWipersBeAvailable(
|
||||
|
|
@ -226,12 +214,6 @@ export default {
|
|||
isRepair() {
|
||||
return this.$store.getters.order.damage.isRepair;
|
||||
},
|
||||
servicePackageDiscountParts() {
|
||||
return findLineItemsWithPartType(
|
||||
partTypeStrings.SERVICE_PACKAGE_DISCOUNT,
|
||||
this.nullSafeAvailableLineItems
|
||||
);
|
||||
},
|
||||
showRecalInServicePackages() {
|
||||
return this.isRecalibrationOnOrder && !this.shouldHideRecalibration;
|
||||
},
|
||||
|
|
@ -280,16 +262,16 @@ export default {
|
|||
const formattedPriceFloat = parseFloat(
|
||||
this.getPackagePrice(packageName, {
|
||||
discountedPrice: false,
|
||||
servicePackageDiscount: false,
|
||||
quotePageDiscount: false,
|
||||
})
|
||||
).toFixed(2);
|
||||
return "$" + formattedPriceFloat;
|
||||
},
|
||||
getDiscountedPackagePriceString(packageName, servicePackageDiscount) {
|
||||
getDiscountedPackagePriceString(packageName, quotePageDiscount) {
|
||||
const formattedPriceFloat = parseFloat(
|
||||
this.getPackagePrice(packageName, {
|
||||
discountedPrice: true,
|
||||
servicePackageDiscount,
|
||||
quotePageDiscount,
|
||||
})
|
||||
).toFixed(2);
|
||||
|
||||
|
|
@ -298,7 +280,7 @@ export default {
|
|||
}
|
||||
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
|
||||
},
|
||||
getPackagePrice(packageName, { discountedPrice = false, servicePackageDiscount = false }) {
|
||||
getPackagePrice(packageName, { discountedPrice = false, quotePageDiscount = false }) {
|
||||
let lineItemsToPrice = [...this.nullSafeAvailableLineItems];
|
||||
|
||||
if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) {
|
||||
|
|
@ -306,17 +288,20 @@ export default {
|
|||
}
|
||||
|
||||
//remove service package discount part
|
||||
if (this.isServicePackageDiscountOnOrder) {
|
||||
if (this.isDiscountOnOrder) {
|
||||
lineItemsToPrice = lineItemsToPrice.filter((item) => {
|
||||
return item.partType != this.servicePackageDiscountParts[0].partType;
|
||||
return item.partType != partTypeStrings.QUOTE_PAGE_DISCOUNT;
|
||||
});
|
||||
}
|
||||
|
||||
if (discountedPrice) {
|
||||
lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos));
|
||||
}
|
||||
if (servicePackageDiscount) {
|
||||
lineItemsToPrice.push(...this.servicePackageDiscountParts);
|
||||
if (quotePageDiscount) {
|
||||
const part = this.getDiscountPartForSelectedPackage(packageName);
|
||||
if (part) {
|
||||
lineItemsToPrice.push(part);
|
||||
}
|
||||
}
|
||||
let priceFloat = this.isInsuranceSelected
|
||||
? 0
|
||||
|
|
@ -328,14 +313,16 @@ export default {
|
|||
|
||||
return priceFloat;
|
||||
},
|
||||
getServicePackageDiscountPrice() {
|
||||
if (this.isServicePackageDiscountOnOrder) {
|
||||
let price = 0;
|
||||
price += baseMixin.methods.getTotalLineItemPrice(
|
||||
this.servicePackageDiscountParts[0]
|
||||
);
|
||||
getDiscountPrice(packageName) {
|
||||
const part = this.getDiscountPartForSelectedPackage(packageName);
|
||||
|
||||
if (part) {
|
||||
const price = baseMixin.methods.getTotalLineItemPrice(part);
|
||||
|
||||
return Math.abs(price);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
getVapsPrice(packageName, applyPromoDiscounts = false) {
|
||||
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
|
||||
|
|
@ -398,13 +385,18 @@ export default {
|
|||
|
||||
return vapsLineItemsForSelectedPackage;
|
||||
},
|
||||
getServicePackageDiscountPartForSelectedPackage(packageName) {
|
||||
const selectedPackage = this.servicePackageAnswers.filter(
|
||||
(item) => item.value === packageName
|
||||
);
|
||||
if (selectedPackage?.[0]?.additionalButtonData?.servicePackageDiscount) {
|
||||
return this.servicePackageDiscountParts;
|
||||
} else return [];
|
||||
getDiscountPartForSelectedPackage(packageName) {
|
||||
if (this.availableQuotePageDiscounts?.length > 0) {
|
||||
const match = this.availableQuotePageDiscounts.find(
|
||||
(discountInfo) => discountInfo.packageLevel === packageName
|
||||
);
|
||||
return match?.item;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
hasDiscountForPackage(packageName) {
|
||||
return !!this.getDiscountPartForSelectedPackage(packageName);
|
||||
},
|
||||
combineLineItemsWithoutDuplicates(lineItemsOne, lineItemsTwo) {
|
||||
const combinedLineItemArray = [...lineItemsTwo];
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
|
||||
for="testradio">
|
||||
<div class="package-specs">
|
||||
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
|
||||
<div v-if="this.additionalButtonData.hasDiscount" class="row">
|
||||
<div class="col md-6">
|
||||
<p class="m-0">
|
||||
<span v-html="this.buttonLabel"></span>
|
||||
|
|
@ -77,10 +77,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
this.additionalButtonData.servicePackageDiscount &&
|
||||
this.additionalButtonData.Text
|
||||
"
|
||||
v-if="this.additionalButtonData.hasDiscount && this.additionalButtonData.Text"
|
||||
class="special-save-box">
|
||||
<span v-html="this.additionalButtonData.Text"></span>
|
||||
</div>
|
||||
|
|
@ -297,12 +294,11 @@ export default {
|
|||
|
||||
.pricing-info .insurance-pricing span {
|
||||
font-size: 0.875rem;
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-weight: 500;
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: Urbanist, Arial, Helvetica, sans-serif;
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
|
@ -315,7 +311,6 @@ export default {
|
|||
color: $green;
|
||||
font-size: 0.875rem;
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
@include media-breakpoint-up(md) {
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
selectedAppontment: {
|
||||
selectedAppointment: {
|
||||
estimatedServiceMinutes: {
|
||||
minimum: null,
|
||||
maximum: null,
|
||||
|
|
@ -58,6 +58,7 @@ export default {
|
|||
timeSlot: null,
|
||||
date: null,
|
||||
},
|
||||
confirmedAppointment: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -67,21 +68,25 @@ export default {
|
|||
this.modal.openModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.pushEvent();
|
||||
this.$emit("close-mobile-first-modal");
|
||||
this.confirmedAppointment = false;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
confirmAppointment() {
|
||||
let selectedTimeSlot = {
|
||||
timeSlot: this.selectedAppontment.timeSlot,
|
||||
routeCode: this.selectedAppontment.timeSlot.id,
|
||||
timeSlot: this.selectedAppointment.timeSlot,
|
||||
routeCode: this.selectedAppointment.timeSlot.id,
|
||||
date: this.selectedAppointment.date,
|
||||
};
|
||||
this.confirmedAppointment = true;
|
||||
this.$emit("confirm-appointment", selectedTimeSlot);
|
||||
this.modal.closeModal();
|
||||
},
|
||||
setSelectedAppointment(appointment) {
|
||||
this.selectedAppontment = appointment;
|
||||
this.selectedAppointment = appointment;
|
||||
},
|
||||
getIsModalOpen() {
|
||||
return this.isModalOpen;
|
||||
|
|
@ -89,6 +94,21 @@ export default {
|
|||
setIsModalOpen(isOpen) {
|
||||
this.isModalOpen = isOpen;
|
||||
},
|
||||
pushEvent() {
|
||||
let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO;
|
||||
if (this.selectedAppointment && this.selectedAppointment.timeSlot) {
|
||||
this.pushEventToGA(
|
||||
this.GaCategories.CONFIRMATION_CLICKED,
|
||||
gaAction,
|
||||
this.selectedAppointment.timeSlot.startTime +
|
||||
"-" +
|
||||
this.selectedAppointment.timeSlot.endTime +
|
||||
" " +
|
||||
this.selectedAppointment.date,
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalName() {
|
||||
|
|
@ -111,37 +131,37 @@ export default {
|
|||
if (token.includes(INLINE_SERVICETYPE_TOKEN)) {
|
||||
const serviceType = "<strong>" + this.getServiceType + "</strong>";
|
||||
tokens.splice(tokens.indexOf(token), 1, serviceType);
|
||||
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppontment.date) {
|
||||
} else if (token.includes(INLINE_DAY_TOKEN) && this.selectedAppointment.date) {
|
||||
const day =
|
||||
"<strong>" +
|
||||
new Date(this.selectedAppontment.date).toLocaleDateString("en-US", {
|
||||
new Date(this.selectedAppointment.date).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
timeZone: "UTC",
|
||||
}) +
|
||||
"</strong>";
|
||||
tokens.splice(tokens.indexOf(token), 1, day);
|
||||
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppontment.date) {
|
||||
const date = new Date(this.selectedAppontment.date).toLocaleDateString(
|
||||
} else if (token.includes(INLINE_DATE_TOKEN) && this.selectedAppointment.date) {
|
||||
const date = new Date(this.selectedAppointment.date).toLocaleDateString(
|
||||
"en-US",
|
||||
{ timeZone: "UTC", weekday: "long", month: "long", day: "numeric" }
|
||||
);
|
||||
tokens.splice(tokens.indexOf(token), 1, date);
|
||||
} else if (
|
||||
token.includes(INLINE_TIMESLOT_TOKEN) &&
|
||||
this.selectedAppontment.timeSlot
|
||||
this.selectedAppointment.timeSlot
|
||||
) {
|
||||
const timeslot =
|
||||
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.startTime) +
|
||||
this.get12HourTimeFormat(this.selectedAppointment.timeSlot.startTime) +
|
||||
" - " +
|
||||
this.get12HourTimeFormat(this.selectedAppontment.timeSlot.endTime);
|
||||
this.get12HourTimeFormat(this.selectedAppointment.timeSlot.endTime);
|
||||
tokens.splice(tokens.indexOf(token), 1, timeslot);
|
||||
} else if (
|
||||
token.includes(INLINE_SERVICE_LENGTH_TOKEN) &&
|
||||
this.selectedAppontment.estimatedServiceMinutes
|
||||
this.selectedAppointment.estimatedServiceMinutes
|
||||
) {
|
||||
const inshopDurationTime = getDisplayTextForDurationLength(
|
||||
this.selectedAppontment.estimatedServiceMinutes.minimum,
|
||||
this.selectedAppontment.estimatedServiceMinutes.maximum
|
||||
this.selectedAppointment.estimatedServiceMinutes.minimum,
|
||||
this.selectedAppointment.estimatedServiceMinutes.maximum
|
||||
);
|
||||
tokens.splice(tokens.indexOf(token), 1, inshopDurationTime);
|
||||
}
|
||||
|
|
|
|||
223
src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue
Normal file
223
src/layouts/schedule/recal-ack-modal/recal-ack-modal.vue
Normal 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>
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
||||
<loadingModal ref="loadingModal" />
|
||||
<loadingModal notFullScreen ref="loadingModal" />
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||
<mobileFirstModal
|
||||
modalWidgetName="MobileFirstModalWidget"
|
||||
ref="mobileFirstModal"
|
||||
@confirm-appointment="updateTimeSlotandNavigateForward" />
|
||||
<recalAckModal
|
||||
modalWidgetName="RecalAckModalWidget"
|
||||
agreementWidgetName="RecalAgreementQuestionWidget"
|
||||
groupName="recalAckGroup"
|
||||
@recalAcknowledged="updateRecalAcknowledgedAndNavigateForward"
|
||||
ref="recalAckModal" />
|
||||
<div
|
||||
class="container page-container-grouped-styles"
|
||||
:class="[isMobileFirstModalOpen ? 'hidden-background' : '']">
|
||||
|
|
@ -190,6 +196,7 @@ import shopLocation from "@/layouts/schedule/shop-location/shop-location";
|
|||
import timeSlotQuestion from "@/layouts/schedule/time-slot-question/time-slot-question.vue";
|
||||
import durationTextBlock from "@/layouts/schedule/duration-text-block/duration-text-block.vue";
|
||||
import mobileFirstModal from "@/layouts/schedule/mobile-first-modal/mobile-first-modal.vue";
|
||||
import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
|
||||
|
||||
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
|
||||
import shopQuestionPopup from "@/layouts/service-location/shop-question/shop-question-popup";
|
||||
|
|
@ -216,6 +223,7 @@ import {
|
|||
RouteCodeFlags,
|
||||
PREMIUM_FEE_PART_TYPE,
|
||||
PRICING_BY_DAY_PART_TYPE,
|
||||
RECAL_ACK_YES,
|
||||
} from "@/constants/schedule-constants";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
|
|
@ -242,6 +250,7 @@ import {
|
|||
isDropOffRouteCode,
|
||||
getTodayDate,
|
||||
} from "@/layouts/schedule/helpers/schedule-helper";
|
||||
import { containsRecalParts, anyPartWithRequiresRecalFlag } from "@/helpers/recal-helper";
|
||||
|
||||
import { DAYS_OF_WEEK } from "@/digital-components/date-picker/mixins/constants";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
|
@ -287,6 +296,7 @@ const getScheduleApiResponse = async ({
|
|||
startDateString,
|
||||
endDateString,
|
||||
providerNumber,
|
||||
inshopProviderNumber,
|
||||
zipCode,
|
||||
includeMobileTimeSlots = true,
|
||||
includeInshopTimeSlots = true,
|
||||
|
|
@ -329,7 +339,7 @@ const getScheduleApiResponse = async ({
|
|||
startDate: apiStartDate,
|
||||
endDate: apiEndDate,
|
||||
shopAppointmentType: "InshopOrDropoff",
|
||||
providerNumber: providerNumber,
|
||||
providerNumber: inshopProviderNumber,
|
||||
},
|
||||
};
|
||||
if (apiStartDate < apiEndDate) {
|
||||
|
|
@ -429,6 +439,7 @@ export default {
|
|||
name: "schedule",
|
||||
data() {
|
||||
return {
|
||||
isRecalAcknowledgedForScheduling: this.getIsRecalAcknowledgedForSchedulingFromStore(),
|
||||
selectedDate: this.getSelectedDateFromStore(),
|
||||
selectedTimeSlotInfo: this.getSelectedTimeSlotInfo(),
|
||||
mobilePremiumAppointmentFee: null,
|
||||
|
|
@ -469,6 +480,11 @@ export default {
|
|||
navigatingForward: false,
|
||||
shopListButton: shopListButton,
|
||||
lastSelectedInshopOrDropoffProvider: null,
|
||||
selectedMobileFirstAppointment: false,
|
||||
isShowMobileFirstAppt: experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
),
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -597,7 +613,9 @@ export default {
|
|||
pricedMobileFeePart,
|
||||
shopProviderData.data
|
||||
);
|
||||
vm.initializeDatePicker();
|
||||
vm.initializeDatePicker().then(() => {
|
||||
vm.showMobileFirstModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -614,7 +632,10 @@ export default {
|
|||
},
|
||||
},
|
||||
isMobileSelected() {
|
||||
return this.appointmentType == AppointmentTypeStrings.MOBILE;
|
||||
return (
|
||||
this.appointmentType == AppointmentTypeStrings.MOBILE ||
|
||||
(this.isServiceableMobile && !this.isServiceableInshop)
|
||||
);
|
||||
},
|
||||
isServiceableMobile() {
|
||||
if (this.isRecalibrationServiceableMobile !== null) {
|
||||
|
|
@ -626,6 +647,9 @@ export default {
|
|||
return this.isGlassServiceableMobile;
|
||||
}
|
||||
},
|
||||
closestInshopProviderNumber() {
|
||||
return this.shopProviderData?.shopProviders[0]?.providerNumber;
|
||||
},
|
||||
showTimeSlotQuestion() {
|
||||
if (
|
||||
this.selectedDate &&
|
||||
|
|
@ -858,7 +882,9 @@ export default {
|
|||
);
|
||||
},
|
||||
isMobileFirstModalOpen() {
|
||||
return this.isMobileSelected ? this.$refs.mobileFirstModal?.getIsModalOpen() : false;
|
||||
return this.selectableDatesMobile.days
|
||||
? this.$refs.mobileFirstModal?.getIsModalOpen()
|
||||
: false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -1121,30 +1147,7 @@ export default {
|
|||
);
|
||||
}
|
||||
},
|
||||
getTimeSlotInfo() {
|
||||
// Get the current date
|
||||
const currentDate = getTodayDate();
|
||||
|
||||
// Add 10 days to the current date
|
||||
currentDate.setDate(currentDate.getDate() + 10);
|
||||
|
||||
// Format the date as yyyy-mm-dd
|
||||
const year = currentDate.getFullYear();
|
||||
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(currentDate.getDate()).padStart(2, "0");
|
||||
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
|
||||
return {
|
||||
date: formattedDate,
|
||||
endTime: "12:00",
|
||||
jobMaxMinutes: "180",
|
||||
jobMinMinutes: "120",
|
||||
routeCode: "3357I-03357-M-I*20988*AM",
|
||||
startTime: "08:00",
|
||||
};
|
||||
},
|
||||
onShopSelected(shopQuestionPopUpData) {
|
||||
async onShopSelected(shopQuestionPopUpData) {
|
||||
this.resetWaitlist();
|
||||
this.preSelectedDate = null;
|
||||
this.shopProviderData = shopQuestionPopUpData.shopProviderData;
|
||||
|
|
@ -1152,6 +1155,8 @@ export default {
|
|||
const selectedProvider = this.shopProviderData.shopProviders.find((shopProvider) => {
|
||||
return shopProvider.providerNumber === shopQuestionPopUpData.selectedProviderNumber;
|
||||
});
|
||||
const didProviderNumberChange =
|
||||
oldProvider?.providerNumber !== selectedProvider?.providerNumber;
|
||||
|
||||
// No zipCodeData comes back if zip was not changed
|
||||
if (shopQuestionPopUpData.zipCodeData) {
|
||||
|
|
@ -1160,13 +1165,12 @@ export default {
|
|||
selectedProvider
|
||||
);
|
||||
} else {
|
||||
const didProviderNumberChange =
|
||||
oldProvider?.providerNumber !== selectedProvider.providerNumber;
|
||||
if (didProviderNumberChange) {
|
||||
this.updateSelectedProvider(selectedProvider);
|
||||
this.initializeDatePicker();
|
||||
}
|
||||
}
|
||||
await this.initializeDatePicker();
|
||||
this.setSelectedDateToFirstAvailable();
|
||||
},
|
||||
async getMoreScheduleData(startDate, endDate) {
|
||||
// called only when "View more dates" is clicked; not on initial page load
|
||||
|
|
@ -1174,6 +1178,9 @@ export default {
|
|||
startDateString: startDate,
|
||||
endDateString: endDate,
|
||||
providerNumber: this.selectedProvider?.providerNumber,
|
||||
inshopProviderNumber: this.isMobileSelected
|
||||
? this.closestInshopProviderNumber
|
||||
: this.selectedProvider?.providerNumber,
|
||||
zipCode: this.zipCode,
|
||||
includeMobileTimeSlots: this.isServiceableMobile,
|
||||
includeInshopTimeSlots: this.isServiceableInshop || this.isServiceableDropoff,
|
||||
|
|
@ -1187,12 +1194,12 @@ export default {
|
|||
moreShopTimeSlots.mobileTimeSlotsData.days
|
||||
);
|
||||
|
||||
this.showMobileFirstModal();
|
||||
|
||||
return moreShopTimeSlots;
|
||||
},
|
||||
|
||||
async initializeDatePicker() {
|
||||
this.isShowMobileFirstAppt && this.showLoadingModal();
|
||||
|
||||
this.selectedDate = null;
|
||||
|
||||
const includeMobileTimeSlots = this.isServiceableMobile;
|
||||
|
|
@ -1204,6 +1211,9 @@ export default {
|
|||
getSelectableDatesCallback: getScheduleApiResponse,
|
||||
preSelectedDate: this.preSelectedDate,
|
||||
providerNumber: this.selectedProvider?.providerNumber,
|
||||
inshopProviderNumber: this.isMobileSelected
|
||||
? this.closestInshopProviderNumber
|
||||
: this.selectedProvider?.providerNumber,
|
||||
zipCode: this.zipCode,
|
||||
includeMobileTimeSlots: includeMobileTimeSlots,
|
||||
includeInshopTimeSlots: includeInshopTimeSlots,
|
||||
|
|
@ -1230,6 +1240,11 @@ export default {
|
|||
this.setDisplayWaitList();
|
||||
}
|
||||
}
|
||||
|
||||
this.isShowMobileFirstAppt && this.hideLoadingModal();
|
||||
},
|
||||
getIsRecalAcknowledgedForSchedulingFromStore() {
|
||||
return store.getters.order.isRecalAcknowledgedForScheduling;
|
||||
},
|
||||
getScheduleApiResponse,
|
||||
getServiceZipCtuCodeFromStore() {
|
||||
|
|
@ -1346,7 +1361,28 @@ export default {
|
|||
showLoadingModal() {
|
||||
this.$refs.loadingModal.showModal();
|
||||
},
|
||||
hideLoadingModal() {
|
||||
this.$refs.loadingModal.hideModal();
|
||||
},
|
||||
async closeRecalAcknowledgementPopup() {
|
||||
this.showRecalAcknowledgementPopup = false;
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
const showRecalAcknowledgementModal = this.showRecalAcknowledgementModal();
|
||||
if (
|
||||
showRecalAcknowledgementModal &&
|
||||
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
|
||||
) {
|
||||
this.showRecalAcknowledgementPopup = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchStoreAction(
|
||||
storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
|
||||
this.isRecalAcknowledgedForScheduling,
|
||||
false
|
||||
);
|
||||
|
||||
this.navigatingForward = true;
|
||||
var ctu = this.zipCodeCtu;
|
||||
if (this.appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||
|
|
@ -1648,6 +1684,8 @@ export default {
|
|||
this.updateSelectedProvider();
|
||||
},
|
||||
updateSelectedProvider(newShopProvider) {
|
||||
if (!this.appointmentTypeStrings) return;
|
||||
|
||||
if (newShopProvider) {
|
||||
this.selectedProvider = newShopProvider;
|
||||
} else if (this.appointmentType === this.appointmentTypeStrings.MOBILE) {
|
||||
|
|
@ -1668,7 +1706,27 @@ export default {
|
|||
this.selectedProvider = this.shopProviderData.shopProviders[0];
|
||||
}
|
||||
},
|
||||
handleZipCodeChange(newZipCode) {
|
||||
async handleZipCodeChange(newZipCode) {
|
||||
// if we were showing the recal ack modal and now are changing the zip, we need to navigate
|
||||
// to service-zip so we can get call parts again to add the recal part if needed
|
||||
if (this.shouldShowRecalAckModal()) {
|
||||
await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
||||
{
|
||||
zipCode: newZipCode.zipCode,
|
||||
state: newZipCode.state,
|
||||
zipCodeCtu: newZipCode.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
|
||||
this.pageName
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetMobileLocation();
|
||||
|
||||
getShopProviderData(newZipCode.zipCode, this.pageName).then((result) => {
|
||||
|
|
@ -1678,7 +1736,9 @@ export default {
|
|||
this.zipCode = newZipCode.zipCode;
|
||||
this.zipCodeCtu = newZipCode.zipCodeCtu;
|
||||
this.selectedDate = null;
|
||||
this.initializeDatePicker();
|
||||
this.initializeDatePicker().then(() => {
|
||||
this.showMobileFirstModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
updateSelectedProviderAndZipCode(newZipCode, newProvider) {
|
||||
|
|
@ -1689,7 +1749,6 @@ export default {
|
|||
this.zipContainsMilitaryBase = newZipCode.containsMilitaryBase;
|
||||
this.selectedDate = null;
|
||||
this.selectedProvider = newProvider;
|
||||
this.initializeDatePicker();
|
||||
},
|
||||
handleAppointmentTypeChange(newAppointmentType) {
|
||||
this.updateFooterButtonText();
|
||||
|
|
@ -1716,7 +1775,6 @@ export default {
|
|||
}
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.updateSelectedProvider();
|
||||
this.showMobileFirstModal();
|
||||
} else if (newAppointmentType) {
|
||||
if (this.appointmentType != AppointmentTypeStrings.MOBILE) {
|
||||
// Clear last shop selected if appointment type was changed in any manner other than from Mobile
|
||||
|
|
@ -1731,7 +1789,11 @@ export default {
|
|||
this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF;
|
||||
}
|
||||
// make sure a selectedProvider exists
|
||||
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
|
||||
if (this.lastSelectedInshopOrDropoffProvider) {
|
||||
this.updateSelectedProvider(this.lastSelectedInshopOrDropoffProvider);
|
||||
} else {
|
||||
this.updateSelectedProvider();
|
||||
}
|
||||
} else {
|
||||
this.appointmentType = null;
|
||||
}
|
||||
|
|
@ -1741,12 +1803,34 @@ export default {
|
|||
setSelectedDateToFirstAvailable() {
|
||||
this.selectedDate = this.getFirstAvailableDate();
|
||||
},
|
||||
showRecalAcknowledgementModal() {
|
||||
if (this.shouldShowRecalAckModal()) {
|
||||
this.$refs.navbar.removeLoader();
|
||||
this.$refs.recalAckModal.openModal();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.closeRecalAcknowledgementPopup();
|
||||
return false;
|
||||
},
|
||||
shouldShowRecalAckModal() {
|
||||
// if any of the glass parts contain an item with the requiresRecalibration flag set to true
|
||||
// and the order does not contain a recalibration part, show the recalibration acknowledgement modal
|
||||
const order = this.$store.getters.order;
|
||||
const containsRecalPart = containsRecalParts(order.lineItems);
|
||||
const hasPartWithRecalRequirement = anyPartWithRequiresRecalFlag(order.lineItems);
|
||||
|
||||
if (
|
||||
!containsRecalPart &&
|
||||
hasPartWithRecalRequirement &&
|
||||
this.isRecalAcknowledgedForScheduling !== RECAL_ACK_YES
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async showMobileFirstModal() {
|
||||
if (this.appointmentType == AppointmentTypeStrings.MOBILE) {
|
||||
const isShowMobileFirstAppt = experimentMixin.methods.hasSettingEqualTo(
|
||||
experimentSettings.SHOW_MOBILE_FIRST_APPT,
|
||||
"true"
|
||||
);
|
||||
if (!this.selectedRouteCodeData?.routeCode) {
|
||||
const pmMobileDays = experimentMixin.methods.getSettingValue(
|
||||
experimentSettings.SHOW_PM_MOBILE_DAYS
|
||||
);
|
||||
|
|
@ -1801,32 +1885,40 @@ export default {
|
|||
);
|
||||
}
|
||||
|
||||
if (isShowMobileFirstAppt) {
|
||||
if (firstMobilePMDate && numberOfDaysToFirstMobilePMDate <= pmMobileDays) {
|
||||
if (this.isShowMobileFirstAppt) {
|
||||
if (pmMobileDays == 0) {
|
||||
// Selects the first available time slot
|
||||
preSelectedMobileAppointment =
|
||||
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
|
||||
? firstMobileAMAppt
|
||||
: firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate <= pmMobileDays
|
||||
) {
|
||||
// Selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobilePMAppt;
|
||||
} else if (
|
||||
firstMobilePMDate &&
|
||||
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
|
||||
) {
|
||||
// Selects the first available AM time slot. If none, selects the first available PM time slot
|
||||
preSelectedMobileAppointment = firstMobileAMAppt
|
||||
? firstMobileAMAppt
|
||||
: null;
|
||||
!preSelectedMobileAppointment &&
|
||||
(preSelectedMobileAppointment = firstMobilePMAppt);
|
||||
: firstMobilePMAppt;
|
||||
}
|
||||
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
if (preSelectedMobileAppointment) {
|
||||
this.$refs.mobileFirstModal.setSelectedAppointment(
|
||||
preSelectedMobileAppointment
|
||||
);
|
||||
this.$refs.mobileFirstModal.openModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async getFirstAvailableMobileApptByTOD(timeOfDay) {
|
||||
if (!this.selectableDatesMobile.days) {
|
||||
await this.initializeDatePicker();
|
||||
}
|
||||
if (!this.selectableDatesMobile?.days?.length) {
|
||||
return null;
|
||||
} else {
|
||||
|
|
@ -1853,9 +1945,17 @@ export default {
|
|||
}
|
||||
},
|
||||
updateTimeSlotandNavigateForward(timeSlotObj) {
|
||||
this.selectedMobileFirstAppointment = true;
|
||||
this.appointmentType = AppointmentTypeStrings.MOBILE;
|
||||
this.selectedDate = timeSlotObj.date;
|
||||
this.updateSelectedProvider();
|
||||
this.updateTimeSlot(timeSlotObj);
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
|
||||
this.isRecalAcknowledgedForScheduling = isAcknowledged;
|
||||
this.forwardButtonAction();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
appointmentTypeFromAppointmentTypeQuestion: {
|
||||
|
|
@ -1872,7 +1972,7 @@ export default {
|
|||
const hasValueChanged = newValue !== oldValue;
|
||||
const isDateDifferent = (newValue || oldValue) !== selectedDate;
|
||||
|
||||
if (hasValueChanged && isDateDifferent) {
|
||||
if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) {
|
||||
this.selectedTimeSlotInfo = {
|
||||
timeSlot: {
|
||||
date: null,
|
||||
|
|
@ -1901,7 +2001,7 @@ export default {
|
|||
textBlock,
|
||||
timeSlotQuestion,
|
||||
mobileFirstModal,
|
||||
|
||||
recalAckModal,
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
appointmentTypeQuestion,
|
||||
|
|
|
|||
|
|
@ -109,10 +109,10 @@ export default {
|
|||
border-radius: $border-radius-pill;
|
||||
|
||||
a {
|
||||
font-family: UrbanistSemibold;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: $white;
|
||||
font-weight: $font-weight-600;
|
||||
line-height: 1.5rem;
|
||||
padding: 0.75rem 3rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -548,9 +548,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
if (this.isModalOpened) this.closeModal();
|
||||
},
|
||||
watch: {
|
||||
waitListRequested(newVal) {
|
||||
if (newVal) {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,8 @@ export default {
|
|||
false
|
||||
);
|
||||
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false);
|
||||
|
||||
// Navigate to the next page
|
||||
this.navigateForward(matchedParts);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -232,6 +232,21 @@ export default {
|
|||
(part) => `${part.glassLocation}-${part.glassName}`
|
||||
);
|
||||
|
||||
const format = (part) => `${part?.partNumber}-${part?.partType ? part.partType : ""}`;
|
||||
const mapParts = (arr) => (arr ?? []).map(format);
|
||||
|
||||
const glassParts = mapParts(order?.lineItems?.glassParts);
|
||||
const supportingItems = mapParts(order?.lineItems?.supportingItems);
|
||||
const vaps = mapParts(order?.lineItems?.vaps);
|
||||
const childParts = [].concat(
|
||||
...(order?.lineItems?.glassParts ?? []).map((gp) => mapParts(gp?.childParts))
|
||||
);
|
||||
const allParts = [...glassParts, ...supportingItems, ...vaps, ...childParts];
|
||||
|
||||
if (applicationUser?.pageData?.quote?.servicePackageSelected) {
|
||||
allParts.push(applicationUser.pageData.quote.servicePackageSelected);
|
||||
}
|
||||
|
||||
var appointment = `${order?.schedule?.date ?? ""} ${order?.schedule?.startTime ?? ""}`;
|
||||
|
||||
var sessionData = {};
|
||||
|
|
@ -251,7 +266,7 @@ export default {
|
|||
? "Insurance"
|
||||
: "Cash";
|
||||
sessionData.damageType = order?.damage?.isRepair ? "Repair" : "Replace";
|
||||
sessionData.productType = glassProducts;
|
||||
sessionData.productType = allParts;
|
||||
sessionData.eon = order?.eon;
|
||||
sessionData.referralNumber = order?.referralNumber;
|
||||
sessionData.referralSequenceNumber = order?.referralSequenceNumber;
|
||||
|
|
@ -958,6 +973,8 @@ function getPageNameFromRouter() {
|
|||
) {
|
||||
return router.currentRoute.value.name;
|
||||
}
|
||||
|
||||
return window.location.href.replace(/\/$/, "").split("/").pop();
|
||||
}
|
||||
|
||||
function getValueToLog(value, valueToLogType) {
|
||||
|
|
|
|||
|
|
@ -129,9 +129,9 @@ export default {
|
|||
});
|
||||
return lineItemsArray;
|
||||
},
|
||||
filterOutServicePackageDiscountPart(lineItems) {
|
||||
filterOutQuotePageDiscountPart(lineItems) {
|
||||
const filteredLineItems = lineItems?.filter((item) => {
|
||||
return !item?.partType?.includes(partTypeStrings.SERVICE_PACKAGE_DISCOUNT);
|
||||
return !item?.partType?.includes(partTypeStrings.QUOTE_PAGE_DISCOUNT);
|
||||
});
|
||||
return filteredLineItems;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,10 +57,12 @@ const navigationScenarios = {
|
|||
|
||||
// Schedule
|
||||
CLICKED_CHANGE_LOCATION: "CLICKED_CHANGE_LOCATION",
|
||||
ZIP_CODE_CHANGED_RECAL_ACK: "ZIP_CODE_CHANGED_RECAL_ACK",
|
||||
|
||||
// Payment
|
||||
CLICKED_INSURANCE: "CLICKED_INSURANCE",
|
||||
CLICKED_PAY_NOW: "CLICKED_PAY_NOW",
|
||||
CLICKED_PAY_NOW_ADYEN: "CLICKED_PAY_NOW_ADYEN",
|
||||
CLICKED_PAY_LATER: "CLICKED_PAY_LATER",
|
||||
PIA_ERROR: "PIA_ERROR",
|
||||
PIA_CC_ERROR: "PIA_CC_ERROR",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,15 @@ export const routeData = {
|
|||
name: "payment-pia-return",
|
||||
path: "/payment-pia-return",
|
||||
},
|
||||
PAYMENT_ADYEN: {
|
||||
name: "payment-adyen",
|
||||
path: "/payment-adyen",
|
||||
},
|
||||
PAYMENT_ADYEN_RETURN: {
|
||||
name: "adyen-return",
|
||||
path: "/virtual/payment/return",
|
||||
virtual: true,
|
||||
},
|
||||
CONFIRMATION: {
|
||||
name: "confirmation",
|
||||
path: "/confirmation",
|
||||
|
|
|
|||
|
|
@ -486,6 +486,10 @@ const routingTable = function () {
|
|||
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
|
||||
destinationPageData: routeData.MOBILE_DETAILS,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
|
||||
destinationPageData: routeData.SERVICE_ZIP,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -537,6 +541,10 @@ const routingTable = function () {
|
|||
scenario: navigationScenarios.CLICKED_PAY_NOW,
|
||||
destinationPageData: routeData.PAYMENT,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN,
|
||||
destinationPageData: routeData.PAYMENT_ADYEN,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_INSURANCE,
|
||||
destinationPageData: routeData.INSURANCE_COMPANY,
|
||||
|
|
@ -563,6 +571,26 @@ const routingTable = function () {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pageName: routeData.PAYMENT_ADYEN.name,
|
||||
maps: [
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationPageData: routeData.PAYMENT_METHOD,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationPageData: routeData.CONFIRMATION,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.PIA_ERROR,
|
||||
destinationPageData: routeData.PAYMENT_METHOD,
|
||||
params: {
|
||||
piaError: "true",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
pageName: routeData.PAYMENT_PIA_RETURN.name,
|
||||
maps: [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
import store from "@/store";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
|
||||
export async function afterEach(to, from) {
|
||||
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
|
||||
|
||||
// digital consumer logging
|
||||
analyticsMixin.methods.logDigitalConsumer();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,39 @@ import { queryStrings } from "@/constants/query-strings";
|
|||
import store from "@/store";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import { updateOrCreateFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
|
||||
export async function consumeReferralQuerystrings() {
|
||||
const referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
|
||||
const parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
|
||||
const correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
|
||||
let referralNumber = getQuerystringParameter(queryStrings.REFERRAL_NUMBER);
|
||||
let parentAccount = getQuerystringParameter(queryStrings.PARENT_ACCOUNT);
|
||||
let correlationId = getQuerystringParameter(queryStrings.CORRELATION_ID);
|
||||
let referralDate = null;
|
||||
let savedSessionId = null;
|
||||
|
||||
// If no referral number in querystring, check if heritage funnel updated last.
|
||||
// This would indicate a customer that went to heritage but did not return through the
|
||||
// normal route. ie: left the site in search of a discount and returned without querystrings.
|
||||
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
|
||||
if (didHeritageFunnelUpdateLast && !referralNumber) {
|
||||
referralNumber = getFunnelCookie().ReferralNumber;
|
||||
parentAccount = getFunnelCookie().ReferralParentAccountNumber;
|
||||
correlationId = getFunnelCookie().ReferralCorrelationId;
|
||||
referralDate = getFunnelCookie().ReferralDate;
|
||||
savedSessionId = getFunnelCookie().SavedSessionId;
|
||||
}
|
||||
|
||||
if (referralNumber) {
|
||||
store.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
|
||||
store.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, parentAccount);
|
||||
store.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, correlationId);
|
||||
// log(" --update cookie");
|
||||
|
||||
if (referralDate) {
|
||||
store.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
|
||||
}
|
||||
if (savedSessionId) {
|
||||
store.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
|
||||
}
|
||||
|
||||
updateOrCreateFunnelCookie();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import { queryStrings } from "@/constants/query-strings";
|
|||
import { consumeReferralQuerystrings } from "@/router/methods/helpers/consume-referral-info";
|
||||
import { loadSessionIfPresent } from "@/helpers/heritage-integration/order-helper";
|
||||
import { routeData } from "@/router/constants/routes";
|
||||
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||
|
||||
export async function handleHeritageReturn(to, from) {
|
||||
const fromHeritageFlag = to.query[queryStrings.FROM_HERITAGE];
|
||||
const didHeritageFunnelUpdateLast = getFunnelCookie()?.DidHeritageFunnelUpdateLast;
|
||||
|
||||
if (fromHeritageFlag) {
|
||||
if (fromHeritageFlag || didHeritageFunnelUpdateLast) {
|
||||
// Get new referral info from querystring in case passed there.
|
||||
await consumeReferralQuerystrings();
|
||||
|
||||
|
|
|
|||
164
src/router/methods/route-logic/adyen-return.js
Normal file
164
src/router/methods/route-logic/adyen-return.js
Normal 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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import router from "@/router";
|
|||
import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
|
||||
import store from "@/store";
|
||||
import { handleHardError } from "@/router/methods/error";
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
|
||||
export async function errorBeforeEnter(to, from) {
|
||||
// Check if `hasAlreadyTriggeredErrror` is available.
|
||||
|
|
@ -18,6 +19,7 @@ export async function errorBeforeEnter(to, from) {
|
|||
nextPage: to?.name,
|
||||
};
|
||||
|
||||
analyticsMixin.methods.logDigitalConsumer();
|
||||
await handleHardError(errorPayload);
|
||||
return;
|
||||
}
|
||||
|
|
@ -31,6 +33,7 @@ export async function errorBeforeEnter(to, from) {
|
|||
nextPage: to?.name,
|
||||
};
|
||||
|
||||
analyticsMixin.methods.logDigitalConsumer();
|
||||
await handleHardError(errorPayload);
|
||||
return;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { restartBeforeEnter } from "@/router/methods/route-logic/restart";
|
|||
import { paymentMethodBeforeEnter } from "@/router/methods/route-logic/payment-method";
|
||||
import { paymentBeforeEnter } from "@/router/methods/route-logic/payment";
|
||||
import { serviceLocationBeforeEnter } from "@/router/methods/route-logic/service-location";
|
||||
import { adyenReturnBeforeEnter } from "@/router/methods/route-logic/adyen-return";
|
||||
|
||||
export const routes = [
|
||||
// Non-virtual pages.
|
||||
|
|
@ -36,6 +37,7 @@ export const routes = [
|
|||
createRoute(routeData.PAYMENT_METHOD, paymentMethodBeforeEnter),
|
||||
createRoute(routeData.PAYMENT, paymentBeforeEnter),
|
||||
createRoute(routeData.PAYMENT_PIA_RETURN),
|
||||
createRoute(routeData.PAYMENT_ADYEN),
|
||||
createRoute(routeData.CONFIRMATION),
|
||||
createRoute(routeData.RETURN_USER),
|
||||
createRoute(routeData.MOBILE_DETAILS),
|
||||
|
|
@ -47,4 +49,5 @@ export const routes = [
|
|||
createVirtualRoute(routeData.AUTO_ROUTE, autoRouteBeforeEnter),
|
||||
createVirtualRoute(routeData.ERROR, errorBeforeEnter),
|
||||
createVirtualRoute(routeData.RESTART, restartBeforeEnter),
|
||||
createVirtualRoute(routeData.PAYMENT_ADYEN_RETURN, adyenReturnBeforeEnter),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ const getDefaultState = () => {
|
|||
capabilityQuestionAnswers: null,
|
||||
dateOfLoss: null,
|
||||
damageCause: null,
|
||||
installOemGlass: null,
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: null,
|
||||
|
|
@ -189,6 +190,7 @@ const getDefaultState = () => {
|
|||
lockToken: null,
|
||||
settledTenderAmount: 0,
|
||||
isRecalAckOptIn: false,
|
||||
isRecalAcknowledgedForScheduling: "",
|
||||
isMSRFeeApplicable: false,
|
||||
},
|
||||
applicationUser: {
|
||||
|
|
@ -342,6 +344,12 @@ export const mutations = {
|
|||
updateIsRecalAckOptIn(state, isRecalAckOptIn) {
|
||||
state.order.isRecalAckOptIn = isRecalAckOptIn;
|
||||
},
|
||||
updateIsRecalAcknowledgedForScheduling(state, isRecalAcknowledgedForScheduling) {
|
||||
state.order.isRecalAcknowledgedForScheduling = isRecalAcknowledgedForScheduling;
|
||||
},
|
||||
updateIsOemGlassSelected(state, isOemGlassSelected) {
|
||||
state.order.damage.installOemGlass = isOemGlassSelected;
|
||||
},
|
||||
updateIsMSRFeeApplicable(state, isMSRFeeApplicable) {
|
||||
state.order.isMSRFeeApplicable = isMSRFeeApplicable;
|
||||
},
|
||||
|
|
@ -712,6 +720,8 @@ export const mutations = {
|
|||
state.order.damage.capabilityQuestionAnswers =
|
||||
sessionInformation.order.damage.capabilityQuestionAnswers;
|
||||
|
||||
state.order.damage.installOemGlass = sessionInformation.order.damage.installOemGlass;
|
||||
|
||||
state.order.lineItems.glassParts = sessionInformation.order.lineItems.glassParts;
|
||||
state.order.lineItems.supportingItems = sessionInformation.order.lineItems.supportingItems;
|
||||
state.order.lineItems.vaps = sessionInformation.order.lineItems.vaps ?? [];
|
||||
|
|
@ -1765,7 +1775,8 @@ export const actions = {
|
|||
const glassArray = damage.glassToReplace;
|
||||
const zipCode = order.serviceLocation.zipCode;
|
||||
const vin = vehicle.vin;
|
||||
const serviceType = order.serviceLocation?.appointmentType;
|
||||
const serviceType = damage.isRepair ? "Repair" : "Install";
|
||||
const parentAccountNumber = context.getters.payment.parentAccountNumber;
|
||||
const referralSeqNumber = order.referralSequenceNumber;
|
||||
|
||||
// create a new array to avoid mutating state
|
||||
|
|
@ -1781,6 +1792,7 @@ export const actions = {
|
|||
vin: vin,
|
||||
serviceType: serviceType,
|
||||
referralSeqNumber: referralSeqNumber,
|
||||
parentAccountNumber: parentAccountNumber,
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
|
|
@ -1836,6 +1848,55 @@ export const actions = {
|
|||
return response;
|
||||
},
|
||||
|
||||
// Parts API Actions
|
||||
async getOemAfterMarketParts(context, { pageNameToLog }) {
|
||||
const vehicle = context.getters.vehicle;
|
||||
const damage = context.getters.damage;
|
||||
const order = context.state.order;
|
||||
|
||||
const carId = vehicle.carId;
|
||||
const glassArray = damage.glassToReplace;
|
||||
const resultsArray = damage.partQuestionAnswers;
|
||||
const zipCode = order.serviceLocation.zipCode;
|
||||
const vin = vehicle.vin;
|
||||
const make = vehicle.make;
|
||||
const serviceType = order.serviceLocation?.appointmentType;
|
||||
const referralSeqNumber = order.referralSequenceNumber;
|
||||
const parentAccountNumber = applicationConfig.CASH_PARENT_ACCOUNT_NUMBER;
|
||||
const oemEndorsementFlag = order.damage.installOemGlass || false;
|
||||
// create a new array to avoid mutating state
|
||||
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
|
||||
const resultsArrayForPayload = convertResultsForApi(resultsArray);
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.GetOemAfterMarketParts.method,
|
||||
endpoint: endpoints.GetOemAfterMarketParts.url,
|
||||
payload: {
|
||||
carId: carId,
|
||||
glassPieces: glassArrayForPayload,
|
||||
answerResults: resultsArrayForPayload,
|
||||
zip: zipCode,
|
||||
vin: vin,
|
||||
serviceType: serviceType,
|
||||
referralSeqNumber: referralSeqNumber,
|
||||
make: make,
|
||||
parentAccountNumber: parentAccountNumber.toString(),
|
||||
oemEndorsementFlag: oemEndorsementFlag,
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
|
||||
// Flatten location and name properties
|
||||
if (response?.data?.glassPieceParts) {
|
||||
response.data.glassPieceParts = convertGlassPieceNamingFromApi(
|
||||
response.data.glassPieceParts
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async getWipers(context, { payload, pageNameToLog }) {
|
||||
const carId = payload.carId;
|
||||
const serviceZipCode = payload.serviceZipCode;
|
||||
|
|
@ -1936,26 +1997,6 @@ export const actions = {
|
|||
return response;
|
||||
});
|
||||
},
|
||||
getServicePackageDiscountPart(context, { pageNameToLog }) {
|
||||
const damage = context.getters.damage;
|
||||
const damageType = damage.isRepair ? "Repair" : "Replace";
|
||||
const glassPieces = convertGlassPieceNamingForApi(damage.glassToReplace);
|
||||
|
||||
return globalMethods
|
||||
.callHttpClient({
|
||||
method: endpoints.GetServicePackageDiscountPart.method,
|
||||
endpoint: endpoints.GetServicePackageDiscountPart.url,
|
||||
payload: {
|
||||
glassPieces: glassPieces,
|
||||
damageType: damageType,
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
})
|
||||
.then((response) => {
|
||||
return response;
|
||||
});
|
||||
},
|
||||
|
||||
getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
|
||||
const escapeRecalibrationType = (rt) => rt.split("&").join("%26");
|
||||
|
|
@ -2363,9 +2404,17 @@ export const actions = {
|
|||
//affiliate cookie save only when submitAfterSave
|
||||
const affiliateCookies = submitAfterSave ? applicationUser.affiliateCookies : null;
|
||||
|
||||
let endPoint = endpoints.SaveSession.url;
|
||||
if (createUnscheduledStatusWorkOrderForPIA) {
|
||||
endPoint = endpoints.SubmitOrderForPia.url;
|
||||
}
|
||||
if (submitAfterSave) {
|
||||
endPoint = endpoints.SubmitOrder.url;
|
||||
}
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.SaveSession.method,
|
||||
endpoint: endpoints.SaveSession.url,
|
||||
endpoint: endPoint,
|
||||
payload: {
|
||||
submitAfterSave: submitAfterSave,
|
||||
userAgent: navigator.userAgent,
|
||||
|
|
@ -2413,6 +2462,7 @@ export const actions = {
|
|||
partQuestionAnswers: order.damage.partQuestionAnswers,
|
||||
moldingQuestionAnswers: order.damage.moldingQuestionAnswers,
|
||||
capabilityQuestionAnswers: order.damage.capabilityQuestionAnswers,
|
||||
installOemGlass: order.damage.installOemGlass,
|
||||
},
|
||||
lineItems: {
|
||||
glassParts: lineItems.glassParts,
|
||||
|
|
@ -2919,6 +2969,17 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_IS_RECAL_ACK_OPT_IN, isRecalAckOptIn);
|
||||
},
|
||||
|
||||
saveIsRecalAcknowledgedForScheduling(context, isRecalAcknowledgedForScheduling) {
|
||||
context.commit(
|
||||
storeMutations.UPDATE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
|
||||
isRecalAcknowledgedForScheduling
|
||||
);
|
||||
},
|
||||
|
||||
saveIsOemGlassSelected(context, isOemGlassSelected) {
|
||||
context.commit(storeMutations.UPDATE_IS_OEM_GLASS_SELECTED, isOemGlassSelected);
|
||||
},
|
||||
|
||||
saveIsMSRFeeApplicable(context, isMSRFeeApplicable) {
|
||||
context.commit(storeMutations.UPDATE_IS_MSR_FEE_APPLICABLE, isMSRFeeApplicable);
|
||||
},
|
||||
|
|
@ -3085,40 +3146,24 @@ export const actions = {
|
|||
promoCode: lineItem.promoCode ?? null,
|
||||
}));
|
||||
|
||||
const pricedLineItemsFormattedForRequest =
|
||||
buildQueryStringParameterFromArrayOfComplexObjects(
|
||||
lineItemsWithOnlyPriceInfo,
|
||||
"lineItems"
|
||||
);
|
||||
|
||||
let queryString = "";
|
||||
if (appointmentType == "Mobile") {
|
||||
queryString =
|
||||
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
|
||||
`&BillToAccountNumber=${billToAccountNumber}` +
|
||||
`&ProviderNumber=${providerNumber}` +
|
||||
`&AppointmentType=${appointmentType}` +
|
||||
`&ServiceLocation.City=${serviceLocationCity}` +
|
||||
`&ServiceLocation.State=${serviceLocationState}` +
|
||||
`&ServiceLocation.ZipCode=${serviceLocationZipCode}` +
|
||||
`&${pricedLineItemsFormattedForRequest}`;
|
||||
} else {
|
||||
queryString =
|
||||
`ParentAccountNumber=${context.getters.order.payment.parentAccountNumber}` +
|
||||
`&BillToAccountNumber=${billToAccountNumber}` +
|
||||
`&ProviderNumber=${providerNumber}` +
|
||||
`&AppointmentType=${appointmentType}` +
|
||||
`&${pricedLineItemsFormattedForRequest}`;
|
||||
}
|
||||
|
||||
const lineItemServerData = context.getters.order.lineItems.serverData;
|
||||
if (lineItemServerData) {
|
||||
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
||||
}
|
||||
const order = context.getters.order;
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.TaxOrderItems.method,
|
||||
endpoint: `${endpoints.TaxOrderItems.url}?${queryString}`,
|
||||
endpoint: endpoints.TaxOrderItems.url,
|
||||
payload: {
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
billToAccountNumber: billToAccountNumber,
|
||||
providerNumber: providerNumber,
|
||||
appointmentType: appointmentType,
|
||||
pricedLineItems: lineItemsWithOnlyPriceInfo,
|
||||
serviceLocation: {
|
||||
city: appointmentType == "Mobile" ? serviceLocationCity : null,
|
||||
state: appointmentType == "Mobile" ? serviceLocationState : null,
|
||||
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
|
||||
},
|
||||
serverData: order.lineItems.serverData ? order.lineItems.serverData : "",
|
||||
},
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ $red-600: #ac160b;
|
|||
$red-700: #840900;
|
||||
$red-800: #5b0600;
|
||||
$red-900: #330300;
|
||||
$red-1000: #a80019;
|
||||
$red-1100: #791710;
|
||||
|
||||
// Greens
|
||||
$green-100: #e3f2ea;
|
||||
|
|
|
|||
|
|
@ -63,13 +63,13 @@ export default {
|
|||
|
||||
<style lang="scss">
|
||||
.btn {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
&.btn-primary {
|
||||
position: relative;
|
||||
background: $red;
|
||||
border: none;
|
||||
color: $white;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
@media (hover: hover) {
|
||||
background: $red;
|
||||
}
|
||||
|
|
@ -92,7 +92,6 @@ export default {
|
|||
background: $gray-200 !important;
|
||||
background: $red !important;
|
||||
color: $white !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
|
@ -116,7 +115,6 @@ export default {
|
|||
background: transparent;
|
||||
border: 1px solid $blue;
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
transition: all 150ms linear;
|
||||
height: 3rem;
|
||||
&:hover {
|
||||
|
|
@ -137,7 +135,6 @@ export default {
|
|||
&:disabled {
|
||||
background: transparent;
|
||||
color: $red !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: 1px solid $red;
|
||||
cursor: pointer;
|
||||
|
|
@ -157,7 +154,6 @@ export default {
|
|||
background: $green-100;
|
||||
border: 1px solid $green-400;
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
transition: all 150ms linear;
|
||||
height: 3rem;
|
||||
&:hover {
|
||||
|
|
@ -177,7 +173,6 @@ export default {
|
|||
&:disabled {
|
||||
background: transparent;
|
||||
color: $gray-550 !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: 1px solid $gray-550;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<!-- prettier-ignore -->
|
||||
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{ text }}<slot name="after-text"></slot></a>
|
||||
<!-- prettier-ignore -->
|
||||
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">{{ text }}<slot name="after-text"></slot></a>
|
||||
<a v-else-if="linkType === 'text'" @click="handleClick" class="text-link" :href="href">{{ text }}<slot name="after-text"></slot></a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -89,5 +89,8 @@ a {
|
|||
&.new-window-link {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
&.text-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue