diff --git a/src/constants/analytics.js b/src/constants/analytics.js
index 8cf147e8..dc023b15 100644
--- a/src/constants/analytics.js
+++ b/src/constants/analytics.js
@@ -1,3 +1,6 @@
+import packageNames from '@/constants/package-names';
+import { paymentMethods } from '@/constants/payment-method-constants';
+
const analyticsPageEvents = Object.freeze({
ENTRY: 'ENTRY',
EVENT: 'EVENT'
@@ -40,4 +43,18 @@ const ValueToLogTypes = Object.freeze({
LAST_5: 'last_5'
});
-export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes };
+const analyticsPaymentTypeMap = new Map([
+ [paymentMethods.AFTERPAY, 'after_pay'],
+ [paymentMethods.CREDIT_CARD, 'credit_card'],
+ [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'],
+ [paymentMethods.PAYPAL, 'pay_pal'],
+ [paymentMethods.APPLEPAY, 'apple_pay'],
+]);
+
+const analyticsServicePackageMap = new Map([
+ [packageNames.TIER_ONE, 'basic'],
+ [packageNames.TIER_TWO, 'essentials'],
+ [packageNames.TIER_THREE, 'essentialsplus']
+]);
+
+export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap };
diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js
index d18d073e..59515bd3 100644
--- a/src/constants/endpoints.js
+++ b/src/constants/endpoints.js
@@ -1,5 +1,3 @@
-import applicationConfig from './application-config';
-
const ACCOUNT_BASE_URL = '/account/api/v1/account';
const ANALYTICS_BASE_URL = '/analytics/api/v1/analytics';
const CLIENT_AUTH_BASE_URL = '/clientauth/api/v1/clientauth';
@@ -8,19 +6,13 @@ const COVERAGE_BASE_URL = '/coverage/api/v1/coverage';
const EXPERIMENTS_BASE_URL = '/experiments/api/v1/experiments';
const LOCATION_BASE_URL = '/location/api/v1/location';
const ORDER_BASE_URL = '/order/api/v1/order';
-const PARTS_BASE_URL = '/parts/api/v2/parts';
const PARTS_V1_BASE_URL = '/parts/api/v1/parts';
+const PARTS_V2_BASE_URL = '/parts/api/v2/parts';
const PRICE_BASE_URL = '/price/api/v1/price';
const SCHEDULE_BASE_URL = '/schedule/api/v1/schedule';
const VEHICLE_BASE_URL = '/vehicle/api/v1/vehicle';
const PAYMENT_BASE_URL = '/payment/api/v1/payment';
-const isLocalOrDev = (() => {
- const currentEnv = (applicationConfig?.CURRENT_ENVIRONMENT || '').toLowerCase();
- return currentEnv === 'localhost' || currentEnv === 'dev';
-})();
-const PARTS_EFFECTIVE_BASE_URL = isLocalOrDev ? PARTS_BASE_URL : PARTS_V1_BASE_URL;
-
const endpoints = Object.freeze({
GetRouteInfo: {
url: (applicationAbbreviation) => `${CONTENT_BASE_URL}/${applicationAbbreviation}/RouteInfo`,
@@ -51,7 +43,7 @@ const endpoints = Object.freeze({
method: 'POST'
},
GetMobilePremiumFee: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/mobile-premium-fee`,
+ url: `${PARTS_V1_BASE_URL}/mobile-premium-fee`,
method: 'GET'
},
GetVehicleYears: {
@@ -71,15 +63,15 @@ const endpoints = Object.freeze({
method: 'GET'
},
GetDamageOptions: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/damage-options`,
+ url: `${PARTS_V1_BASE_URL}/damage-options`,
method: 'GET'
},
GetPartsOrQuestions: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/parts-or-questions`,
+ url: `${PARTS_V1_BASE_URL}/parts-or-questions`,
method: 'POST'
},
GetParts: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/parts`,
+ url: `${PARTS_V1_BASE_URL}/parts`,
method: 'POST'
},
GetITACPriceOrderItems: {
@@ -108,19 +100,19 @@ const endpoints = Object.freeze({
method: 'GET'
},
GetCapabilityQuestions: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/capability-questions`,
+ url: `${PARTS_V1_BASE_URL}/capability-questions`,
method: 'GET'
},
GetPartFromCapabilityAnswer: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/part-from-capability-answer`,
+ url: `${PARTS_V1_BASE_URL}/part-from-capability-answer`,
method: 'POST'
},
GetWipers: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/wipers`,
+ url: `${PARTS_V1_BASE_URL}/wipers`,
method: 'GET'
},
GetRainDefense: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/rain-repel`,
+ url: `${PARTS_V1_BASE_URL}/rain-repel`,
method: 'GET'
},
GetRecalParts: {
@@ -132,19 +124,19 @@ const endpoints = Object.freeze({
zipCode,
applicationName,
referralSequenceNumber
- ) => `${PARTS_EFFECTIVE_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`,
+ ) => `${PARTS_V1_BASE_URL}/recal-parts/${carId}/${partNumber}/${recalibrationType}/${parentAccountNumber}/${zipCode}/${applicationName}/${referralSequenceNumber}`,
method: 'GET'
},
GetGlassFees: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/glass-fees`,
+ url: `${PARTS_V1_BASE_URL}/glass-fees`,
method: 'GET'
},
GetSupportingItems: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/supporting-items`,
+ url: `${PARTS_V1_BASE_URL}/supporting-items`,
method: 'POST'
},
GetMobileFeePart: {
- url: `${PARTS_EFFECTIVE_BASE_URL}/mobile-fee`,
+ url: `${PARTS_V2_BASE_URL}/mobile-fee`,
method: 'GET'
},
GetServiceabilityDetails: {
diff --git a/src/constants/part-number-strings.js b/src/constants/part-number-strings.js
index 89f4a97f..55905408 100644
--- a/src/constants/part-number-strings.js
+++ b/src/constants/part-number-strings.js
@@ -2,7 +2,9 @@ const partNumberStrings = Object.freeze({
RECYCLE_FEE: 'RECYCLE FEE',
LABOR2: 'LABOR2',
RECAL_MOBILE: 'RECAL MOBILE',
- RECAL_MOBILEDUAL: 'RECAL MOBILEDUAL'
+ RECAL_MOBILEDUAL: 'RECAL MOBILEDUAL',
+ REPAIR: 'WSREPAIR',
+ SUPPLIES_REPAIR: 'SUPPLIES-REPAIR'
});
export default partNumberStrings;
diff --git a/src/helpers/object-helper.js b/src/helpers/object-helper.js
index 37ee46be..e142c236 100644
--- a/src/helpers/object-helper.js
+++ b/src/helpers/object-helper.js
@@ -86,3 +86,17 @@ export function getPropertyCaseInsensitive(obj, property) {
while (prop = props.pop()) if (prop.toLowerCase() === property.toLowerCase()) return prop;
return null;
}
+
+export function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
+ return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
+}
+
+export function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) {
+ if (!arrayOfObjects) return null;
+
+ return arrayOfObjects.sort((a, b) => {
+ if (a[propertyName] < b[propertyName]) return -1;
+ if (a[propertyName] > b[propertyName]) return 1;
+ return 0;
+ });
+}
diff --git a/src/helpers/recal-helper.js b/src/helpers/recal-helper.js
index f364bf68..029aee25 100644
--- a/src/helpers/recal-helper.js
+++ b/src/helpers/recal-helper.js
@@ -1,5 +1,5 @@
import partTypeStrings from '@/constants/part-type-strings';
-import { deepClone } from '@/helpers/object-helper';
+import { deepClone, getNonFalseValuesOfPropertyInArrayOfObjects } from '@/helpers/object-helper';
const recalPartTypes = [partTypeStrings.RECALIBRATION, partTypeStrings.ADAS_RECALIBRATION];
@@ -73,6 +73,31 @@ export function containsRecalParts(lineItems) {
}
}
+export function isRecalOrder(lineItems) {
+ return (containsRecalParts(lineItems) && getHasRecalibrationPart(lineItems));
+}
+
+export function getHasRecalibrationPart(lineItems) {
+ const hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(lineItems.glassParts, 'requiresRecalibration')?.length > 0;
+ const hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(lineItems.glassParts, 'recalibrationType')?.length > 0;
+
+ if (hasRequiresRecalibration) {
+ if (hasRecalibrationType) {
+ // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType'
+ return (
+ getNonFalseValuesOfPropertyInArrayOfObjects(
+ lineItems.glassParts,
+ 'recalibrationType'
+ )[0].toLowerCase() !== 'unknown'
+ );
+ }
+ // Has 'requiresRecalibration' but no 'recalibrationType' at all
+ return true;
+ }
+ // Does not have 'requiresRecalibration'
+ return false;
+}
+
export function anyPartWithRequiresRecalFlag(lineItems) {
if (!lineItems) {
return false;
diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js
index be64fdc8..9438b038 100644
--- a/src/layouts/order-confirmation/order-confirmation.spec.js
+++ b/src/layouts/order-confirmation/order-confirmation.spec.js
@@ -14,6 +14,8 @@ import coverageType from '@/constants/coverage-type';
import { deepClone } from '@/helpers/object-helper';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import { paymentMethods } from '@/constants/payment-method-constants';
+import partTypeStrings from '@/constants/part-type-strings';
+import packageNames from '@/constants/package-names';
jest.mock('@/helpers/layout-helper.js', () => jest.fn());
@@ -27,7 +29,9 @@ jest.mock('@/helpers/cms-content-helper', () => ({
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
- setCmsContent: jest.fn()
+ setCmsContent: jest.fn(),
+ savePageDataToStore: jest.fn(),
+ pushEventToGA: jest.fn()
}
};
@@ -152,7 +156,8 @@ const sessionStorage = {
currentDeductible: {
replace: 100,
repair: 0
- }
+ },
+ isUnverified: false
};
function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRun = () => {}, mixin = mockMixin) {
@@ -778,7 +783,8 @@ describe('OrderConfirmation.vue', () => {
}
return '';
}),
- setCmsContent: jest.fn()
+ setCmsContent: jest.fn(),
+ savePageDataToStore: jest.fn()
}
};
});
@@ -827,7 +833,8 @@ describe('OrderConfirmation.vue', () => {
}
return '';
}),
- setCmsContent: jest.fn()
+ setCmsContent: jest.fn(),
+ savePageDataToStore: jest.fn()
}
};
});
@@ -955,6 +962,251 @@ describe('OrderConfirmation.vue', () => {
});
});
describe('Methods', () => {
+ describe('handleAnalyticsEvents', () => {
+ let mixin;
+ beforeEach(() => {
+ mixin = {
+ methods: {
+ getCmsContent: jest.fn(),
+ setCmsContent: jest.fn(),
+ savePageDataToStore: jest.fn(),
+ pushEventToGA: jest.fn()
+ }
+ };
+ });
+ test('should call pushEventToGA with confirmation event for mobile repair', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE;
+ order.damage.isRepair = true;
+ order.isVerified = false;
+ order.isMobileAppointment = true;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('mobile'), true);
+ });
+ test('should call pushEventToGA with confirmation event for in-shop replace', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP;
+ order.damage.isRepair = false;
+ order.isVerified = true;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('replace'), true);
+ });
+ test('should call pushEventToGA for payment PIA successful when payment.isPayInAdvance is true', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.payment.isPayInAdvance = true;
+ order.payment.paymentMethod = paymentMethods.CREDIT_CARD;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('payment_page', 'pia_successful', expect.any(String), true);
+ });
+ test('should not call pushEventToGA for PIA when payment.isPayInAdvance is false', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.payment.isPayInAdvance = false;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ const paymentPageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'payment_page');
+ expect(paymentPageCalls.length).toBe(0);
+ });
+ test('should call pushEventToGA for service package when submittedOrder.servicePackage exists', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.servicePackage = packageNames.TIER_ONE;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('service_package', 'package_purchased', expect.any(String), true);
+ });
+ test('should not call pushEventToGA for service package when submittedOrder.servicePackage is null', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.servicePackage = null;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ const servicePackageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'service_package');
+ expect(servicePackageCalls.length).toBe(0);
+ });
+ test('should call pushEventToGA for total price when isFirstLoad is true and cart total greater than 0', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ const expectedTotal = 100;
+ order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }];
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // Act
+ wrapper.vm.handleAnalyticsEvents(true);
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('total_price', expectedTotal.toString(), expect.any(String), true);
+ });
+ test('should not call pushEventToGA for total price when isFirstLoad is false', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ const expectedTotal = 100;
+ order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }];
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ useMainStore().pageData = jest.fn().mockReturnValue({ isFirstLoad: false });
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price');
+ expect(totalPriceCalls.length).toBe(0);
+ });
+ test('should not call pushEventToGA for total price when cart total is 0', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ const expectedTotal = 0;
+ order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }];
+ order.insuranceCoverage.coverageType = coverageType.ITAC;
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price');
+ expect(totalPriceCalls.length).toBe(0);
+ });
+ test('should call pushEventToGA for rain defense purchased when hasRainRepel is true', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.lineItems.vaps = [{ partType: partTypeStrings.RAIN_DEFENSE }];
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'purchased', expect.any(String), true);
+ });
+ test('should call pushEventToGA for rain defense no_purchase when hasRainRepel is false', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.lineItems.vaps = [];
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'no_purchase', expect.any(String), true);
+ });
+ test('should call pushEventToGA for visitor_info_confirmation with ClientSite referring_site', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'referring_site', 'ClientSite', true);
+ });
+ test('should call pushEventToGA for visitor_info_confirmation with client_name', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'client_name', expect.any(String), true);
+ });
+ test('should call pushEventToGA for wipers purchased when hasWipers is true', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.lineItems.vaps = [{ partType: partTypeStrings.FRONT_WIPER }];
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const mixin = {
+ methods: {
+ getCmsContent: jest.fn().mockReturnValue([{ Text: 'Front Beam' }]),
+ setCmsContent: jest.fn(),
+ savePageDataToStore: jest.fn(),
+ pushEventToGA: jest.fn()
+ }
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'purchased', expect.any(String), true);
+ });
+ test('should call pushEventToGA for wipers no_purchase when hasWipers is false', () => {
+ // Arrange
+ const order = deepClone(sessionStorage);
+ order.lineItems.vaps = [];
+ const mockStoreActions = () => {
+ useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order);
+ };
+ const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin);
+
+ // handleAnalyticsEvents is called on mount, no need to act
+
+ // Assert
+ expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true);
+ });
+ });
describe('formatDate', () => {
test.each([
['Wednesday, April 22, 2020', '2020-04-22'],
diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue
index 21ce4fc7..063346f2 100644
--- a/src/layouts/order-confirmation/order-confirmation.vue
+++ b/src/layouts/order-confirmation/order-confirmation.vue
@@ -55,7 +55,7 @@
:isRepair="isRepair" />
{{ wipersTitle }}
{{ description }}
{{ rainRepelTitle }}
{{ rainRepelBody }}
@@ -135,6 +135,10 @@ import widgetFields from '@/constants/cms-widget-fields.js';
import { getGlassList } from '@/helpers/damage-helper';
import partTypeStrings from '@/constants/part-type-strings';
import coverageType from '@/constants/coverage-type';
+import { analyticsPaymentTypeMap, analyticsServicePackageMap } from '@/constants/analytics';
+import { containsRecalParts } from '@/helpers/recal-helper';
+import issPageValues from '@/router/router-constants/issPage-values';
+import { getCartTotal } from '@/helpers/cart-helper';
export default {
name: 'order-confirmation',
@@ -199,6 +203,7 @@ export default {
hasRecalibrationPart,
referralNumber: referralNumber?.toString(),
isNoComp: this.submittedOrder.insuranceCoverage.coverageType === coverageType.NO_COMP,
+ isITAC: this.submittedOrder.insuranceCoverage.coverageType === coverageType.ITAC,
widgets: {
siteHeader: 'SiteHeaderWidget',
emailConfirmation: 'EmailConfirmationWordingWidget',
@@ -418,10 +423,16 @@ export default {
// Temporarily set return true to see cart in localhost or dev environment
return false;
},
- displayWipers() {
+ hasWipers() {
const hasWiperPart = this.submittedOrder.lineItems.vaps.some((part) => part.partType.toLowerCase().includes('wiper'));
return hasWiperPart;
},
+ hasFrontWiper() {
+ return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER);
+ },
+ hasRearWiper() {
+ return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER);
+ },
wipersTitle() {
return this.getCmsContent(
this.widgets.wipersText,
@@ -431,13 +442,10 @@ export default {
wipersBody() {
const wiperTypesOnOrder = [];
- const hasFrontWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.FRONT_WIPER);
- const hasRearWiper = this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.REAR_WIPER);
-
- if (hasFrontWiper) {
+ if (this.hasFrontWiper) {
wiperTypesOnOrder.push(partTypeStrings.FRONT_WIPER);
}
- if (hasRearWiper) {
+ if (this.hasRearWiper) {
wiperTypesOnOrder.push(partTypeStrings.REAR_WIPER);
}
@@ -447,7 +455,7 @@ export default {
return wiperDescriptions;
},
- displayRainRepel() {
+ hasRainRepel() {
return this.submittedOrder.lineItems.vaps.some((part) => part.partType === partTypeStrings.RAIN_DEFENSE);
},
rainRepelTitle() {
@@ -508,6 +516,10 @@ export default {
if (this.carrierUrl) {
this.$refs.siteFooter.updateButtonText(`Go back to ${this.carrierName}`);
}
+
+ const isFirstLoad = this.mainStore.pageData(issPageValues.ORDER_CONFIRMATION)?.isFirstLoad ?? true;
+ this.savePageDataToStore(issPageValues.ORDER_CONFIRMATION, { isFirstLoad: false });
+ this.handleAnalyticsEvents(isFirstLoad);
},
methods: {
arePagePrerequisitesValid() {
@@ -611,6 +623,72 @@ export default {
const vapsTypeDescription = vapsItemDescriptions?.find((entry) => entry?.Name === vapsPartType);
return vapsTypeDescription?.Text ?? '';
+ },
+ handleAnalyticsEvents(isFirstLoad) {
+ const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop';
+ const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified';
+ const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace';
+ this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true);
+
+ if (this.payment.isPayInAdvance) {
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.payment.paymentMethod);
+ this.pushEventToGA('payment_page', 'pia_successful', paymentMethodForAnalytics, true);
+ }
+
+ if (this.submittedOrder.servicePackage) {
+ const servicePackageForAnalytics = analyticsServicePackageMap.get(this.submittedOrder.servicePackage);
+ this.pushEventToGA('service_package', 'package_purchased', servicePackageForAnalytics, true);
+ }
+
+ if (containsRecalParts(this.submittedOrder.lineItems)) {
+ let coverageType = '';
+ if (this.isITAC) {
+ coverageType = 'ITAC';
+ } else if (this.isNoComp) {
+ coverageType = 'no_comp';
+ } else if (this.submittedOrder.isVerified) {
+ coverageType = 'verified';
+ } else {
+ coverageType = 'unverified';
+ }
+ const recalibrationType = this.submittedOrder.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType;
+ this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true);
+ }
+
+ const ymms = `${this.vehicle.year}_${this.vehicle.make}_${this.vehicle.model}_${this.vehicle.style}`;
+ if (isFirstLoad && getCartTotal(this.submittedOrder) > 0) {
+ this.pushEventToGA('total_price', getCartTotal(this.submittedOrder).toString(), ymms, true);
+ }
+
+ this.pushEventToGA('rain_defense', this.hasRainRepel ? 'purchased' : 'no_purchase', ymms, true);
+
+ // TODO: Check if we're coming from SFA and add relevant events
+ // eslint-disable-next-line
+ if (false) {
+ this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'SFA', null);
+ }
+ else {
+ this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'ClientSite', true);
+ }
+
+ this.pushEventToGA('visitor_info_confirmation', 'client_name', this.mainStore.accountNameForEvents, true);
+
+ const wiperAction = this.hasWipers ? 'purchased' : 'no_purchase';
+
+ let wiperLabel = this.wipersBody.join('_').toLowerCase().replace('
', '').replace(/beam/g, '').replace(/blades/g, '').trim().replace(/ /g, '_');
+ if (wiperLabel.indexOf('front') < 0 && wiperLabel.indexOf('rear') < 0) {
+ wiperLabel = 'none_none';
+ } else {
+ if (wiperLabel.indexOf('front') < 0) {
+ wiperLabel = 'front_none_' + wiperLabel;
+ }
+ if (wiperLabel.indexOf('rear') < 0) {
+ wiperLabel = wiperLabel + '_rear_none';
+ }
+ }
+ wiperLabel = wiperLabel.replace(/__/g, '_');
+
+ this.pushEventToGA('wipers', wiperAction, wiperLabel, true);
}
}
};
diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue
index 91462f51..8608bc29 100644
--- a/src/layouts/payment-page-adyen/payment-page-adyen.vue
+++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue
@@ -75,6 +75,7 @@ import { mapAdyenToIssPaymentMethod, mapIssToAdyenPaymentMethod } from '@/helper
import { createAdyenCheckout } from "@/helpers/adyen-helper";
import { Dropin } from "@adyen/adyen-web/auto";
import applicationConfig from '@/constants/application-config';
+import { analyticsPaymentTypeMap } from '@/constants/analytics';
export default {
name: 'payment-page-adyen',
@@ -118,6 +119,7 @@ export default {
},
mounted() {
showIssLoadingModal(true);
+ this.pushEventToGA('payment_page', 'Mode', 'Adyen', true);
this.initializeAdyen().finally(() => {
showIssLoadingModal(false);
});
@@ -353,6 +355,8 @@ export default {
},
async paymentFailedPayLater() {
this.showIssLoadingModal(true);
+ const paymentMethod = analyticsPaymentTypeMap.get(this.piaType);
+ this.pushEventToGA('transaction_declined_displayed', 'save_your_appointment_clicked', `${paymentMethod}_declined`, true);
this.mainStore.savePaymentMethodChoice(paymentMethods.PAY_AT_TIME_OF_SERVICE);
await submitWorkOrder({ submitType: submitType.SAFELITE });
this.$router.navigate(
@@ -523,6 +527,8 @@ export default {
const paymentMethodFromSession = adyenResponse?.paymentMethod;
const paymentMethod = mapAdyenToIssPaymentMethod(paymentMethodFromSession);
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod);
+ this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0);
this.mainStore.savePaymentMethodChoice(paymentMethod);
@@ -544,6 +550,10 @@ export default {
await global.$logger.logError(stringToLog);
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType);
+ this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0);
+ this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode);
+
this.hasPaymentFailureError = true;
}
@@ -556,6 +566,9 @@ export default {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
await global.$logger.logError(stringToLog);
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType);
+ this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0);
+ this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name);
this.hasPaymentFailureError = true;
}
diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue
index d306857d..ce1ae7e5 100644
--- a/src/layouts/payment-return-adyen/payment-return-adyen.vue
+++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue
@@ -18,6 +18,7 @@ import navigationScenarios from '@/router/router-constants/navigation-scenarios'
import submitType from '@/constants/submit-type';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
import { paymentMethods } from '@/constants/payment-method-constants';
+import { analyticsPaymentTypeMap } from '@/constants/analytics';
export default {
name: 'payment-return-adyen',
@@ -58,6 +59,7 @@ export default {
const sessionInfo = await getSessionInfo(sessionId, result.sessionResult);
const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod);
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(paymentMethod);
const amountDue = getCartTotal(store.order);
const ccToken = generateCcToken(sessionInfo);
if (paymentMethod === paymentMethods.AFTERPAY) {
@@ -67,6 +69,7 @@ export default {
ccToken.expMonth = "03";
ccToken.expYear = "2030";
}
+ this.pushEventToGA('PIA', 'Pay Today', paymentMethodForAnalytics, true, null, 0);
store.savePaymentMethodChoice(paymentMethod);
store.updateCreditCardToken(ccToken);
@@ -95,6 +98,11 @@ export default {
}
);
},
+ computed: {
+ piaType() {
+ return this.mainStore.payment.paymentMethod;
+ }
+ },
methods: {
finalizeAdyenPayment(sessionId, redirectResult) {
return new Promise((resolve, reject) => {
@@ -111,6 +119,10 @@ export default {
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`;
global.$logger.logError(stringToLog);
+
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType);
+ this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0);
+ this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode);
}
reject({
@@ -123,6 +135,10 @@ export default {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
global.$logger.logError(stringToLog);
+
+ const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType);
+ this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0);
+ this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name);
}
reject({
diff --git a/src/layouts/schedule-page/schedule-page.spec.js b/src/layouts/schedule-page/schedule-page.spec.js
index fefde032..411f7e4f 100644
--- a/src/layouts/schedule-page/schedule-page.spec.js
+++ b/src/layouts/schedule-page/schedule-page.spec.js
@@ -38,7 +38,8 @@ const mockMixin = {
days: []
}
};
- })
+ }),
+ pushGenericObjectToGA: jest.fn()
}
};
diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue
index 904329f3..b40ad1d1 100644
--- a/src/layouts/schedule-page/schedule-page.vue
+++ b/src/layouts/schedule-page/schedule-page.vue
@@ -514,6 +514,8 @@ export default {
jobMinMinutes: emittedValue.estimatedServiceMinutesMin.toString(),
};
+ this.selectedAppointmentType = AppointmentTypeStrings.MOBILE;
+ this.pushServiceTypeEvent();
this.mainStore.saveSchedule(timeSlotToUse);
showIssLoadingModal(true);
this.$router.navigate(
@@ -813,6 +815,7 @@ export default {
if (this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
+ this.pushMobileFirstDateEvent(initialData);
this.mobileDatesData = initialData;
if (this.selectedProvider?.providerNumber) {
@@ -833,9 +836,11 @@ export default {
daysFromStart: endDateObject.daysFromStart || null
}));
+ this.pushInshopFirstDateEvent(inShopData);
this.inShopDatesData = inShopData;
}
} else {
+ this.pushInshopFirstDateEvent(initialData);
this.inShopDatesData = initialData;
this.mobileDatesData = [];
}
@@ -940,6 +945,30 @@ export default {
});
}
},
+ pushInshopFirstDateEvent(inshopData) {
+ let dateDiffString = '';
+ if (inshopData?.initialShopTimeSlotsResponse?.days?.length > 0) {
+ const todayDateString = convertDateToDateString(new Date());
+ dateDiffString = calcDaysBetweenDates(todayDateString, inshopData.initialShopTimeSlotsResponse.days[0].date).toString();
+ }
+ this.pushEventToGA('appointment', 'availability_inshop', `days_out_${dateDiffString}`, true, null, null);
+ },
+ pushMobileFirstDateEvent(mobileData) {
+ let dateDiffString = '';
+ if (mobileData?.initialShopTimeSlotsResponse?.days?.length > 0) {
+ const todayDateString = convertDateToDateString(new Date());
+ dateDiffString = calcDaysBetweenDates(todayDateString, mobileData.initialShopTimeSlotsResponse.days[0].date).toString();
+ }
+ this.pushEventToGA('appointment', 'availability_mobile', `days_out_${dateDiffString}`, true, null, null);
+ },
+ pushServiceTypeEvent() {
+ if (this.selectedAppointmentType) {
+ const gaScheduleType = {
+ ['service_type']: this.selectedAppointmentType.toLowerCase()
+ };
+ this.pushGenericObjectToGA(gaScheduleType);
+ }
+ },
async refreshDatePicker() {
this.resetLocalFlags();
this.isDatePickerRefreshing = true;
@@ -977,6 +1006,7 @@ export default {
}
await this.$refs.serviceLocation.forwardButtonAction(appointmentTypeToUse);
+ this.pushServiceTypeEvent();
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
diff --git a/src/layouts/schedule-page/service-location/service-location.vue b/src/layouts/schedule-page/service-location/service-location.vue
index f9787d16..9b23059e 100644
--- a/src/layouts/schedule-page/service-location/service-location.vue
+++ b/src/layouts/schedule-page/service-location/service-location.vue
@@ -571,7 +571,6 @@ export default {
await mobileFeePromise.then((result) => {
this.mobileFeePart = result ?? null;
});
-
}
}
}
diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue
index 9833cdcd..42ae3603 100644
--- a/src/layouts/vehicle-damage/vehicle-damage.vue
+++ b/src/layouts/vehicle-damage/vehicle-damage.vue
@@ -487,6 +487,7 @@ export default {
logEvents() {
const vehicleString = this.mainStore.order.vehicle.make + '_' + this.mainStore.order.vehicle.model + '_' + this.mainStore.order.vehicle.style;
this.pushEventToGA("CAR SUBMISSION", this.mainStore.order.vehicle.year?.toString(), vehicleString, true, null, 0);
+ this.pushEventToGA('forward_progress', 'continue_clicked', 'vehicle_damage_2', true);
if (this.isWindshieldRepair) {
this.pushEventToGA("damage", "selected", "repair", true, null, null);
diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue
index af598359..9057e864 100644
--- a/src/layouts/vin-lookup/vin-lookup.vue
+++ b/src/layouts/vin-lookup/vin-lookup.vue
@@ -179,7 +179,7 @@ export default {
if (this.needToLookupVehicle) {
try {
const vehicleLookupResponse = await this.lookupVehicleByVin(this.vin);
- this.pushEventToGA("VIN", "TEXT", "SUCCESS", true, null, '1');
+ this.pushEventToGA("policy_vehicle", "VIN_lookup", "SUCCESS", true, null, '1');
if (!vehicleLookupResponse.data.canSafeliteService) {
this.mainStore.setBailout(bailoutMessage.HeavyTruckVehicle(vehicleLookupResponse.data.carId));
@@ -196,6 +196,7 @@ export default {
}
);
} catch (e) {
+ this.pushEventToGA("policy_vehicle", "VIN_lookup", "FAIL", true, null, '0');
if (e.status === 404) {
this.activeVehicleLookupAlertType = vehicleLookupAlertTypes.NOT_FOUND;
this.resetVehicleFromLookup();
@@ -203,7 +204,6 @@ export default {
// because the form itself actually passes its client-side validation.
// SSR-189 Scenario #4.
this.$refs.siteFooter.enableForwardAction();
- this.pushEventToGA("VIN", "TEXT", "NO VEHICLE FOUND", true, null, '0');
showIssLoadingModal(false);
return;
}
diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js
index cfe48753..81d63e41 100644
--- a/src/mixins/analytics-mixin.js
+++ b/src/mixins/analytics-mixin.js
@@ -21,9 +21,10 @@ import {
ValueToLogTypes
} from '@/constants/analytics';
import { getCartTotal, getSubtotal } from '@/helpers/cart-helper';
-import { getRecalPartNumbers } from "@/helpers/recal-helper";
+import { getRecalPartNumbers, isRecalOrder } from "@/helpers/recal-helper";
import coverageStatuses from '@/constants/coverage-statuses';
import coverageType from '@/constants/coverage-type';
+import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import issPageValues from '@/router/router-constants/issPage-values';
import { useMainStore } from '@/store';
@@ -91,7 +92,6 @@ export default {
store.logCustomEvent(payload);
},
-
pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null, value = undefined) {
const currentPageName = this.getPageNameByQueryString();
const labelToLog = getValueToLog(label, valueToLogType);
@@ -111,6 +111,9 @@ export default {
this.logCustomEvent(category, action, labelToLog, value);
}
},
+ pushGenericObjectToGA(object) {
+ pushToDataLayerIfDefined(object);
+ },
pushPageViewToGA() {
const currentPageName = this.getPageNameByQueryString();
@@ -126,9 +129,201 @@ export default {
},
pushValueToGA() {
- pushToDataLayerIfDefined({
- siteType: useMainStore().issConfig.siteType
- });
+ const gaServiceType = {
+ ['service_type']: useMainStore().order?.serviceLocation?.appointmentType?.toLowerCase()
+ };
+ if (gaServiceType && gaServiceType['service_type']) {
+ this.pushGenericObjectToGA(gaServiceType);
+ }
+ },
+
+ pushOrderToDataLayer() {
+ // helper check for if an object is defined (but maybe falsey)
+ const isDefined = (x) => x !== null && x !== undefined;
+ const store = useMainStore();
+
+ // Get correct order object
+ const hasSubmittedOrder = store.hasSubmittedOrder();
+ const submittedOrder = store.getSubmittedOrder();
+ const order = hasSubmittedOrder ? submittedOrder : store.order;
+ const deviceId = getDeviceIdValue();
+ const sid = getSessionIdValue();
+
+ // Begin assembling payload for data layer
+ const payload = {};
+
+ payload.appName = "ISS";
+ payload.siteType = store.issConfig.siteType;
+ payload.pageName = this.getPageNameByQueryString();
+ payload.deviceId = deviceId;
+ payload.sessionId = sid;
+ payload.clientName = store.issConfig.clientName;
+ payload.lossCause = order.policy?.damageCause ?? "";
+
+ // Service Zip
+ if (
+ order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE &&
+ isDefined(order.serviceLocation.zipCode)
+ ) {
+ payload.serviceZipCode = order.serviceLocation.zipCode;
+ } else if (
+ isDefined(order.serviceLocation.appointmentType) &&
+ order.serviceLocation.appointmentType !== AppointmentTypeStrings.MOBILE &&
+ isDefined(order.serviceLocation.provider.address.zipCode)
+ ) {
+ payload.serviceZipCode = order.serviceLocation.provider.address.zipCode;
+ } else {
+ payload.serviceZipCode = "";
+ }
+
+ // Damage Type
+ if (isDefined(order.damage.isRepair)) {
+ payload.damageType = order.damage.isRepair ? "repair" : "replace";
+ } else {
+ payload.damageType = "";
+ }
+
+ // Account Type - always insurance for ISS
+ payload.accountType = "insurance";
+
+ // Promo Codes
+ const promos = order.lineItems.promos ?? [];
+ if (promos.length === 0) {
+ payload.promoCodes = "";
+ } else {
+ const promoCodes = promos.map((promo) => promo.promoCode);
+ const promoString = promoCodes.reduce((prev, next) => `${prev},${next}`);
+ payload.promoCodes = promoString;
+ }
+
+ // Vehicle info
+ if (isDefined(order.vehicle.year)) {
+ // Ensure cast to string.
+ payload.vehicleYear = `${order.vehicle.year}`;
+ } else {
+ payload.vehicleYear = "";
+ }
+
+ if (isDefined(order.vehicle.make)) {
+ payload.vehicleMake = order.vehicle.make;
+ } else {
+ payload.vehicleMake = "";
+ }
+
+ if (isDefined(order.vehicle.model)) {
+ payload.vehicleModel = order.vehicle.model;
+ } else {
+ payload.vehicleModel = "";
+ }
+
+ if (isDefined(order.vehicle.style)) {
+ payload.vehicleStyle = order.vehicle.style;
+ } else {
+ payload.vehicleStyle = "";
+ }
+
+ // Glass pieces
+ const glass = order.damage.glassToReplace ?? [];
+ if (glass.length === 0) {
+ payload.glassToReplace = "";
+ } else {
+ const glassNames = glass.map((g) => `${g.glassLocation}/${g.glassName}`);
+ const glassString = glassNames.reduce((prev, next) => `${prev},${next}`);
+
+ payload.glassToReplace = glassString;
+ }
+
+ //EON
+ if (order.eon) {
+ payload.eon = order.eon;
+ } else {
+ payload.eon = "";
+ }
+
+ // Work Order Id
+ if (order.workOrderId) {
+ const parsedId = parseInt(order.workOrderId);
+ if (!isNaN(parsedId)) {
+ payload.workOrderId = parsedId;
+ } else {
+ payload.workOrderId = "";
+ }
+ } else {
+ payload.workOrderId = "";
+ }
+
+ // Provider Ctu
+ if (isDefined(order.serviceLocation.zipCodeCtu)) {
+ payload.providerCtu = order.serviceLocation.zipCodeCtu;
+ } else {
+ payload.providerCtu = "";
+ }
+
+ // Work Order Number
+ if (order.workOrderNumber) {
+ payload.orderNumber = order.workOrderNumber;
+ } else {
+ payload.orderNumber = "";
+ }
+
+ // Unverified (no price or deductible displayed)
+ if (!store.isVerified) {
+ payload.priceSubTotal = "";
+ payload.priceTotal = "";
+ }
+ // Deductible case (no price is displayed, only deductible)
+ else if (
+ store.isVerified &&
+ (typeof store.currentDeductible === 'number' && store.currentDeductible >= 0) &&
+ !store.isITAC &&
+ !store.isNoComp
+ ) {
+ payload.priceSubTotal = "";
+ payload.priceTotal = "";
+ // ITAC or NoComp (where cash price is shown)
+ } else if (store.isVerified && (store.isITAC || store.isNoComp)) {
+ const subtotal = getSubtotal(order).toFixed(2);
+ payload.priceSubTotal = parseFloat(subtotal);
+ const total = getCartTotal(order).toFixed(2);
+ payload.priceTotal = parseFloat(total);
+ } else {
+ payload.priceSubTotal = "";
+ payload.priceTotal = "";
+ }
+
+ // Cash Quote or Cash Price Sub Total
+ payload.cashPriceSubTotal = getSubtotal(order).toString();
+
+ // Recalibration
+ payload.isRecalibrationOnOrder = isRecalOrder(order.lineItems);
+
+ // Appointment Type
+ if (isDefined(order.serviceLocation.appointmentType)) {
+ payload.appointmentType = order.serviceLocation.appointmentType;
+ } else {
+ payload.appointmentType = "";
+ }
+
+ payload.isInsuranceVerified = store.isVerified;
+ payload.insuranceCompanyName = store.issConfig.clientName ?? "";
+ if (!store.isVerified) {
+ payload.isInsuranceItac = "";
+ payload.isInsuranceNoComp = "";
+ } else {
+ payload.isInsuranceItac = store.isITAC ?? "";
+ payload.isInsuranceNoComp = store.isNoComp ?? "";
+ }
+ if (
+ store.isITAC ||
+ store.isNoComp ||
+ !store.isVerified
+ ) {
+ payload.insuranceDeductible = "";
+ } else {
+ payload.insuranceDeductible = store.currentDeductible ?? "";
+ }
+
+ pushToDataLayerIfDefined(payload);
},
pushExperimentsToDataLayer() {
@@ -229,13 +424,13 @@ export default {
const applicationUser = store.applicationUser;
// NOTE: ISS does not support early bird times.
- const isEarlyBird = false; /*order?.lineItems?.supportingItems?.find(
+ const isEarlyBird = false; /* order?.lineItems?.supportingItems?.find(
(lineItem) => lineItem.partType == partTypeStrings.EARLY_BIRD
);
*/
// NOTE: No promo codes on ISS at the moment.
- const promoCodes = ""; /*Array.isArray(order?.lineItems?.promos)
+ const promoCodes = ""; /* Array.isArray(order?.lineItems?.promos)
? order?.lineItems?.promos
.map((item) => item.promoCode)
.filter((code) => code)
@@ -280,7 +475,7 @@ export default {
}
}
*/
- var parentAccountNumber = (order?.parentAccountNumber ?? ( issConfig.parentAccountNumber ?? 0)).toString();
+ var parentAccountNumber = (order?.parentAccountNumber ?? (issConfig.parentAccountNumber ?? 0)).toString();
var sessionData = {};
sessionData.currentPage = currentPageName;
@@ -335,7 +530,7 @@ export default {
sessionData.coverageSubStatus = coverageType.mapToApi(order?.insuranceCoverage.coverageType);
sessionData.isNoComp = store.isNoComp;
- sessionData.isItac = store.isITAC;
+ sessionData.isItac = store.isITAC;
sessionData.subTotalPrice = getSubtotal(order).toString();
sessionData.totalPrice = getCartTotal(order).toString();
sessionData.cashPriceSubTotal = getSubtotal(order).toString();
diff --git a/src/router/index.js b/src/router/index.js
index eeca615e..4fd5306e 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -192,6 +192,9 @@ router.afterEach(async (to, from) => {
// Push values to GA
analyticsMixin.methods.pushValueToGA();
+
+ // Push current order status to Data Layer
+ analyticsMixin.methods.pushOrderToDataLayer();
}
});
diff --git a/src/store/index.js b/src/store/index.js
index 2aa8248a..680561af 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -22,11 +22,12 @@ import {
repairWaivedForSelectedVehicle
} from '@/helpers/policy-vehicle-helper';
import { buildURLSearchParams, getPartNumbersListForQueryString } from '@/helpers/querystring-helper';
-import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal } from '@/helpers/recal-helper';
+import { getRecalPartNumbers, getTopLevelGlassPartsWithRecal, getHasRecalibrationPart } from '@/helpers/recal-helper';
import { getDateForSavedSessionTimeout } from '@/helpers/session-helper';
import { isMobileDevice } from '@/helpers/useragent-helper';
import issPageValues from '@/router/router-constants/issPage-values';
import CoverageStatuses from '@/constants/coverage-statuses';
+import { getNonFalseValuesOfPropertyInArrayOfObjects, sortArrayOfObjectsByPropertyValue } from '@/helpers/object-helper';
const storeId = 'main';
@@ -276,7 +277,7 @@ export const useMainStore = defineStore({
state: () => state,
getters: {
billToAccountNumber: (storeState) => storeState.issConfig.billToAccountNumber,
- hasRecalibrationPart: (storeState) => getHasRecalibrationPart(storeState),
+ hasRecalibrationPart: (storeState) => getHasRecalibrationPartOnOrder(storeState),
vehicle: (storeState) => storeState.order.vehicle,
damage: (storeState) => storeState.order.damage,
lineItems: (state) => state.order.lineItems,
@@ -1034,7 +1035,7 @@ export const useMainStore = defineStore({
const damageType = this.damage.isRepair ? 'Repair' : 'Install';
const parts = getLineItemsFlattened(this.order.lineItems.glassParts);
- const partNumbers = parts.map((part) => part.partNumber).join(',');
+ const partNumbers = this.damage.isRepair ? partNumberStrings.REPAIR : parts.map((part) => part.partNumber).join(',');
let url = `${endpoints.GetTpaAndSafeliteProviders.url}/${zipCode}?accountNumber=${parentAccountNumber}&carId=${carId}&damageType=${damageType}&partNumbers=${partNumbers}`;
if (provderNameSearchString) {
@@ -1348,45 +1349,6 @@ export const useMainStore = defineStore({
});
},
getServiceabilityDetails(serviceZipCode) {
- console.log('BEEP BOOP');
- console.log('--ENVIRONMENT DETECTOR ROBOT ENGAGED--');
- const currentEnvironment = applicationConfig.CURRENT_ENVIRONMENT;
- console.log('I HAVE DETERMINED THAT THE CURRENT ENVIRONMENT IS:', currentEnvironment);
- console.log('---');
- console.log('---');
- if (currentEnvironment === 'Localhost' || currentEnvironment === 'Dev') {
- console.log('NEW SERVICEABILITY DETAILS METHOD ACTIVATED');
- return this.getServiceabilityDetailsNewMethod(serviceZipCode);
- } else {
- console.log('OLD SERVICEABILITY DETAILS METHOD ACTIVATED');
- return this.getServiceabilityDetailsOldMethod(serviceZipCode);
- }
- },
- getServiceabilityDetailsOldMethod(serviceZipCode) {
- const { vehicle, damage, parentAccountNumber, referralSequenceNumber, lineItems } = this.order;
- const { carId } = vehicle;
- const glassArray = convertGlassPieceNamingForApi(damage.glassToReplace);
- const lineItemParts = [...(lineItems.glassParts || []), ...(lineItems.supportingItems || [])].map((part) => ({
- partNumber: part.partNumber,
- recalibrationType: part.recalibrationType
- }));
-
- const params = buildURLSearchParams({
- applicationName: applicationConfig.APPLICATION_NAME,
- parentAccountNumber,
- referralSequenceNumber,
- zip: serviceZipCode,
- carId,
- glassPieces: glassArray,
- lineItems: lineItemParts
- });
-
- return globalMethods.callHttpClient({
- method: endpoints.GetServiceabilityDetails.method,
- endpoint: `${endpoints.GetServiceabilityDetails.url}?${params.toString()}`
- });
- },
- getServiceabilityDetailsNewMethod(serviceZipCode) {
const { damage, lineItems, parentAccountNumber, vehicle } = this.order;
const { carId } = vehicle;
const flattenedGlassParts = getLineItemsFlattened(lineItems.glassParts);
@@ -1985,8 +1947,13 @@ export const useMainStore = defineStore({
// Process Recycle Fee
this.updateRecycleFee(partsData.find((rf) => rf.partNumber === partNumberStrings.RECYCLE_FEE));
- // Remove RecycleFee from supporting items since it's already been added to feeItems
- this.order.lineItems.supportingItems = partsData.filter((i) => i.partNumber !== partNumberStrings.RECYCLE_FEE);
+ // Remove Recycle Fee from supporting items since it's already been added to feeItems
+ let supportingItems = partsData.filter((i) => i.partNumber !== partNumberStrings.RECYCLE_FEE);
+
+ // Remove Repair Supplies Fee, which is hidden for ISS except in scenarios not yet implemented
+ supportingItems = supportingItems.filter((i) => i.partNumber !== partNumberStrings.SUPPLIES_REPAIR);
+
+ this.order.lineItems.supportingItems = supportingItems;
},
updateVaps(partsData) {
@@ -3174,39 +3141,8 @@ export const useMainStore = defineStore({
// Private Functions
-function getHasRecalibrationPart(state) {
- const hasRequiresRecalibration = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, 'requiresRecalibration')?.length > 0;
- const hasRecalibrationType = getNonFalseValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, 'recalibrationType')?.length > 0;
-
- if (hasRequiresRecalibration) {
- if (hasRecalibrationType) {
- // Has both 'requiresRecalibration' and 'recalibrationType' and 'recalibrationType'
- return (
- getNonFalseValuesOfPropertyInArrayOfObjects(
- state.order.lineItems.glassParts,
- 'recalibrationType'
- )[0].toLowerCase() !== 'unknown'
- );
- }
- // Has 'requiresRecalibration' but no 'recalibrationType' at all
- return true;
- }
- // Does not have 'requiresRecalibration'
- return false;
-}
-
-function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
- return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
-}
-
-function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) {
- if (!arrayOfObjects) return null;
-
- return arrayOfObjects.sort((a, b) => {
- if (a[propertyName] < b[propertyName]) return -1;
- if (a[propertyName] > b[propertyName]) return 1;
- return 0;
- });
+function getHasRecalibrationPartOnOrder(state) {
+ return getHasRecalibrationPart(state.order.lineItems);
}
function convertGlassPieceNamingForApi(glassArray) {
diff --git a/src/store/store.spec.js b/src/store/store.spec.js
index 38a52c16..d6a29e43 100644
--- a/src/store/store.spec.js
+++ b/src/store/store.spec.js
@@ -15,6 +15,7 @@ import { AppointmentTypeStrings } from '@/constants/schedule-constants';
import coverageType from '@/constants/coverage-type';
import { getEnumName } from '@/helpers/unit-test-helper';
import { strictEqual } from 'assert';
+import partNumberStrings from '@/constants/part-number-strings';
describe('Store', () => {
let store;
@@ -1978,7 +1979,7 @@ describe('Store', () => {
const carId = getRandomString(10, 14);
const parentAccountNumber = getRandomString(6, 6);
- const partNumber = getRandomString(6, 6);
+ const partNumber = isRepair ? partNumberStrings.REPAIR : getRandomString(6, 6);
store.order.parentAccountNumber = parentAccountNumber;
store.order.damage.isRepair = isRepair;
store.order.vehicle.carId = carId;
@@ -2512,6 +2513,28 @@ describe('Store', () => {
])
);
});
+ it('updateSupportingItems removes Repair Supplies Fee from supporting items in store', () => {
+ // Arrange
+ const partsData = [
+ {
+ partNumber: partNumberStrings.SUPPLIES_REPAIR,
+ partType: 'REPAIR FEE'
+ }
+ ];
+
+ // Act
+ store.updateSupportingItems(partsData);
+
+ // Assert
+ expect(store.order.lineItems.supportingItems).not.toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ partNumber: partNumberStrings.SUPPLIES_REPAIR,
+ partType: 'REPAIR FEE'
+ })
+ ])
+ );
+ });
});
describe('updateGlassFees method', () => {
it('updateGlassFees does not add Labor2 fee to feeItems', () => {