diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index 92a86e609..54e40214b 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -135,7 +135,6 @@ export default { vm.setCmsContent(resultMap.cmsContent); vm.lineItems = lineItemsFromSubmittedOrder; vm.vaps = availableVaps; - analyticsMixin.methods.pushSubmittedOrderToDataLayer(); }); }, data() { diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index d394190ff..cfa68c82c 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -120,77 +120,182 @@ export default { await this.logPageView(analyticsPageEvents.ENTRY); }, - pushSubmittedOrderToDataLayer() { - // check if submitted order exists; exit if not. - const hasSubmittedOrder = store.getters.hasSubmittedOrder; + pushOrderToDataLayer() { + // helper check for if an object is defined (but maybe falsey) + const isDefined = (x) => x !== null && x !== undefined; - if (!hasSubmittedOrder) { - return; + // Get correct order object + const hasSubmittedOrder = store.getters.hasSubmittedOrder; + const order = hasSubmittedOrder ? store.getters.submittedOrder : store.getters.order; + + // Begin assembling payload for data layer + const payload = {}; + + // 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 = ""; } - const order = store.getters.submittedOrder; + // Damage Type + if (isDefined(order.damage.isRepair)) { + payload.damageType = order.damage.isRepair ? "repair" : "replace"; + } else { + payload.damageType = ""; + } - // assemble data for payload - // // reduce promocode array + // Account Type + if (isDefined(order.payment.isInsurance)) { + payload.accountType = order.payment.isInsurance ? "insurance" : "cash"; + } else { + payload.accountType = ""; + } + + // Promo Codes const promos = order.lineItems.promos ?? []; - const promoCodes = promos.map((promo) => promo.promoCode); - const promoString = - promoCodes.length === 0 ? "" : promoCodes.reduce((prev, next) => `${prev},${next}`); + 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; + } - // // reduce glass array - const glassToReplace = order.damage.glassToReplace ?? []; - const glassToReplaceNames = glassToReplace.map( - (glassPiece) => `${glassPiece.glassLocation}/${glassPiece.glassName}` - ); - const glassString = - glassToReplaceNames.length === 0 - ? "" - : glassToReplaceNames.reduce((prev, next) => `${prev},${next}`); + // Vehicle info + if (isDefined(order.vehicle.year)) { + // Ensure cast to string. + payload.vehicleYear = `${order.vehicle.year}`; + } else { + payload.vehicleYear = ""; + } - // // calculate subtotal - const lineItems = order.lineItems; + 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; + } + + // 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)) { + const parsedCtu = parseInt(order.serviceLocation.zipCodeCtu); + if (!isNaN(parsedCtu)) { + payload.providerCtu = parseInt(order.serviceLocation.zipCodeCtu); + } else { + payload.providerCtu = ""; + } + } else { + payload.providerCtu = ""; + } + + // Work Order Number + if (order.workOrderNumber) { + payload.orderNumber = order.workOrderNumber; + } else { + payload.orderNumber = ""; + } + + // Pricing + // Only fire for completed orders? + + const lineItems = order.lineItems ?? {}; const combinedLineItems = [ ...(lineItems.glassParts ?? []), ...(lineItems.supportingItems ?? []), ...(lineItems.vaps ?? []), ...(lineItems.promos ?? []), ]; - const subtotal = baseMixin.methods - .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false) - .toFixed(2); - // // get correct zip code - const providerZip = order.serviceLocation.provider.address.zipCode; - const serviceZip = - order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE - ? order.serviceLocation.zipCode - : providerZip; + const isPricingAvailable = + combinedLineItems.length > 0 && + combinedLineItems.every( + (lineItem) => + isDefined(lineItem.kitPrice) && + isDefined(lineItem.laborAmount) && + isDefined(lineItem.sellingPrice) + ); + const isTaxAvailable = + isPricingAvailable && + combinedLineItems.every((lineItem) => isDefined(lineItem.salesTax)); - // // calculate total - const total = baseMixin.methods - .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, true) - .toFixed(2); + if (isPricingAvailable) { + const subtotal = baseMixin.methods + .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false) + .toFixed(2); - const payload = { - serviceZipCode: serviceZip, - damageType: order.damage.isRepair ? "repair" : "replace", - accountType: order.payment.isInsurance ? "insurance" : "cash", - promoCodes: promoString, - vehicleYear: `${order.vehicle.year}`, - vehicleMake: order.vehicle.make, - vehicleModel: order.vehicle.model, - vehicleStyle: order.vehicle.style, - glassToReplace: glassString, - workOrderId: parseInt(order.workOrderId), - providerCtu: parseInt(order.serviceLocation.zipCodeCtu), - orderNumber: order.workOrderNumber, - priceTotal: parseFloat(total), - priceSubTotal: parseFloat(subtotal), - isRecalibrationOnOrder: store.getters.isRecalibrationOnSubmittedOrder, - appointmentType: order.serviceLocation.appointmentType, - }; + payload.priceSubTotal = parseFloat(subtotal); + } else { + payload.priceSubTotal = ""; + } + + if (isTaxAvailable) { + const total = baseMixin.methods + .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, true) + .toFixed(2); + + payload.priceTotal = parseFloat(total); + } else { + payload.priceTotal = ""; + } + + // Recalibration + if (hasSubmittedOrder) { + payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnSubmittedOrder; + } else { + payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder; + } + + // Appointment Type + if (isDefined(order.serviceLocation.appointmentType)) { + payload.appointmentType = order.serviceLocation.appointmentType; + } else { + payload.appointmentType = ""; + } - // push to data layer. pushToDataLayerIfDefined(payload); }, diff --git a/src/mixins/analytics-mixin.spec.js b/src/mixins/analytics-mixin.spec.js index ff6c30e7e..42aba5f31 100644 --- a/src/mixins/analytics-mixin.spec.js +++ b/src/mixins/analytics-mixin.spec.js @@ -38,27 +38,38 @@ const parts = { requiresRecalibration: true, salesTax: 63.86, sellingPrice: 791.46, + kitPrice: 0, + laborAmount: 0, }, frontWipers: { name: "front wipers", partNumber: "SBB16", description: "SAFELITE BEAM BLADE 16", partType: "FRONT WIPER", - price: 32.64, + sellingPrice: 32.64, + kitPrice: 0, + laborAmount: 0, + salesTax: 1, }, rearWipers: { name: "rear wipers", partNumber: "SBBR12A", description: "SAFELITE REAR BLADE 12A", partType: "REAR WIPER", - price: 24.48, + sellingPrice: 24.48, + kitPrice: 0, + laborAmount: 0, + salesTax: 2, }, rainDefense: { name: "rain defense", partNumber: "RAIN DEFENSE", description: null, partType: "RAIN DEFENSE", - price: 35.5, + sellingPrice: 35.5, + kitPrice: 0, + laborAmount: 0, + salesTax: 0, }, }; @@ -324,10 +335,10 @@ describe("analyticsMixin.js", () => { ]); }); - describe("pushSubmittedOrderToDataLayer", () => { + describe("pushOrderToDataLayer", () => { beforeEach(() => { - store.getters.hasSubmittedOrder = true; - store.getters.submittedOrder = { + store.getters.hasSubmittedOrder = false; + store.getters.order = { vehicle: { year: "2020", make: "acura", @@ -342,8 +353,8 @@ describe("analyticsMixin.js", () => { address2: "add2", city: "city", state: "state", - zipCode: "zip", - zipCodeCtu: "zipCtu", + zipCode: "11111", + zipCodeCtu: "11110", appointmentType: "IN_SHOP", isVehicleProtected: true, provider: { @@ -352,8 +363,8 @@ describe("analyticsMixin.js", () => { streetAddress: "add3", city: "city2", state: "state2", - zipCode: "zip2", - zipCodeCtu: "zipCtu2", + zipCode: "22222", + zipCodeCtu: "22220", }, }, techNotes: "", @@ -368,13 +379,28 @@ describe("analyticsMixin.js", () => { damage: { isRepair: false, numberOfChips: null, - glassToReplace: [{ glassName: "single", location: "windshield" }], + glassToReplace: [{ glassName: "single", glassLocation: "windshield" }], }, lineItems: { glassParts: [parts.windshield], supportingItems: [], vaps: [parts.frontWipers], - promos: [{ promoCode: "promoTEST" }, { promoCode: "promoTEST2" }], + promos: [ + { + promoCode: "promoTEST", + kitPrice: 0, + sellingPrice: 0, + laborAmount: 0, + salesTax: 0, + }, + { + promoCode: "promoTEST2", + kitPrice: 0, + sellingPrice: 0, + laborAmount: 0, + salesTax: 0, + }, + ], }, payment: { isInsurance: false, @@ -397,6 +423,8 @@ describe("analyticsMixin.js", () => { workOrderNumber: "01820-111111", workOrderId: "222222222222", }; + store.getters.isRecalibrationOnOrder = true; + store.getters.isRecalibrationOnSubmittedOrder = false; }); test("Pushes to data layer if nominal", () => { @@ -408,28 +436,203 @@ describe("analyticsMixin.js", () => { }; // Act - analyticsMixin.methods.pushSubmittedOrderToDataLayer(); + analyticsMixin.methods.pushOrderToDataLayer(); // Assert expect(mockDataLayerFn).toHaveBeenCalled(); }); - test("Does not push to data layer if no submitted order available.", () => { + test("Pushes populated data to data layer", () => { // Arrange - store.getters.hasSubmittedOrder = false; - store.getters.submittedOrder = undefined; + window.dataLayer = []; - const mockDataLayerFn = jest.fn(); + // Act + analyticsMixin.methods.pushOrderToDataLayer(); - window.dataLayer = { - push: mockDataLayerFn, + const result = window.dataLayer[0]; + + // Assert + console.log(result); + const allFieldsPopulated = Object.keys(result).every( + (key) => result[key] === false || !!result[key] + ); + expect(allFieldsPopulated).toBe(true); + }); + + test("Pushes correct data when submitted order is present", () => { + // Arrange + window.dataLayer = []; + store.getters.hasSubmittedOrder = true; + store.getters.submittedOrder = store.getters.order; + store.getters.order = { + vehicle: { + year: null, + make: null, + model: null, + style: null, + carId: null, + category: null, + vin: null, + }, + serviceLocation: { + address: null, + address2: null, + city: null, + state: null, + zipCode: null, + zipCodeCtu: null, + appointmentType: null, + isVehicleProtected: null, + provider: { + providerNumber: null, + address: { + streetAddress: null, + city: null, + state: null, + zipCode: null, + zipCodeCtu: null, + }, + }, + techNotes: null, + }, + customer: { + firstName: null, + lastName: null, + emailAddress: null, + phoneNumber: null, + isSmsOptIn: null, + }, + damage: { + isRepair: null, + numberOfChips: null, + glassToReplace: null, + }, + lineItems: { + glassParts: null, + supportingItems: null, + vaps: null, + promos: null, + }, + payment: { + isInsurance: null, + insuranceCoverage: { + isVerified: null, + coverageStatus: null, + coverageVerificationType: null, + }, + isPia: null, + piaType: null, + inactivePromos: null, + }, + schedule: { + date: null, + startTime: null, + endTime: null, + jobMinMinutes: null, + jobMaxMinutes: null, + }, + workOrderNumber: null, + workOrderId: null, }; // Act - analyticsMixin.methods.pushSubmittedOrderToDataLayer(); + analyticsMixin.methods.pushOrderToDataLayer(); + + const result = window.dataLayer[0]; // Assert - expect(mockDataLayerFn).not.toHaveBeenCalled(); + console.log(result); + const allFieldsPopulated = Object.keys(result).every( + (key) => result[key] === false || !!result[key] + ); + expect(allFieldsPopulated).toBe(true); + }); + + test("Pushes default values when data is missing", () => { + // Arrange + window.dataLayer = []; + store.getters.order = { + vehicle: { + year: null, + make: null, + model: null, + style: null, + carId: null, + category: null, + vin: null, + }, + serviceLocation: { + address: null, + address2: null, + city: null, + state: null, + zipCode: null, + zipCodeCtu: null, + appointmentType: null, + isVehicleProtected: null, + provider: { + providerNumber: null, + address: { + streetAddress: null, + city: null, + state: null, + zipCode: null, + zipCodeCtu: null, + }, + }, + techNotes: null, + }, + customer: { + firstName: null, + lastName: null, + emailAddress: null, + phoneNumber: null, + isSmsOptIn: null, + }, + damage: { + isRepair: null, + numberOfChips: null, + glassToReplace: null, + }, + lineItems: { + glassParts: null, + supportingItems: null, + vaps: null, + promos: null, + }, + payment: { + isInsurance: null, + insuranceCoverage: { + isVerified: null, + coverageStatus: null, + coverageVerificationType: null, + }, + isPia: null, + piaType: null, + inactivePromos: null, + }, + schedule: { + date: null, + startTime: null, + endTime: null, + jobMinMinutes: null, + jobMaxMinutes: null, + }, + workOrderNumber: null, + workOrderId: null, + }; + + // Act + analyticsMixin.methods.pushOrderToDataLayer(); + + const result = window.dataLayer[0]; + + // Assert + console.log(result); + const allFieldsPopulatedOrDefault = Object.keys(result).every( + (key) => result[key] === false || !!result[key] || result[key] === "" + ); + expect(allFieldsPopulatedOrDefault).toBe(true); }); test("Glass and Promo strings correctly formatted", () => { @@ -437,7 +640,7 @@ describe("analyticsMixin.js", () => { window.dataLayer = []; // Act - analyticsMixin.methods.pushSubmittedOrderToDataLayer(); + analyticsMixin.methods.pushOrderToDataLayer(); // Assert const glassString = window.dataLayer[0].glassToReplace; diff --git a/src/router/index.js b/src/router/index.js index 4a13a8fc7..d7eee07ea 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -220,6 +220,9 @@ router.afterEach(async (to, from) => { // Push experiments to Data Layer analyticsMixin.methods.pushExperimentsToDataLayer(); + + // Push current order status to Data Layer + analyticsMixin.methods.pushOrderToDataLayer(); }); router.navigateWithoutSaving = (