CASH-2916 | Consume new v2 endpoints for scheduling

Providers are now grouped for in-shop
They can accept ranges up to 180 but that is not needed yet
This commit is contained in:
scottkiener-at-safelite 2026-07-07 12:08:59 -04:00
parent d5483f0a14
commit 87c1baf381
4 changed files with 234 additions and 32 deletions

View file

@ -112,10 +112,18 @@ const endpoints = {
url: "/schedule/api/v1/schedule/shop-time-slots", url: "/schedule/api/v1/schedule/shop-time-slots",
method: "POST", method: "POST",
}, },
GetShopTimeSlotsV2: {
url: "/schedule/api/v2/schedule/shop-time-slots",
method: "POST",
},
GetMobileTimeSlots: { GetMobileTimeSlots: {
url: "/schedule/api/v1/schedule/mobile-time-slots", url: "/schedule/api/v1/schedule/mobile-time-slots",
method: "POST", method: "POST",
}, },
GetMobileTimeSlotsV2: {
url: "/schedule/api/v2/schedule/mobile-time-slots",
method: "POST",
},
GetMobilePremiumFee: { GetMobilePremiumFee: {
url: "/parts/api/v1/parts/mobile-premium-fee", url: "/parts/api/v1/parts/mobile-premium-fee",
method: "GET", method: "GET",

View file

@ -35,7 +35,9 @@ const storeActions = {
GET_PRICING_BY_DAY_PART: "getPricingByDayPart", GET_PRICING_BY_DAY_PART: "getPricingByDayPart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_SHOP_TIME_SLOTS: "getShopTimeSlots", GET_SHOP_TIME_SLOTS: "getShopTimeSlots",
GET_SHOP_TIME_SLOTS_V2: "getShopTimeSlotsV2",
GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots", GET_MOBILE_TIME_SLOTS: "getMobileTimeSlots",
GET_MOBILE_TIME_SLOTS_V2: "getMobileTimeSlotsV2",
GET_PROVIDERS: "getProviders", GET_PROVIDERS: "getProviders",
GET_MOBILE_PREMIUM_FEE: "getMobilePremiumFee", GET_MOBILE_PREMIUM_FEE: "getMobilePremiumFee",
SAVE_SESSION: "saveSession", SAVE_SESSION: "saveSession",

View file

@ -111,16 +111,46 @@ function appendDays(entry, newSlots) {
} }
/** /**
* Returns a promise for inshop time slots for a single provider. * Assigns time slots from a v2 multi-provider response onto in-shop provider entries.
* @param {{ startDate: string, endDate: string, providerNumber: string, pageNameToLog: string }} params * @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries
* @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[], provisionalTriggers?: string[] }> } | null | undefined} multiProviderResponse
*/ */
function fetchInshopTimeSlots({ startDate, endDate, providerNumber, pageNameToLog }) { function assignInshopTimeSlotsFromV2Response(entries, multiProviderResponse) {
return store.dispatch("getShopTimeSlots", { const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? [];
entries.forEach((entry) => {
entry.timeSlots =
providerTimeSlots.find(
(pts) => pts.providerNumber === entry.provider.providerNumber
) ?? null;
});
}
/**
* Appends days from a v2 multi-provider response onto existing in-shop provider entries.
* @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries
* @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[] }> } | null | undefined} multiProviderResponse
*/
function appendInshopTimeSlotsFromV2Response(entries, multiProviderResponse) {
const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? [];
entries.forEach((entry) => {
const newSlots = providerTimeSlots.find(
(pts) => pts.providerNumber === entry.provider.providerNumber
);
appendDays(entry, newSlots);
});
}
/**
* Returns a promise for inshop time slots for multiple providers.
* @param {{ startDate: string, endDate: string, providerNumbers: string[], pageNameToLog: string }} params
*/
function fetchInshopTimeSlots({ startDate, endDate, providerNumbers, pageNameToLog }) {
return store.dispatch("getShopTimeSlotsV2", {
payload: { payload: {
startDate, startDate,
endDate, endDate,
shopAppointmentType: "InshopOrDropoff", shopAppointmentType: "InshopOrDropoff",
providerNumber, providerNumbers,
}, },
pageNameToLog, pageNameToLog,
}); });
@ -131,8 +161,12 @@ function fetchInshopTimeSlots({ startDate, endDate, providerNumber, pageNameToLo
* @param {{ startDate: string, endDate: string, zipCode: string, pageNameToLog: string }} params * @param {{ startDate: string, endDate: string, zipCode: string, pageNameToLog: string }} params
*/ */
function fetchMobileTimeSlots({ startDate, endDate, zipCode, pageNameToLog }) { function fetchMobileTimeSlots({ startDate, endDate, zipCode, pageNameToLog }) {
return store.dispatch("getMobileTimeSlots", { return store.dispatch("getMobileTimeSlotsV2", {
payload: { startDate, endDate, zipCode }, payload: {
startDate,
endDate,
zipCode,
},
pageNameToLog, pageNameToLog,
}); });
} }
@ -176,16 +210,21 @@ export default {
: null; : null;
const startDate = toDateString(0); const startDate = toDateString(0);
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1); const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
const providerNumbers = providers.map((provider) => provider.providerNumber);
const timeSlotsPromiseResultMap = [ const timeSlotsPromiseResultMap = [
...providers.map((provider, i) => ({ ...(providerNumbers.length
resultKey: `inshopTimeSlots_${i}`, ? [
promise: fetchInshopTimeSlots({ {
startDate, resultKey: "inshopTimeSlots",
endDate, promise: fetchInshopTimeSlots({
providerNumber: provider.providerNumber, startDate,
pageNameToLog: to.name, endDate,
}), providerNumbers,
})), pageNameToLog: to.name,
}),
},
]
: []),
// Get the mobile time slots if a mobile provider number is available // Get the mobile time slots if a mobile provider number is available
...(mobileProviderNumber ...(mobileProviderNumber
? [ ? [
@ -202,9 +241,10 @@ export default {
: []), : []),
]; ];
const timeSlotsResultMap = await settleAllPromises(timeSlotsPromiseResultMap); const timeSlotsResultMap = await settleAllPromises(timeSlotsPromiseResultMap);
vm.inShopProvidersAndTimeslots.forEach((entry, i) => { assignInshopTimeSlotsFromV2Response(
entry.timeSlots = timeSlotsResultMap[`inshopTimeSlots_${i}`] ?? null; vm.inShopProvidersAndTimeslots,
}); timeSlotsResultMap.inshopTimeSlots
);
if (vm.mobileProviderAndTimeSlot) { if (vm.mobileProviderAndTimeSlot) {
vm.mobileProviderAndTimeSlot.timeSlots = timeSlotsResultMap.mobileTimeSlots ?? null; vm.mobileProviderAndTimeSlot.timeSlots = timeSlotsResultMap.mobileTimeSlots ?? null;
} }
@ -332,17 +372,24 @@ export default {
const newStartDate = toDateString(1, this.datePickerEndDate); const newStartDate = toDateString(1, this.datePickerEndDate);
const newEndDate = toDateString(SCHEDULE_FETCH_DAYS, this.datePickerEndDate); const newEndDate = toDateString(SCHEDULE_FETCH_DAYS, this.datePickerEndDate);
const providerNumbers = this.inShopProvidersAndTimeslots.map(
({ provider }) => provider.providerNumber
);
const promiseResultMap = [ const promiseResultMap = [
...this.inShopProvidersAndTimeslots.map(({ provider }, i) => ({ ...(providerNumbers.length
resultKey: `inshopTimeSlots_${i}`, ? [
promise: fetchInshopTimeSlots({ {
startDate: newStartDate, resultKey: "inshopTimeSlots",
endDate: newEndDate, promise: fetchInshopTimeSlots({
providerNumber: provider.providerNumber, startDate: newStartDate,
pageNameToLog: this.pageName, endDate: newEndDate,
}), providerNumbers,
})), pageNameToLog: this.pageName,
}),
},
]
: []),
...(this.mobileProviderAndTimeSlot ...(this.mobileProviderAndTimeSlot
? [ ? [
{ {
@ -360,10 +407,10 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
// Provider order is fixed after beforeRouteEnter, so index keys are stable. appendInshopTimeSlotsFromV2Response(
this.inShopProvidersAndTimeslots.forEach((entry, index) => { this.inShopProvidersAndTimeslots,
appendDays(entry, resultMap[`inshopTimeSlots_${index}`]); resultMap.inshopTimeSlots
}); );
if (this.mobileProviderAndTimeSlot) { if (this.mobileProviderAndTimeSlot) {
appendDays(this.mobileProviderAndTimeSlot, resultMap.mobileTimeSlots); appendDays(this.mobileProviderAndTimeSlot, resultMap.mobileTimeSlots);

View file

@ -2543,6 +2543,149 @@ export const actions = {
return globalMethods.callHttpClient(options); return globalMethods.callHttpClient(options);
}, },
getShopTimeSlotsV2(
context,
{ payload: { startDate, endDate, shopAppointmentType, providerNumbers }, pageNameToLog }
) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
const payment = context.state.order.payment;
const lineItems = getFlattenedLineItemsWithGlassPartTag(order.lineItems);
const glassPieces = order.damage.glassToReplace
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
: [];
var payload = {
providerNumbers: providerNumbers,
startDate: startDate,
endDate: endDate,
shopAppointmentType: shopAppointmentType,
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: payment.parentAccountNumber,
carId: vehicle.carId,
lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
billToAccountNumber: context.getters.payment.billToAccountNumber,
coverage: {
status: payment.insuranceCoverage.coverageStatus,
deductible: order.policy.currentDeductible,
additionalAuthFlag: order.policy.additionalAuthFlag,
},
partSelection: {
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
hasManuallySelectedParts:
!!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
.length,
},
vehicle: {
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
style: vehicle.style,
vin: vehicle.vin ?? "",
},
};
let hasCalled = timeSlotCallFlags.shopV2;
if (!hasCalled) {
timeSlotCallFlags.shopV2 = true;
}
const options = {
method: endpoints.GetShopTimeSlotsV2.method,
endpoint: endpoints.GetShopTimeSlotsV2.url,
payload: payload,
logApiCall: true,
pageNameToLog: pageNameToLog,
};
// Only set handler if this is the very first call in this session
if (!hasCalled) {
options.additionalSuccessEventDataHandler = (response) => {
const firstProviderTimeSlots = response.data.providerTimeSlots?.[0];
return getTimeSlotsAdditionalEventData(
firstProviderTimeSlots?.provisionalTriggers,
order.serviceLocation.zipCode,
firstProviderTimeSlots?.days?.[0]?.date,
shopAppointmentType
);
};
}
return globalMethods.callHttpClient(options);
},
getMobileTimeSlotsV2(context, { payload: { startDate, endDate, zipCode }, pageNameToLog }) {
const order = context.state.order;
const vehicle = context.state.order.vehicle;
const payment = context.state.order.payment;
const lineItems = getFlattenedLineItemsWithGlassPartTag(order.lineItems);
const glassPieces = order.damage.glassToReplace
? convertGlassPieceToBackEndCompatibleFormat(order.damage.glassToReplace)
: [];
var payload = {
startDate: startDate,
endDate: endDate,
applicationName: applicationConfig.APPLICATION_NAME,
parentAccountNumber: payment.parentAccountNumber,
carId: vehicle.carId,
lineItems: lineItems,
glassPieces: glassPieces,
eon: order.eon,
billToAccountNumber: context.getters.payment.billToAccountNumber,
coverage: {
status: payment.insuranceCoverage.coverageStatus,
deductible: order.policy.currentDeductible,
additionalAuthFlag: order.policy.additionalAuthFlag,
},
partSelection: {
hasAnsweredPartQuestions: !!order.damage.partQuestionAnswers?.length,
hasAnsweredMoldingQuestions: !!order.damage.moldingQuestionAnswers?.length,
hasAnsweredCapabilityQuestions: !!order.damage.capabilityQuestionAnswers?.length,
hasManuallySelectedParts:
!!context.state.applicationUser.pageData["vehicle-parts"]?.partsOrQuestions
.length,
},
vehicle: {
year: vehicle.year,
make: vehicle.make,
model: vehicle.model,
style: vehicle.style,
vin: vehicle.vin ?? "",
},
zipCode: zipCode ?? order.serviceLocation.zipCode,
};
let hasCalled = timeSlotCallFlags.mobileV2;
if (!hasCalled) {
timeSlotCallFlags.mobileV2 = true;
}
const options = {
method: endpoints.GetMobileTimeSlotsV2.method,
endpoint: endpoints.GetMobileTimeSlotsV2.url,
payload: payload,
logApiCall: true,
pageNameToLog: pageNameToLog,
};
// Only set handler if this is the very first call in this session
if (!hasCalled) {
options.additionalSuccessEventDataHandler = (response) =>
getTimeSlotsAdditionalEventData(
response.data.provisionalTriggers,
order.serviceLocation.zipCode,
response.data.days?.[0]?.date
);
}
return globalMethods.callHttpClient(options);
},
getMobilePremiumFee(context, { pageNameToLog }) { getMobilePremiumFee(context, { pageNameToLog }) {
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace"; const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash"; const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash";
@ -4530,6 +4673,8 @@ function saveExternalParameterState(externalParameterState) {
const timeSlotCallFlags = { const timeSlotCallFlags = {
shop: false, shop: false,
mobile: false, mobile: false,
shopV2: false,
mobileV2: false,
}; };
function checkIfMiscParts(partsOrQuestions) { function checkIfMiscParts(partsOrQuestions) {