Merge pull request #3300 from Safelite/nation/CASH-2801

Nation/cash 2801
This commit is contained in:
CarlNation 2026-07-31 14:43:24 -04:00 committed by GitHub
commit 5509fb6604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 372 additions and 7 deletions

View file

@ -2,13 +2,23 @@ import { shallowMount } from "@vue/test-utils";
import scheduling from "./scheduling"; import scheduling from "./scheduling";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import store from "@/store"; import store from "@/store";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
dispatch: jest.fn().mockResolvedValue(null), dispatch: jest.fn().mockResolvedValue(null),
getters: { getters: {
order: { order: {
serviceLocation: { zipCode: "43235", appointmentType: null }, serviceLocation: {
zipCode: "43235",
zipCodeCtu: "1234",
appointmentType: null,
address: null,
address2: null,
city: null,
state: "OH",
isVehicleProtected: null,
},
payment: { payment: {
isInsurance: false, isInsurance: false,
insuranceCoverage: { isVerified: false }, insuranceCoverage: { isVerified: false },
@ -19,6 +29,7 @@ jest.mock("@/store", () => ({
lineItems: { glassParts: [] }, lineItems: { glassParts: [] },
policy: { isItac: false, isNoComp: false }, policy: { isItac: false, isNoComp: false },
}, },
lineItems: { supportingItems: [] },
applicationUser: { applicationUser: {
experiments: [], experiments: [],
}, },
@ -49,12 +60,35 @@ jest.mock("@/helpers/page-prerequisites-helper.js", () => ({
})); }));
function setupMocks({ isAppleBrowser = false } = {}) { function setupMocks({ isAppleBrowser = false } = {}) {
const dispatchStoreAction = jest.fn().mockResolvedValue(null);
const baseMixin = { const baseMixin = {
methods: { methods: {
getCmsContent: jest.fn(() => ""), getCmsContent: jest.fn(() => ""),
setCmsContent: jest.fn(), setCmsContent: jest.fn(),
getTotalLineItemPrice: jest.fn(() => 0), getTotalLineItemPrice: jest.fn(() => 0),
isAppleBrowser: jest.fn(() => isAppleBrowser), isAppleBrowser: jest.fn(() => isAppleBrowser),
dispatchStoreAction,
},
computed: {
storeActions() {
return {
SAVE_SERVICE_LOCATION: "saveServiceLocation",
SAVE_SCHEDULE: "saveSchedule",
SAVE_WAITLIST_REQUESTED: "saveWaitListRequested",
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
"saveSupportingItemsSuppressingStateResetting",
};
},
navigationScenarios() {
return {
CLICKED_FORWARD: "clickedForward",
CLICKED_FORWARD_WITH_MOBILE_SERVICE: "clickedForwardWithMobileService",
CLICKED_BACK: "clickedBack",
};
},
pageName() {
return "scheduling";
},
}, },
}; };
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
@ -67,7 +101,7 @@ function setupMocks({ isAppleBrowser = false } = {}) {
}); });
mountOptions.global.mixins = [baseMixin]; mountOptions.global.mixins = [baseMixin];
const wrapper = shallowMount(scheduling, mountOptions); const wrapper = shallowMount(scheduling, mountOptions);
return { wrapper }; return { wrapper, dispatchStoreAction };
} }
describe("scheduling.vue", () => { describe("scheduling.vue", () => {
@ -227,7 +261,7 @@ describe("scheduling.vue", () => {
expect(store.dispatch).toHaveBeenCalledWith("getProviders", { expect(store.dispatch).toHaveBeenCalledWith("getProviders", {
payload: { serviceZipCode: "44101" }, payload: { serviceZipCode: "44101" },
pageNameToLog: undefined, pageNameToLog: "scheduling",
}); });
expect(wrapper.vm.billToAccountNumber).toBe("87291"); expect(wrapper.vm.billToAccountNumber).toBe("87291");
expect(settleAllPromises).toHaveBeenCalled(); expect(settleAllPromises).toHaveBeenCalled();
@ -251,6 +285,134 @@ describe("scheduling.vue", () => {
}); });
}); });
describe("forwardButtonAction", () => {
test("saves service location, schedule, waitlist and navigates for inshop selection", async () => {
const { wrapper, dispatchStoreAction } = setupMocks();
wrapper.vm.selectedDate = "2026-07-22";
wrapper.vm.isWaitlistRequested = true;
wrapper.vm.selectedScheduling = {
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "05018",
routeCodeId: "route-inshop",
};
wrapper.vm.inshopProvidersAndTimeSlots = [
{
provider: {
providerNumber: "05018",
address: {
streetAddress: "123 Main",
city: "Columbus",
state: "OH",
zipCode: "43235",
zipCodeCtu: "9999",
},
},
timeSlots: {
estimatedServiceMinutesMinimum: 60,
estimatedServiceMinutesMaximum: 120,
days: [
{
date: "2026-07-22",
timeSlots: [
{
id: "route-inshop",
startTime: "09:00",
endTime: "10:00",
},
],
},
],
},
},
];
await wrapper.vm.forwardButtonAction();
expect(dispatchStoreAction).toHaveBeenCalledWith(
"saveServiceLocation",
expect.objectContaining({
appointmentType: AppointmentTypeStrings.IN_SHOP,
zipCodeCtu: "9999",
provider: expect.objectContaining({ providerNumber: "05018" }),
}),
false
);
expect(dispatchStoreAction).toHaveBeenCalledWith(
"saveSchedule",
expect.objectContaining({
date: "2026-07-22",
routeCode: "route-inshop",
startTime: "09:00",
endTime: "10:00",
jobMinMinutes: "60",
jobMaxMinutes: "120",
}),
false
);
expect(dispatchStoreAction).toHaveBeenCalledWith("saveWaitListRequested", true, false);
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
"clickedForward",
"scheduling"
);
wrapper.unmount();
});
test("navigates with mobile scenario and clears provider address for mobile selection", async () => {
const { wrapper, dispatchStoreAction } = setupMocks();
wrapper.vm.selectedDate = "2026-07-22";
wrapper.vm.selectedScheduling = {
appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "MOBILE1",
routeCodeId: "route-mobile",
isPremiumAppointment: false,
};
wrapper.vm.mobileProviderAndTimeSlot = {
providerNumber: "MOBILE1",
timeSlots: {
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 150,
days: [
{
date: "2026-07-22",
timeSlots: [
{
id: "route-mobile",
startTime: "08:00",
endTime: "12:00",
},
],
},
],
},
};
await wrapper.vm.forwardButtonAction();
expect(dispatchStoreAction).toHaveBeenCalledWith(
"saveServiceLocation",
expect.objectContaining({
appointmentType: AppointmentTypeStrings.MOBILE,
provider: {
providerNumber: "MOBILE1",
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
},
},
}),
false
);
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
"clickedForwardWithMobileService",
"scheduling"
);
wrapper.unmount();
});
});
describe("in-shop Maps link", () => { describe("in-shop Maps link", () => {
const address = { const address = {
city: "Lewis Center", city: "Lewis Center",

View file

@ -102,6 +102,7 @@ import textLink from "@/ux-components/text-link/text-link";
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants"; import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { isDropOffRouteCode } from "@/layouts/schedule/helpers/schedule-helper";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
@ -149,14 +150,23 @@ function appendDays(entry, newSlots) {
/** /**
* Assigns time slots from a v2 multi-provider response onto in-shop provider entries. * Assigns time slots from a v2 multi-provider response onto in-shop provider entries.
* @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries * @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries
* @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[], provisionalTriggers?: string[] }> } | null | undefined} multiProviderResponse * @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[], provisionalTriggers?: string[] }>, estimatedServiceMinutesMinimum?: number, estimatedServiceMinutesMaximum?: number } | null | undefined} multiProviderResponse
*/ */
function assignInshopTimeSlotsFromV2Response(entries, multiProviderResponse) { function assignInshopTimeSlotsFromV2Response(entries, multiProviderResponse) {
const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? []; const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? [];
entries.forEach((entry) => { entries.forEach((entry) => {
entry.timeSlots = const matchedSlots =
providerTimeSlots.find((pts) => pts.providerNumber === entry.provider.providerNumber) ?? providerTimeSlots.find((pts) => pts.providerNumber === entry.provider.providerNumber) ??
null; null;
if (matchedSlots) {
matchedSlots.estimatedServiceMinutesMinimum =
matchedSlots.estimatedServiceMinutesMinimum ??
multiProviderResponse?.estimatedServiceMinutesMinimum;
matchedSlots.estimatedServiceMinutesMaximum =
matchedSlots.estimatedServiceMinutesMaximum ??
multiProviderResponse?.estimatedServiceMinutesMaximum;
}
entry.timeSlots = matchedSlots;
}); });
} }
@ -609,9 +619,202 @@ export default {
this.datePickerEndDate = newEndDate; this.datePickerEndDate = newEndDate;
this.isLoadingDates = false; this.isLoadingDates = false;
}, },
forwardButtonAction() { getInShopOrDropOffApptType(selectedScheduling = this.selectedScheduling) {
const appointmentType = store.getters.order.serviceLocation.appointmentType; // TODO: remove this once we have a proper appointment type if (selectedScheduling?.appointmentType === AppointmentTypeStrings.MOBILE) {
return AppointmentTypeStrings.MOBILE;
}
return isDropOffRouteCode(selectedScheduling?.routeCodeId)
? AppointmentTypeStrings.DROP_OFF
: AppointmentTypeStrings.IN_SHOP;
},
getSelectedProvider(appointmentType, selectedScheduling = this.selectedScheduling) {
if (appointmentType === AppointmentTypeStrings.MOBILE) { if (appointmentType === AppointmentTypeStrings.MOBILE) {
return {
providerNumber: selectedScheduling?.providerNumber,
address: {
streetAddress: null,
city: null,
state: null,
zipCode: null,
zipCodeCtu: null,
},
};
}
return (
this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === selectedScheduling?.providerNumber
)?.provider ?? null
);
},
getSelectedTimeSlot(
appointmentType,
selectedScheduling = this.selectedScheduling,
selectedDate = this.selectedDate
) {
const routeCodeId = selectedScheduling?.routeCodeId;
if (!routeCodeId || !selectedDate) {
return null;
}
let timeSlots;
if (appointmentType === AppointmentTypeStrings.MOBILE) {
const day = this.mobileProviderAndTimeSlot?.timeSlots?.days?.find(
(d) => d.date === selectedDate
);
timeSlots = day?.timeSlots ?? [];
} else {
const providerEntry = this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === selectedScheduling.providerNumber
);
const day = providerEntry?.timeSlots?.days?.find((d) => d.date === selectedDate);
timeSlots = day?.timeSlots ?? [];
}
return timeSlots.find((timeSlot) => timeSlot.id === routeCodeId) ?? null;
},
getEstimatedServiceMinutes(appointmentType, selectedScheduling = this.selectedScheduling) {
if (appointmentType === AppointmentTypeStrings.MOBILE) {
return {
minimum:
this.mobileProviderAndTimeSlot?.timeSlots?.estimatedServiceMinutesMinimum,
maximum:
this.mobileProviderAndTimeSlot?.timeSlots?.estimatedServiceMinutesMaximum,
};
}
const providerEntry = this.inshopProvidersAndTimeSlots.find(
({ provider }) => provider.providerNumber === selectedScheduling?.providerNumber
);
return {
minimum: providerEntry?.timeSlots?.estimatedServiceMinutesMinimum,
maximum: providerEntry?.timeSlots?.estimatedServiceMinutesMaximum,
};
},
updateSupportingItems(selectedScheduling = this.selectedScheduling) {
const supportingItems = store.getters.lineItems?.supportingItems;
if (!supportingItems) {
return;
}
const isPremiumMobile =
selectedScheduling?.appointmentType === AppointmentTypeStrings.MOBILE &&
selectedScheduling?.isPremiumAppointment;
if (isPremiumMobile && this.mobilePremiumAppointmentFee) {
const premiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (premiumFeeIndex > -1) {
supportingItems[premiumFeeIndex].laborAmount =
this.mobilePremiumAppointmentFee.laborAmount;
supportingItems[premiumFeeIndex].sellingPrice =
this.mobilePremiumAppointmentFee.sellingPrice;
supportingItems[premiumFeeIndex].kitPrice =
this.mobilePremiumAppointmentFee.kitPrice;
} else {
supportingItems.push(this.mobilePremiumAppointmentFee);
}
} else {
const removePremiumFeeIndex = supportingItems.findIndex(
(item) => item.partType == PREMIUM_FEE_PART_TYPE
);
if (removePremiumFeeIndex >= 0) {
supportingItems.splice(removePremiumFeeIndex, 1);
}
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,
false
);
},
async forwardButtonAction() {
const selectedScheduling = this.selectedScheduling;
const selectedDate = this.selectedDate;
const appointmentType = this.getInShopOrDropOffApptType(selectedScheduling);
const selectedProvider = this.getSelectedProvider(appointmentType, selectedScheduling);
const serviceLocation = store.getters.order.serviceLocation;
const isMobile = appointmentType === AppointmentTypeStrings.MOBILE;
let zipCodeCtu = serviceLocation.zipCodeCtu;
if (
!isMobile &&
selectedProvider?.address?.zipCodeCtu &&
zipCodeCtu !== selectedProvider.address.zipCodeCtu
) {
zipCodeCtu = selectedProvider.address.zipCodeCtu;
}
await this.dispatchStoreAction(
this.storeActions.SAVE_SERVICE_LOCATION,
{
address: isMobile ? serviceLocation.address : "",
address2: isMobile ? serviceLocation.address2 : "",
city: isMobile ? serviceLocation.city : "",
state: serviceLocation.state,
zipCode: serviceLocation.zipCode,
zipCodeCtu: zipCodeCtu,
appointmentType,
isVehicleProtected: isMobile ? serviceLocation.isVehicleProtected : null,
provider: {
providerNumber: selectedProvider?.providerNumber,
address: {
streetAddress: selectedProvider?.address?.streetAddress ?? null,
city: selectedProvider?.address?.city ?? null,
state: selectedProvider?.address?.state ?? null,
zipCode: selectedProvider?.address?.zipCode ?? null,
zipCodeCtu: selectedProvider?.address?.zipCodeCtu ?? null,
},
},
},
false
);
if (this.billToAccountNumber) {
this.dispatchStoreAction(
this.storeActions.SAVE_BILL_TO_ACCOUNT_NUMBER,
this.billToAccountNumber,
false
);
}
this.updateSupportingItems(selectedScheduling);
const selectedTimeSlot = this.getSelectedTimeSlot(
appointmentType,
selectedScheduling,
selectedDate
);
const estimatedServiceMinutes = this.getEstimatedServiceMinutes(
appointmentType,
selectedScheduling
);
this.dispatchStoreAction(
this.storeActions.SAVE_SCHEDULE,
{
date: selectedDate,
routeCode: selectedScheduling?.routeCodeId ?? null,
startTime: selectedTimeSlot?.startTime ?? null,
endTime: selectedTimeSlot?.endTime ?? null,
jobMinMinutes: estimatedServiceMinutes.minimum?.toString() ?? null,
jobMaxMinutes: estimatedServiceMinutes.maximum?.toString() ?? null,
},
false
);
if (this.isWaitlistRequested !== null && this.isWaitlistRequested !== undefined) {
this.dispatchStoreAction(
this.storeActions.SAVE_WAITLIST_REQUESTED,
this.isWaitlistRequested,
false
);
}
if (isMobile) {
this.$router.navigateWithSaving( this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE, this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
this.pageName this.pageName