From a6c81ca5b0199484e9a544ac6c06ab37d1d07a96 Mon Sep 17 00:00:00 2001 From: Nate Elliott Date: Wed, 4 Sep 2024 16:41:27 -0500 Subject: [PATCH 001/208] 10275: WIP: set up interactor for evaluating calendaring rules --- ...stedTrialSessionCalendarInteractor.test.ts | 64 +++++++++++++++++++ ...SuggestedTrialSessionCalendarInteractor.ts | 26 ++++++++ 2 files changed, 90 insertions(+) create mode 100644 web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.test.ts create mode 100644 web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.test.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.test.ts new file mode 100644 index 00000000000..59c59bb9e2c --- /dev/null +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.test.ts @@ -0,0 +1,64 @@ +import { + CASE_STATUS_TYPES, + SESSION_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; +import { FORMATS } from '@shared/business/utilities/DateHandler'; +import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; +import { MOCK_LOCK } from '../../../../../shared/src/test/mockLock'; +import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; +import { checkForReadyForTrialCasesInteractor } from '../checkForReadyForTrialCasesInteractor'; +import { generateSuggestedTrialSessionCalendar } from '@web-api/business/useCases/trialSessions/generateSuggestedTrialSessionCalendar'; + +describe('checkForReadyForTrialCasesInteractor', () => { + // beforeAll(() => { + // applicationContext + // .getPersistenceGateway() + // .getReadyForTrialCases.mockImplementation(() => mockCasesReadyForTrial); + + // applicationContext.getPersistenceGateway().updateCase.mockReturnValue({}); + // }); + + // beforeEach(() => { + // applicationContext + // .getPersistenceGateway() + // .getLock.mockReturnValue(undefined); + // }); + + it('should generate a trial term when valid date range is provided', async () => { + const mockStartDate = '2019-08-22T04:00:00.000Z'; + const mockEndDate = '2019-09-22T04:00:00.000Z'; + + const result = await generateSuggestedTrialSessionCalendar( + applicationContext, + { trialTermEndDate: mockEndDate, trialTermStartDate: mockStartDate }, + ); + + expect(result).toEqual([ + { + city: 'City A', + sessionType: SESSION_TYPES.regular, + weekOf: '01/01/01', + }, + { + city: 'City B', + sessionType: SESSION_TYPES.small, + weekOf: '01/07/01', + }, + ]); + }); + + // it('should throw an error when...', async () => { + // mockCasesReadyForTrial = []; + // applicationContext + // .getPersistenceGateway() + // .getCaseByDocketNumber.mockReturnValue(MOCK_CASE); + + // await expect( + // checkForReadyForTrialCasesInteractor(applicationContext), + // ).resolves.not.toThrow(); + + // expect( + // applicationContext.getPersistenceGateway().getReadyForTrialCases, + // ).toHaveBeenCalled(); + // }); +}); diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts new file mode 100644 index 00000000000..9fede700a5f --- /dev/null +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -0,0 +1,26 @@ +import { CASE_STATUS_TYPES } from '../../../../../shared/src/business/entities/EntityConstants'; +import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +import { ServerApplicationContext } from '@web-api/applicationContext'; + +/** + * @param {object} applicationContext the application context + */ +export const generateSuggestedTrialSessionCalendarInteractor = async ( + applicationContext: ServerApplicationContext, + { + trialTermEndDate, + trialTermStartDate, + }: { trialTermEndDate: string; trialTermStartDate: string }, +) => { + // get cases that are ready for trial + // + // Maximum of 6 sessions per week + // Maximum of 5 sessions total per location + // Regular: 40 regular cases minimum to create a session and a maximum of 100 per session + // Small: 40 small cases minimum to create a session and a maximum of 125 per session + // Hybrid: After Small and Regular sessions are created, if small and regular cases can be added together to meet a minimum of 50, create a hybrid session. Maximum will be 100 cases per session + // Special sessions already created will be automatically included + // If there has been no trial in the last two terms for a location, then add a session if there are any cases. (ignore the minimum rule) + // + // data that has a table +}; From 88502900fd6d5aefb26615e754b23cb812acd0fa Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:53:51 -0500 Subject: [PATCH 002/208] 10275: first pass at scheduling algorithm --- ...SuggestedTrialSessionCalendarInteractor.ts | 162 +++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index 9fede700a5f..ccd175669e8 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -1,10 +1,34 @@ import { CASE_STATUS_TYPES } from '../../../../../shared/src/business/entities/EntityConstants'; import { Case } from '../../../../../shared/src/business/entities/cases/Case'; import { ServerApplicationContext } from '@web-api/applicationContext'; +// One session per location per week. +const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // +// Maximum of 6 sessions per week overall. +const MAX_SESSIONS_PER_WEEK = 6; -/** - * @param {object} applicationContext the application context - */ +// Maximum of 5 total sessions per location during a term. +const MAX_SESSIONS_PER_LOCATION = 5; + +// Regular Cases: +// Minimum of 40 cases to create a session. +// Maximum of 100 cases per session. +const REGULAR_CASE_MINIMUM_QUANTITY = 40; +const REGULAR_CASE_MAX_QUANTITY = 100; + +// Small Cases: +// Minimum of 40 cases to create a session. +// Maximum of 125 cases per session. +const SMALL_CASE_MINIMUM_QUANTITY = 40; +const SMALL_CASE_MAX_QUANTITY = 125; + +// Hybrid Sessions: +// If neither Small nor Regular categories alone meet the session minimum, +// combine them to reach a minimum of 50 cases. +// Maximum of 100 cases per hybrid session. +const HYBRID_CASE_MINIMUM_QUANTITY = 50; +const HYBRID_CASE_MAX_QUANTITY = 100; + +// NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid export const generateSuggestedTrialSessionCalendarInteractor = async ( applicationContext: ServerApplicationContext, { @@ -14,7 +38,7 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( ) => { // get cases that are ready for trial // - // Maximum of 6 sessions per week + // Maximum of 6 sessions per week // Maximum of 5 sessions total per location // Regular: 40 regular cases minimum to create a session and a maximum of 100 per session // Small: 40 small cases minimum to create a session and a maximum of 125 per session @@ -23,4 +47,134 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( // If there has been no trial in the last two terms for a location, then add a session if there are any cases. (ignore the minimum rule) // // data that has a table + + return scheduleTrialSessions(); }; + +function scheduleTrialSessions({ + cases, + input, + specialSessions, +}: { + cases: Case[]; + specialSessions: SpecialSession[]; + input: InputForm; +}): TrialSession[] { + const sessions: TrialSession[] = []; + const sessionCountPerWeek: Record = {}; // weekOf -> session count + const sessionCountPerCity: Record = {}; // city -> session count + const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities + + let currentWeek = getMondayOfWeek(input.startDate); + + const formattedSpecialSessions = specialSessions.map(session => { + session.weekOf = getMondayOfWeek(session.startDate); + }); + + while (currentWeek <= input.endDate) { + const weekOfString = currentWeek.toLocaleDateString('en-US', { + day: '2-digit', + month: '2-digit', + }); + + if (!sessionCountPerWeek[weekOfString]) { + sessionCountPerWeek[weekOfString] = 0; + } + + if (!sessionScheduledPerCityPerWeek[weekOfString]) { + sessionScheduledPerCityPerWeek[weekOfString] = new Set(); + } + + const specialSessionsForWeek = formattedSpecialSessions.filter( + s => s.weekOf === weekOfString, + ); + + specialSessionsForWeek.forEach(session => { + sessions.push({ + cases: [], + city: session.city, + sessionType: 'special', + weekOf: session.weekOf, // Special sessions cases are handled differently + }); + + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[session.city]++; + }); + + const cities = cases.map(c => c.location); + + for (const city of cities) { + if (!sessionCountPerCity[city]) { + sessionCountPerCity[city] = 0; + } + + if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { + continue; // Skip this city if a session is already scheduled for this week + } + + if ( + sessionCountPerWeek[weekOfString] < MAX_SESSIONS_PER_WEEK && + sessionCountPerCity[city] < MAX_SESSIONS_PER_LOCATION + ) { + // Handle Regular Sessions + const regularCases = readyCases.filter( + c => c.caseType === 'regular' && c.location === city, + ); + if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { + sessions.push({ + cases: regularCases.slice(0, REGULAR_CASE_MAX_QUANTITY), + city, + sessionType: 'regular', + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week + continue; // Only one session per city per week, so continue to the next city + } + + // Handle Small Sessions + const smallCases = readyCases.filter( + c => c.caseType === 'small' && c.location === city, + ); + if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { + sessions.push({ + cases: smallCases.slice(0, SMALL_CASE_MAX_QUANTITY), + city, + sessionType: 'small', + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + } + + // Handle Hybrid Sessions + const remainingRegularCases = regularCases.slice( + REGULAR_CASE_MAX_QUANTITY, + ); + const remainingSmallCases = smallCases.slice(SMALL_CASE_MAX_QUANTITY); + if ( + remainingRegularCases.length + remainingSmallCases.length >= + HYBRID_CASE_MINIMUM_QUANTITY + ) { + sessions.push({ + cases: [...remainingRegularCases, ...remainingSmallCases].slice( + 0, + HYBRID_CASE_MAX_QUANTITY, + ), + city, + sessionType: 'hybrid', + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + } + } + } + + currentWeek = addWeeks(currentWeek, 1); // Move to the next week + } + + return sessions; +} From cd6fbcdbf6c2c90a7762be9f46c85fbacdf22b23 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:03:34 -0500 Subject: [PATCH 003/208] 10275: replace caseType with correct procedureType, use PROCEDURE_TYPES and SESSION_TYPES constants --- ...SuggestedTrialSessionCalendarInteractor.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index ccd175669e8..d4c5b989166 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -1,8 +1,11 @@ -import { CASE_STATUS_TYPES } from '../../../../../shared/src/business/entities/EntityConstants'; import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +import { + PROCEDURE_TYPES_MAP, + SESSION_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; // One session per location per week. -const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // +const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; //sessionScheduledPerCityPerWeek // Maximum of 6 sessions per week overall. const MAX_SESSIONS_PER_WEEK = 6; @@ -47,8 +50,11 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( // If there has been no trial in the last two terms for a location, then add a session if there are any cases. (ignore the minimum rule) // // data that has a table + const input = { endDate: trialTermEndDate, startDate: trialTermStartDate }; + const cases = getCases(); //? + const specialSessions = getSpecialSessions(); //? - return scheduleTrialSessions(); + return scheduleTrialSessions({ cases, input, specialSessions }); }; function scheduleTrialSessions({ @@ -117,14 +123,16 @@ function scheduleTrialSessions({ sessionCountPerCity[city] < MAX_SESSIONS_PER_LOCATION ) { // Handle Regular Sessions - const regularCases = readyCases.filter( - c => c.caseType === 'regular' && c.location === city, + const regularCases = cases.filter( + c => + c.procedureType === PROCEDURE_TYPES_MAP.regular && + c.location === city, ); if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { sessions.push({ cases: regularCases.slice(0, REGULAR_CASE_MAX_QUANTITY), city, - sessionType: 'regular', + sessionType: SESSION_TYPES.regular, weekOf: weekOfString, }); sessionCountPerWeek[weekOfString]++; @@ -135,14 +143,16 @@ function scheduleTrialSessions({ } // Handle Small Sessions - const smallCases = readyCases.filter( - c => c.caseType === 'small' && c.location === city, + const smallCases = cases.filter( + c => + c.procedureType === PROCEDURE_TYPES_MAP.small && + c.location === city, ); if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { sessions.push({ cases: smallCases.slice(0, SMALL_CASE_MAX_QUANTITY), city, - sessionType: 'small', + sessionType: SESSION_TYPES.small, weekOf: weekOfString, }); sessionCountPerWeek[weekOfString]++; @@ -164,7 +174,7 @@ function scheduleTrialSessions({ HYBRID_CASE_MAX_QUANTITY, ), city, - sessionType: 'hybrid', + sessionType: SESSION_TYPES.hybrid, weekOf: weekOfString, }); sessionCountPerWeek[weekOfString]++; From a2407c31b2560fd74d22af29fd8ac958badd916e Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:14:17 -0500 Subject: [PATCH 004/208] 10275: more constants --- ...generateSuggestedTrialSessionCalendarInteractor.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index d4c5b989166..e937df5eb1e 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -5,7 +5,7 @@ import { } from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; // One session per location per week. -const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; //sessionScheduledPerCityPerWeek +const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek // Maximum of 6 sessions per week overall. const MAX_SESSIONS_PER_WEEK = 6; @@ -32,6 +32,7 @@ const HYBRID_CASE_MINIMUM_QUANTITY = 50; const HYBRID_CASE_MAX_QUANTITY = 100; // NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid + export const generateSuggestedTrialSessionCalendarInteractor = async ( applicationContext: ServerApplicationContext, { @@ -99,7 +100,7 @@ function scheduleTrialSessions({ sessions.push({ cases: [], city: session.city, - sessionType: 'special', + sessionType: SESSION_TYPES.special, weekOf: session.weekOf, // Special sessions cases are handled differently }); @@ -107,7 +108,7 @@ function scheduleTrialSessions({ sessionCountPerCity[session.city]++; }); - const cities = cases.map(c => c.location); + const cities = cases.map(c => c.preferredTrialCity); for (const city of cities) { if (!sessionCountPerCity[city]) { @@ -126,7 +127,7 @@ function scheduleTrialSessions({ const regularCases = cases.filter( c => c.procedureType === PROCEDURE_TYPES_MAP.regular && - c.location === city, + c.preferredTrialCity === city, ); if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { sessions.push({ @@ -146,7 +147,7 @@ function scheduleTrialSessions({ const smallCases = cases.filter( c => c.procedureType === PROCEDURE_TYPES_MAP.small && - c.location === city, + c.preferredTrialCity === city, ); if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { sessions.push({ From 8759bcd0c27078da34a4a3add1d309fe5244ae3d Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:57:18 -0500 Subject: [PATCH 005/208] 10275: nothing at all --- .../generateSuggestedTrialSessionCalendarInteractor.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index e937df5eb1e..623e84c9985 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -52,7 +52,9 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( // // data that has a table const input = { endDate: trialTermEndDate, startDate: trialTermStartDate }; - const cases = getCases(); //? + const cases = applicationContext + .getPersistenceGateway() + .getReadyForTrialCases({ applicationContext }); const specialSessions = getSpecialSessions(); //? return scheduleTrialSessions({ cases, input, specialSessions }); From 3b20da087006102d5f258dbabe1fc6f8c80aa1de Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:44:33 -0500 Subject: [PATCH 006/208] 10275: WIP --- ...SuggestedTrialSessionCalendarInteractor.ts | 172 ++++++++++-------- 1 file changed, 95 insertions(+), 77 deletions(-) diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index 623e84c9985..074ed9e1baa 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -4,6 +4,9 @@ import { SESSION_TYPES, } from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; +import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; +// use different lib? +import { addWeeks, isWithinInterval, startOfWeek } from 'date-fns'; // One session per location per week. const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek // Maximum of 6 sessions per week overall. @@ -35,12 +38,8 @@ const HYBRID_CASE_MAX_QUANTITY = 100; export const generateSuggestedTrialSessionCalendarInteractor = async ( applicationContext: ServerApplicationContext, - { - trialTermEndDate, - trialTermStartDate, - }: { trialTermEndDate: string; trialTermStartDate: string }, + { endDate, startDate }: { endDate: string; startDate: string }, ) => { - // get cases that are ready for trial // // Maximum of 6 sessions per week // Maximum of 5 sessions total per location @@ -50,41 +49,41 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( // Special sessions already created will be automatically included // If there has been no trial in the last two terms for a location, then add a session if there are any cases. (ignore the minimum rule) // - // data that has a table - const input = { endDate: trialTermEndDate, startDate: trialTermStartDate }; - const cases = applicationContext + + const cases = await applicationContext .getPersistenceGateway() .getReadyForTrialCases({ applicationContext }); - const specialSessions = getSpecialSessions(); //? + const sessions = await applicationContext + .getPersistenceGateway() + .getTrialSessions({ applicationContext }); - return scheduleTrialSessions({ cases, input, specialSessions }); + const specialSessions = sessions.filter( + session => session.sessionType === SESSION_TYPES.special, + ); + + return scheduleTrialSessions({ cases, endDate, specialSessions, startDate }); }; function scheduleTrialSessions({ cases, - input, + endDate, specialSessions, + startDate, }: { - cases: Case[]; - specialSessions: SpecialSession[]; - input: InputForm; + cases: RawCase[]; + specialSessions: RawTrialSession[]; + endDate: string; + startDate: string; }): TrialSession[] { - const sessions: TrialSession[] = []; + const sessions: {}[] = []; const sessionCountPerWeek: Record = {}; // weekOf -> session count const sessionCountPerCity: Record = {}; // city -> session count const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities - let currentWeek = getMondayOfWeek(input.startDate); + let currentWeek = getMondayOfWeek(startDate); - const formattedSpecialSessions = specialSessions.map(session => { - session.weekOf = getMondayOfWeek(session.startDate); - }); - - while (currentWeek <= input.endDate) { - const weekOfString = currentWeek.toLocaleDateString('en-US', { - day: '2-digit', - month: '2-digit', - }); + while (currentWeek <= endDate) { + const weekOfString = currentWeek; if (!sessionCountPerWeek[weekOfString]) { sessionCountPerWeek[weekOfString] = 0; @@ -94,23 +93,25 @@ function scheduleTrialSessions({ sessionScheduledPerCityPerWeek[weekOfString] = new Set(); } - const specialSessionsForWeek = formattedSpecialSessions.filter( - s => s.weekOf === weekOfString, + const specialSessionsForWeek = specialSessions.filter( + s => getMondayOfWeek(s.startDate) === weekOfString, ); specialSessionsForWeek.forEach(session => { sessions.push({ cases: [], - city: session.city, + city: session.trialLocation, sessionType: SESSION_TYPES.special, - weekOf: session.weekOf, // Special sessions cases are handled differently + weekOf: getMondayOfWeek(session.startDate), // Special sessions cases are handled differently }); sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[session.city]++; + sessionCountPerCity[session.trialLocation!]++; + sessionScheduledPerCityPerWeek[weekOfString]; }); - const cities = cases.map(c => c.preferredTrialCity); + // filter instead of preferredTrialCity!? + const cities: string[] = cases.map(c => c.preferredTrialCity!); for (const city of cities) { if (!sessionCountPerCity[city]) { @@ -122,72 +123,89 @@ function scheduleTrialSessions({ } if ( - sessionCountPerWeek[weekOfString] < MAX_SESSIONS_PER_WEEK && + sessionCountPerWeek[weekOfString] < MAX_SESSIONS_PER_WEEK && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. sessionCountPerCity[city] < MAX_SESSIONS_PER_LOCATION ) { - // Handle Regular Sessions const regularCases = cases.filter( c => c.procedureType === PROCEDURE_TYPES_MAP.regular && c.preferredTrialCity === city, ); - if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { - sessions.push({ - cases: regularCases.slice(0, REGULAR_CASE_MAX_QUANTITY), - city, - sessionType: SESSION_TYPES.regular, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week - continue; // Only one session per city per week, so continue to the next city - } - // Handle Small Sessions const smallCases = cases.filter( c => c.procedureType === PROCEDURE_TYPES_MAP.small && c.preferredTrialCity === city, ); - if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { - sessions.push({ - cases: smallCases.slice(0, SMALL_CASE_MAX_QUANTITY), - city, - sessionType: SESSION_TYPES.small, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - } - // Handle Hybrid Sessions - const remainingRegularCases = regularCases.slice( - REGULAR_CASE_MAX_QUANTITY, - ); - const remainingSmallCases = smallCases.slice(SMALL_CASE_MAX_QUANTITY); + let regularCaseSliceSize; + let smallCaseSliceSize; + if ( - remainingRegularCases.length + remainingSmallCases.length >= - HYBRID_CASE_MINIMUM_QUANTITY + regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY || + smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY ) { - sessions.push({ - cases: [...remainingRegularCases, ...remainingSmallCases].slice( - 0, - HYBRID_CASE_MAX_QUANTITY, - ), - city, - sessionType: SESSION_TYPES.hybrid, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; + if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { + regularCaseSliceSize = REGULAR_CASE_MAX_QUANTITY; + sessions.push({ + cases: regularCases.slice(0, regularCaseSliceSize), + city, + sessionType: SESSION_TYPES.regular, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week + continue; // Only one session per city per week, so continue to the next city + } + + if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { + smallCaseSliceSize = SMALL_CASE_MAX_QUANTITY; + sessions.push({ + cases: smallCases.slice(0, smallCaseSliceSize), + city, + sessionType: SESSION_TYPES.small, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week + continue; // Only one session per city per week, so continue to the next city + } + } else { + regularCaseSliceSize = 0; + smallCaseSliceSize = 0; + // Handle Hybrid Sessions + const remainingRegularCases = + regularCases.slice(regularCaseSliceSize); + const remainingSmallCases = smallCases.slice(smallCaseSliceSize); + if ( + remainingRegularCases.length + remainingSmallCases.length >= + HYBRID_CASE_MINIMUM_QUANTITY + ) { + sessions.push({ + cases: [...remainingRegularCases, ...remainingSmallCases].slice( + 0, + HYBRID_CASE_MAX_QUANTITY, + ), + city, + sessionType: SESSION_TYPES.hybrid, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + sessionScheduledPerCityPerWeek[weekOfString].add(city); + } } } } - currentWeek = addWeeks(currentWeek, 1); // Move to the next week } - return sessions; } + +// Helper function to get the Monday of the week for a given date +function getMondayOfWeek(date: string): string { + return startOfWeek(date, { weekStartsOn: 1 }); // Monday as the first day of the week +} From 194ecf122684a950f00d2e15502670da07a403ae Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 6 Sep 2024 09:02:33 -0400 Subject: [PATCH 007/208] 10275: work on extracting calendar logic into its own module --- .../scheduleTrialSessions.test.ts | 43 ++++ .../trialSessions/scheduleTrialSessions.ts | 183 +++++++++++++++++ ...SuggestedTrialSessionCalendarInteractor.ts | 184 +----------------- 3 files changed, 228 insertions(+), 182 deletions(-) create mode 100644 web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts create mode 100644 web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts new file mode 100644 index 00000000000..c1ac24a8d79 --- /dev/null +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -0,0 +1,43 @@ +import { + getFakeFile, + testPdfDoc, +} from '../../../../../shared/src/business/test/getFakeFile'; + +import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; +import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; +import { SESSION_TYPES } from '@shared/business/entities/EntityConstants'; +import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; +import { scheduleTrialSessions } from './scheduleTrialSessions'; +import { serveGeneratedNoticesOnCase } from './serveGeneratedNoticesOnCase'; + +const mockCases = []; +const mockSpecialSessions = []; +const mockEndDate = '2019-09-22T04:00:00.000Z'; +const mockStartDate = '2019-08-22T04:00:00.000Z'; + +describe('scheduleTrialSessions', () => { + it('should format case and trial sessions into calendar compatible format', () => { + let params = { + cases: mockCases, + endDate: mockEndDate, + specialSessions: mockSpecialSessions, + startDate: mockStartDate, + }; + + let result = scheduleTrialSessions(params); + + expect(result).toEqual([ + { + city: 'City A', + sessionType: SESSION_TYPES.regular, + weekOf: '01/01/01', + }, + { + city: 'City B', + sessionType: SESSION_TYPES.small, + weekOf: '01/07/01', + }, + ]); + }); +}); diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts new file mode 100644 index 00000000000..cfbe8e2448c --- /dev/null +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -0,0 +1,183 @@ +import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +import { + PROCEDURE_TYPES_MAP, + SESSION_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; +import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; + +// use different lib? +import { addWeeks, isWithinInterval, startOfWeek } from 'date-fns'; +// One session per location per week. +const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek +// Maximum of 6 sessions per week overall. +const MAX_SESSIONS_PER_WEEK = 6; + +// Maximum of 5 total sessions per location during a term. +const MAX_SESSIONS_PER_LOCATION = 5; + +// Regular Cases: +// Minimum of 40 cases to create a session. +// Maximum of 100 cases per session. +const REGULAR_CASE_MINIMUM_QUANTITY = 40; +const REGULAR_CASE_MAX_QUANTITY = 100; + +// Small Cases: +// Minimum of 40 cases to create a session. +// Maximum of 125 cases per session. +const SMALL_CASE_MINIMUM_QUANTITY = 40; +const SMALL_CASE_MAX_QUANTITY = 125; + +// Hybrid Sessions: +// If neither Small nor Regular categories alone meet the session minimum, +// combine them to reach a minimum of 50 cases. +// Maximum of 100 cases per hybrid session. +const HYBRID_CASE_MINIMUM_QUANTITY = 50; +const HYBRID_CASE_MAX_QUANTITY = 100; + +// NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid + +export function scheduleTrialSessions({ + cases, + endDate, + specialSessions, + startDate, +}: { + cases: RawCase[]; + specialSessions: RawTrialSession[]; + endDate: string; + startDate: string; +}): TrialSession[] { + const sessions: {}[] = []; + const sessionCountPerWeek: Record = {}; // weekOf -> session count + const sessionCountPerCity: Record = {}; // city -> session count + const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities + + let currentWeek = getMondayOfWeek(startDate); + + while (currentWeek <= endDate) { + const weekOfString = currentWeek; + + if (!sessionCountPerWeek[weekOfString]) { + sessionCountPerWeek[weekOfString] = 0; + } + + if (!sessionScheduledPerCityPerWeek[weekOfString]) { + sessionScheduledPerCityPerWeek[weekOfString] = new Set(); + } + + const specialSessionsForWeek = specialSessions.filter( + s => getMondayOfWeek(s.startDate) === weekOfString, + ); + + specialSessionsForWeek.forEach(session => { + sessions.push({ + cases: [], + city: session.trialLocation, + sessionType: SESSION_TYPES.special, + weekOf: getMondayOfWeek(session.startDate), // Special sessions cases are handled differently + }); + + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[session.trialLocation!]++; + sessionScheduledPerCityPerWeek[weekOfString]; + }); + + // filter instead of preferredTrialCity!? + const cities: string[] = cases.map(c => c.preferredTrialCity!); + + for (const city of cities) { + if (!sessionCountPerCity[city]) { + sessionCountPerCity[city] = 0; + } + + if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { + continue; // Skip this city if a session is already scheduled for this week + } + + if ( + sessionCountPerWeek[weekOfString] < MAX_SESSIONS_PER_WEEK && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. + sessionCountPerCity[city] < MAX_SESSIONS_PER_LOCATION + ) { + const regularCases = cases.filter( + c => + c.procedureType === PROCEDURE_TYPES_MAP.regular && + c.preferredTrialCity === city, + ); + + const smallCases = cases.filter( + c => + c.procedureType === PROCEDURE_TYPES_MAP.small && + c.preferredTrialCity === city, + ); + + let regularCaseSliceSize; + let smallCaseSliceSize; + + if ( + regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY || + smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY + ) { + if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { + regularCaseSliceSize = REGULAR_CASE_MAX_QUANTITY; + sessions.push({ + cases: regularCases.slice(0, regularCaseSliceSize), + city, + sessionType: SESSION_TYPES.regular, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week + continue; // Only one session per city per week, so continue to the next city + } + + if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { + smallCaseSliceSize = SMALL_CASE_MAX_QUANTITY; + sessions.push({ + cases: smallCases.slice(0, smallCaseSliceSize), + city, + sessionType: SESSION_TYPES.small, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week + continue; // Only one session per city per week, so continue to the next city + } + } else { + regularCaseSliceSize = 0; + smallCaseSliceSize = 0; + // Handle Hybrid Sessions + const remainingRegularCases = + regularCases.slice(regularCaseSliceSize); + const remainingSmallCases = smallCases.slice(smallCaseSliceSize); + if ( + remainingRegularCases.length + remainingSmallCases.length >= + HYBRID_CASE_MINIMUM_QUANTITY + ) { + sessions.push({ + cases: [...remainingRegularCases, ...remainingSmallCases].slice( + 0, + HYBRID_CASE_MAX_QUANTITY, + ), + city, + sessionType: SESSION_TYPES.hybrid, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + sessionScheduledPerCityPerWeek[weekOfString].add(city); + } + } + } + } + currentWeek = addWeeks(currentWeek, 1); // Move to the next week + } + return sessions; +} + +// Helper function to get the Monday of the week for a given date +function getMondayOfWeek(date: string): string { + return startOfWeek(date, { weekStartsOn: 1 }); // Monday as the first day of the week +} diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index 074ed9e1baa..897651768a6 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -1,40 +1,6 @@ -import { Case } from '../../../../../shared/src/business/entities/cases/Case'; -import { - PROCEDURE_TYPES_MAP, - SESSION_TYPES, -} from '../../../../../shared/src/business/entities/EntityConstants'; +import { SESSION_TYPES } from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; -import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; -// use different lib? -import { addWeeks, isWithinInterval, startOfWeek } from 'date-fns'; -// One session per location per week. -const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek -// Maximum of 6 sessions per week overall. -const MAX_SESSIONS_PER_WEEK = 6; - -// Maximum of 5 total sessions per location during a term. -const MAX_SESSIONS_PER_LOCATION = 5; - -// Regular Cases: -// Minimum of 40 cases to create a session. -// Maximum of 100 cases per session. -const REGULAR_CASE_MINIMUM_QUANTITY = 40; -const REGULAR_CASE_MAX_QUANTITY = 100; - -// Small Cases: -// Minimum of 40 cases to create a session. -// Maximum of 125 cases per session. -const SMALL_CASE_MINIMUM_QUANTITY = 40; -const SMALL_CASE_MAX_QUANTITY = 125; - -// Hybrid Sessions: -// If neither Small nor Regular categories alone meet the session minimum, -// combine them to reach a minimum of 50 cases. -// Maximum of 100 cases per hybrid session. -const HYBRID_CASE_MINIMUM_QUANTITY = 50; -const HYBRID_CASE_MAX_QUANTITY = 100; - -// NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid +import { scheduleTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/scheduleTrialSessions'; export const generateSuggestedTrialSessionCalendarInteractor = async ( applicationContext: ServerApplicationContext, @@ -63,149 +29,3 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( return scheduleTrialSessions({ cases, endDate, specialSessions, startDate }); }; - -function scheduleTrialSessions({ - cases, - endDate, - specialSessions, - startDate, -}: { - cases: RawCase[]; - specialSessions: RawTrialSession[]; - endDate: string; - startDate: string; -}): TrialSession[] { - const sessions: {}[] = []; - const sessionCountPerWeek: Record = {}; // weekOf -> session count - const sessionCountPerCity: Record = {}; // city -> session count - const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities - - let currentWeek = getMondayOfWeek(startDate); - - while (currentWeek <= endDate) { - const weekOfString = currentWeek; - - if (!sessionCountPerWeek[weekOfString]) { - sessionCountPerWeek[weekOfString] = 0; - } - - if (!sessionScheduledPerCityPerWeek[weekOfString]) { - sessionScheduledPerCityPerWeek[weekOfString] = new Set(); - } - - const specialSessionsForWeek = specialSessions.filter( - s => getMondayOfWeek(s.startDate) === weekOfString, - ); - - specialSessionsForWeek.forEach(session => { - sessions.push({ - cases: [], - city: session.trialLocation, - sessionType: SESSION_TYPES.special, - weekOf: getMondayOfWeek(session.startDate), // Special sessions cases are handled differently - }); - - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[session.trialLocation!]++; - sessionScheduledPerCityPerWeek[weekOfString]; - }); - - // filter instead of preferredTrialCity!? - const cities: string[] = cases.map(c => c.preferredTrialCity!); - - for (const city of cities) { - if (!sessionCountPerCity[city]) { - sessionCountPerCity[city] = 0; - } - - if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { - continue; // Skip this city if a session is already scheduled for this week - } - - if ( - sessionCountPerWeek[weekOfString] < MAX_SESSIONS_PER_WEEK && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. - sessionCountPerCity[city] < MAX_SESSIONS_PER_LOCATION - ) { - const regularCases = cases.filter( - c => - c.procedureType === PROCEDURE_TYPES_MAP.regular && - c.preferredTrialCity === city, - ); - - const smallCases = cases.filter( - c => - c.procedureType === PROCEDURE_TYPES_MAP.small && - c.preferredTrialCity === city, - ); - - let regularCaseSliceSize; - let smallCaseSliceSize; - - if ( - regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY || - smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY - ) { - if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { - regularCaseSliceSize = REGULAR_CASE_MAX_QUANTITY; - sessions.push({ - cases: regularCases.slice(0, regularCaseSliceSize), - city, - sessionType: SESSION_TYPES.regular, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week - continue; // Only one session per city per week, so continue to the next city - } - - if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { - smallCaseSliceSize = SMALL_CASE_MAX_QUANTITY; - sessions.push({ - cases: smallCases.slice(0, smallCaseSliceSize), - city, - sessionType: SESSION_TYPES.small, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week - continue; // Only one session per city per week, so continue to the next city - } - } else { - regularCaseSliceSize = 0; - smallCaseSliceSize = 0; - // Handle Hybrid Sessions - const remainingRegularCases = - regularCases.slice(regularCaseSliceSize); - const remainingSmallCases = smallCases.slice(smallCaseSliceSize); - if ( - remainingRegularCases.length + remainingSmallCases.length >= - HYBRID_CASE_MINIMUM_QUANTITY - ) { - sessions.push({ - cases: [...remainingRegularCases, ...remainingSmallCases].slice( - 0, - HYBRID_CASE_MAX_QUANTITY, - ), - city, - sessionType: SESSION_TYPES.hybrid, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); - } - } - } - } - currentWeek = addWeeks(currentWeek, 1); // Move to the next week - } - return sessions; -} - -// Helper function to get the Monday of the week for a given date -function getMondayOfWeek(date: string): string { - return startOfWeek(date, { weekStartsOn: 1 }); // Monday as the first day of the week -} From cc91ac98d816314f22fbeddf346bef43277756fd Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:25:11 -0500 Subject: [PATCH 008/208] 10275: WIP on unit test for scheduling logic --- shared/src/test/mockCase.ts | 9 +++ .../scheduleTrialSessions.test.ts | 20 +++++- .../trialSessions/scheduleTrialSessions.ts | 62 +++++++++++++++---- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/shared/src/test/mockCase.ts b/shared/src/test/mockCase.ts index 068bb79c0b7..03406e57609 100644 --- a/shared/src/test/mockCase.ts +++ b/shared/src/test/mockCase.ts @@ -5,6 +5,7 @@ import { COUNTRY_TYPES, PARTY_TYPES, PAYMENT_STATUS, + PROCEDURE_TYPES_MAP, SERVICE_INDICATOR_TYPES, } from '../business/entities/EntityConstants'; import { @@ -513,3 +514,11 @@ export const MOCK_CAV_CONSOLIDATED_MEMBER_CASE = { sortableDocketNumber: 2019000110, status: CASE_STATUS_TYPES.cav, }; + +export const MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING = { + ...MOCK_LEAD_CASE_WITH_PAPER_SERVICE, + leadDocketNumber: undefined, + preferredTrialCity: 'Washington, District of Columbia', + procedureType: PROCEDURE_TYPES_MAP.regular, + status: CASE_STATUS_TYPES.generalDocketReadyForTrial, +}; diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index c1ac24a8d79..cb465571852 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -5,19 +5,35 @@ import { import { Case } from '../../../../../shared/src/business/entities/cases/Case'; import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; -import { MOCK_CASE } from '../../../../../shared/src/test/mockCase'; +import { MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING } from '../../../../../shared/src/test/mockCase'; import { SESSION_TYPES } from '@shared/business/entities/EntityConstants'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { scheduleTrialSessions } from './scheduleTrialSessions'; import { serveGeneratedNoticesOnCase } from './serveGeneratedNoticesOnCase'; -const mockCases = []; const mockSpecialSessions = []; +const mockCalendaringConfig = { + hybridCaseMaxQuantity: 10, + hybridCaseMinimumQuantity: 5, + maxSessionsPerLocation: 5, // Note that we may need to rethink this and maxSessionsPerWeek for testing purposes + maxSessionsPerWeek: 6, + regularCaseMaxQuantity: 10, + regularCaseMinimumQuantity: 4, + smallCaseMaxQuantity: 13, + smallCaseMinimumQuantity: 4, +}; const mockEndDate = '2019-09-22T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; describe('scheduleTrialSessions', () => { it('should format case and trial sessions into calendar compatible format', () => { + const mockCases = [MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING]; + + + for (let i = 0; i < 11; ++i) { + mockCases.push({...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber:`10${i}-${i}${i}`, preferredTrialCity: ,}) + } + let params = { cases: mockCases, endDate: mockEndDate, diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index cfbe8e2448c..01e898c95be 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -1,9 +1,13 @@ import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +import { NumberLiteralType } from 'typescript'; import { PROCEDURE_TYPES_MAP, SESSION_TYPES, } from '../../../../../shared/src/business/entities/EntityConstants'; -import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; +import { + RawTrialSession, + TrialSession, +} from '@shared/business/entities/trialSessions/TrialSession'; // use different lib? import { addWeeks, isWithinInterval, startOfWeek } from 'date-fns'; @@ -36,16 +40,45 @@ const HYBRID_CASE_MAX_QUANTITY = 100; // NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid +export type EligibleCase = Pick< + RawCase, + 'preferredTrialCity' | 'procedureType' +>; + +// export type ScheduledSpecialTrialSession = { +// // +// }; + export function scheduleTrialSessions({ + calendaringConfig = { + hybridCaseMaxQuantity: HYBRID_CASE_MAX_QUANTITY, + hybridCaseMinimumQuantity: HYBRID_CASE_MINIMUM_QUANTITY, + maxSessionsPerLocation: MAX_SESSIONS_PER_LOCATION, + maxSessionsPerWeek: MAX_SESSIONS_PER_WEEK, + regularCaseMaxQuantity: REGULAR_CASE_MAX_QUANTITY, + regularCaseMinimumQuantity: REGULAR_CASE_MINIMUM_QUANTITY, + smallCaseMaxQuantity: SMALL_CASE_MAX_QUANTITY, + smallCaseMinimumQuantity: SMALL_CASE_MINIMUM_QUANTITY, + }, cases, endDate, specialSessions, startDate, }: { - cases: RawCase[]; + cases: EligibleCase[]; specialSessions: RawTrialSession[]; endDate: string; startDate: string; + calendaringConfig: { + maxSessionsPerWeek: number; + maxSessionsPerLocation: number; + regularCaseMinimumQuantity: number; + regularCaseMaxQuantity: number; + smallCaseMinimumQuantity: number; + smallCaseMaxQuantity: number; + hybridCaseMaxQuantity: number; + hybridCaseMinimumQuantity: number; + }; }): TrialSession[] { const sessions: {}[] = []; const sessionCountPerWeek: Record = {}; // weekOf -> session count @@ -71,7 +104,7 @@ export function scheduleTrialSessions({ specialSessionsForWeek.forEach(session => { sessions.push({ - cases: [], + cases: session.caseOrder, city: session.trialLocation, sessionType: SESSION_TYPES.special, weekOf: getMondayOfWeek(session.startDate), // Special sessions cases are handled differently @@ -95,8 +128,9 @@ export function scheduleTrialSessions({ } if ( - sessionCountPerWeek[weekOfString] < MAX_SESSIONS_PER_WEEK && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. - sessionCountPerCity[city] < MAX_SESSIONS_PER_LOCATION + sessionCountPerWeek[weekOfString] < + calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. + sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation ) { const regularCases = cases.filter( c => @@ -114,11 +148,13 @@ export function scheduleTrialSessions({ let smallCaseSliceSize; if ( - regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY || - smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY + regularCases.length >= calendaringConfig.regularCaseMinimumQuantity || + smallCases.length >= calendaringConfig.smallCaseMinimumQuantity ) { - if (regularCases.length >= REGULAR_CASE_MINIMUM_QUANTITY) { - regularCaseSliceSize = REGULAR_CASE_MAX_QUANTITY; + if ( + regularCases.length >= calendaringConfig.regularCaseMinimumQuantity + ) { + regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; sessions.push({ cases: regularCases.slice(0, regularCaseSliceSize), city, @@ -132,8 +168,8 @@ export function scheduleTrialSessions({ continue; // Only one session per city per week, so continue to the next city } - if (smallCases.length >= SMALL_CASE_MINIMUM_QUANTITY) { - smallCaseSliceSize = SMALL_CASE_MAX_QUANTITY; + if (smallCases.length >= calendaringConfig.smallCaseMinimumQuantity) { + smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; sessions.push({ cases: smallCases.slice(0, smallCaseSliceSize), city, @@ -154,12 +190,12 @@ export function scheduleTrialSessions({ const remainingSmallCases = smallCases.slice(smallCaseSliceSize); if ( remainingRegularCases.length + remainingSmallCases.length >= - HYBRID_CASE_MINIMUM_QUANTITY + calendaringConfig.hybridCaseMinimumQuantity ) { sessions.push({ cases: [...remainingRegularCases, ...remainingSmallCases].slice( 0, - HYBRID_CASE_MAX_QUANTITY, + calendaringConfig.hybridCaseMaxQuantity, ), city, sessionType: SESSION_TYPES.hybrid, From e4f73266ae3fcfd6dd5ccaf4fc9d7847ee9f8b84 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:31:36 -0500 Subject: [PATCH 009/208] 10275: WIP unit test for scheduleTrialSessions, refactor to us luxon --- .../scheduleTrialSessions.test.ts | 59 ++++++++++++++----- .../trialSessions/scheduleTrialSessions.ts | 13 +++- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index cb465571852..3a26d3fce41 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -6,7 +6,11 @@ import { import { Case } from '../../../../../shared/src/business/entities/cases/Case'; import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; import { MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING } from '../../../../../shared/src/test/mockCase'; -import { SESSION_TYPES } from '@shared/business/entities/EntityConstants'; +import { + PROCEDURE_TYPES_MAP, + SESSION_TYPES, + TRIAL_CITY_STRINGS, +} from '@shared/business/entities/EntityConstants'; import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { scheduleTrialSessions } from './scheduleTrialSessions'; import { serveGeneratedNoticesOnCase } from './serveGeneratedNoticesOnCase'; @@ -26,15 +30,27 @@ const mockEndDate = '2019-09-22T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; describe('scheduleTrialSessions', () => { - it('should format case and trial sessions into calendar compatible format', () => { + it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity', () => { const mockCases = [MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING]; + const totalNumberOfMockCases = + mockCalendaringConfig.maxSessionsPerWeek * + mockCalendaringConfig.regularCaseMaxQuantity + + mockCalendaringConfig.regularCaseMinimumQuantity; - for (let i = 0; i < 11; ++i) { - mockCases.push({...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber:`10${i}-${i}${i}`, preferredTrialCity: ,}) + for (let i = 0; i < totalNumberOfMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-24`, + preferredTrialCity: TRIAL_CITY_STRINGS[i], + procedureType: PROCEDURE_TYPES_MAP.regular, + }); } + //console.log('mockCases', mockCases); + let params = { + calendaringConfig: mockCalendaringConfig, cases: mockCases, endDate: mockEndDate, specialSessions: mockSpecialSessions, @@ -43,17 +59,28 @@ describe('scheduleTrialSessions', () => { let result = scheduleTrialSessions(params); - expect(result).toEqual([ - { - city: 'City A', - sessionType: SESSION_TYPES.regular, - weekOf: '01/01/01', - }, - { - city: 'City B', - sessionType: SESSION_TYPES.small, - weekOf: '01/07/01', - }, - ]); + const weekOfMap = result.reduce((acc, session) => { + acc[session.weekOf] = (acc[session.weekOf] || 0) + 1; + return acc; + }, {}); + + Object.values(weekOfMap).forEach(count => { + expect(count).toBeLessThanOrEqual( + mockCalendaringConfig.maxSessionsPerWeek, + ); + }); + + // expect(result).toEqual([ + // { + // city: 'City A', + // sessionType: SESSION_TYPES.regular, + // weekOf: '01/01/01', + // }, + // { + // city: 'City B', + // sessionType: SESSION_TYPES.small, + // weekOf: '01/07/01', + // }, + // ]); }); }); diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index 01e898c95be..a1ce0f78818 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -10,7 +10,12 @@ import { } from '@shared/business/entities/trialSessions/TrialSession'; // use different lib? -import { addWeeks, isWithinInterval, startOfWeek } from 'date-fns'; +import { + FORMATS, + createDateAtStartOfWeekEST, +} from '@shared/business/utilities/DateHandler'; +import { addWeeks } from 'date-fns'; + // One session per location per week. const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek // Maximum of 6 sessions per week overall. @@ -45,6 +50,8 @@ export type EligibleCase = Pick< 'preferredTrialCity' | 'procedureType' >; +export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; + // export type ScheduledSpecialTrialSession = { // // // }; @@ -79,7 +86,7 @@ export function scheduleTrialSessions({ hybridCaseMaxQuantity: number; hybridCaseMinimumQuantity: number; }; -}): TrialSession[] { +}): TrialSessionReadyForCalendaring[] { const sessions: {}[] = []; const sessionCountPerWeek: Record = {}; // weekOf -> session count const sessionCountPerCity: Record = {}; // city -> session count @@ -215,5 +222,5 @@ export function scheduleTrialSessions({ // Helper function to get the Monday of the week for a given date function getMondayOfWeek(date: string): string { - return startOfWeek(date, { weekStartsOn: 1 }); // Monday as the first day of the week + return createDateAtStartOfWeekEST(date, FORMATS.MMDDYY); // Monday as the first day of the week } From 76680197a7dee7318e98ae644646bffa9160660d Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:46:45 -0500 Subject: [PATCH 010/208] 10275: add new week based function to DateHandler --- shared/src/business/utilities/DateHandler.ts | 20 +++++++++++++++++++ .../trialSessions/scheduleTrialSessions.ts | 14 ++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/shared/src/business/utilities/DateHandler.ts b/shared/src/business/utilities/DateHandler.ts index cc4641bec08..7aba0819316 100644 --- a/shared/src/business/utilities/DateHandler.ts +++ b/shared/src/business/utilities/DateHandler.ts @@ -654,3 +654,23 @@ export function normalizeIsoDateRange( //validate time string //create IsoDateRange from day + time range } + +/** + * Returns startDate plus n weeksToAdd + * @param {string} startDate the date to add days to + * @param {number} weeksToAdd number of days to add to startDate + * @returns {string} a formatted MMDDYY string if date object is valid + */ +export const addWeeksToDate = ({ + startDate, + weeksToAdd, +}: { + weeksToAdd: number; + startDate: string; +}): string => { + const parsedDate = DateTime.fromFormat(startDate, FORMATS.MMDDYY); + + const newDate = parsedDate.plus({ weeks: weeksToAdd }); + + return newDate.toFormat(FORMATS.MMDDYY); +}; diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index a1ce0f78818..5b2eba34c54 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -1,4 +1,9 @@ import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +import { + FORMATS, + addWeeksToDate, + createDateAtStartOfWeekEST, +} from '@shared/business/utilities/DateHandler'; import { NumberLiteralType } from 'typescript'; import { PROCEDURE_TYPES_MAP, @@ -9,13 +14,6 @@ import { TrialSession, } from '@shared/business/entities/trialSessions/TrialSession'; -// use different lib? -import { - FORMATS, - createDateAtStartOfWeekEST, -} from '@shared/business/utilities/DateHandler'; -import { addWeeks } from 'date-fns'; - // One session per location per week. const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek // Maximum of 6 sessions per week overall. @@ -215,7 +213,7 @@ export function scheduleTrialSessions({ } } } - currentWeek = addWeeks(currentWeek, 1); // Move to the next week + currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week } return sessions; } From cc3441455ea6cda6868d4caaed5b181b4427a63f Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 9 Sep 2024 12:32:55 -0400 Subject: [PATCH 011/208] 10275: factor out addTrialSession function --- .../trialSessions/scheduleTrialSessions.ts | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index 5b2eba34c54..d4e53cc05ae 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -1,10 +1,8 @@ -import { Case } from '../../../../../shared/src/business/entities/cases/Case'; import { FORMATS, addWeeksToDate, createDateAtStartOfWeekEST, } from '@shared/business/utilities/DateHandler'; -import { NumberLiteralType } from 'typescript'; import { PROCEDURE_TYPES_MAP, SESSION_TYPES, @@ -14,8 +12,6 @@ import { TrialSession, } from '@shared/business/entities/trialSessions/TrialSession'; -// One session per location per week. -const MAX_SESSIONS_PER_LOCATION_PER_WEEK = 1; // sessionScheduledPerCityPerWeek // Maximum of 6 sessions per week overall. const MAX_SESSIONS_PER_WEEK = 6; @@ -50,9 +46,10 @@ export type EligibleCase = Pick< export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; -// export type ScheduledSpecialTrialSession = { -// // -// }; +const sessions: {}[] = []; +const sessionCountPerWeek: Record = {}; // weekOf -> session count +const sessionCountPerCity: Record = {}; // city -> session count +const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities export function scheduleTrialSessions({ calendaringConfig = { @@ -85,11 +82,6 @@ export function scheduleTrialSessions({ hybridCaseMinimumQuantity: number; }; }): TrialSessionReadyForCalendaring[] { - const sessions: {}[] = []; - const sessionCountPerWeek: Record = {}; // weekOf -> session count - const sessionCountPerCity: Record = {}; // city -> session count - const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities - let currentWeek = getMondayOfWeek(startDate); while (currentWeek <= endDate) { @@ -108,16 +100,12 @@ export function scheduleTrialSessions({ ); specialSessionsForWeek.forEach(session => { - sessions.push({ + addTrialSession({ cases: session.caseOrder, city: session.trialLocation, sessionType: SESSION_TYPES.special, - weekOf: getMondayOfWeek(session.startDate), // Special sessions cases are handled differently + weekOfString, }); - - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[session.trialLocation!]++; - sessionScheduledPerCityPerWeek[weekOfString]; }); // filter instead of preferredTrialCity!? @@ -160,30 +148,31 @@ export function scheduleTrialSessions({ regularCases.length >= calendaringConfig.regularCaseMinimumQuantity ) { regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - sessions.push({ - cases: regularCases.slice(0, regularCaseSliceSize), + + const casesToBeAdded = regularCases.slice(0, regularCaseSliceSize); + + addTrialSession({ + cases: casesToBeAdded, city, sessionType: SESSION_TYPES.regular, - weekOf: weekOfString, + weekOfString, }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week continue; // Only one session per city per week, so continue to the next city } if (smallCases.length >= calendaringConfig.smallCaseMinimumQuantity) { smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - sessions.push({ - cases: smallCases.slice(0, smallCaseSliceSize), + + const casesToBeAdded = regularCases.slice(0, smallCaseSliceSize); + + addTrialSession({ + cases: casesToBeAdded, city, sessionType: SESSION_TYPES.small, - weekOf: weekOfString, + weekOfString, }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week + continue; // Only one session per city per week, so continue to the next city } } else { @@ -197,24 +186,26 @@ export function scheduleTrialSessions({ remainingRegularCases.length + remainingSmallCases.length >= calendaringConfig.hybridCaseMinimumQuantity ) { - sessions.push({ - cases: [...remainingRegularCases, ...remainingSmallCases].slice( - 0, - calendaringConfig.hybridCaseMaxQuantity, - ), + const casesToBeAdded = [ + ...remainingRegularCases, + ...remainingSmallCases, + ].slice(0, calendaringConfig.hybridCaseMaxQuantity); + + addTrialSession({ + cases: casesToBeAdded, city, sessionType: SESSION_TYPES.hybrid, - weekOf: weekOfString, + weekOfString, }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); } } } } + console.log('currentWeek before', currentWeek); currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week + console.log('currentWeek after', currentWeek); } + return sessions; } @@ -222,3 +213,16 @@ export function scheduleTrialSessions({ function getMondayOfWeek(date: string): string { return createDateAtStartOfWeekEST(date, FORMATS.MMDDYY); // Monday as the first day of the week } + +function addTrialSession({ cases, city, sessionType, weekOfString }) { + sessions.push({ + cases, + city, + sessionType, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; + + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week +} From 4403f830744eccae3bb4e30d7871229d233ec39f Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 9 Sep 2024 13:32:28 -0400 Subject: [PATCH 012/208] 10275: continue working on scheduling logic --- .../trialSessions/scheduleTrialSessions.ts | 82 +++++++++++++------ 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index d4e53cc05ae..2189712f33b 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -108,10 +108,40 @@ export function scheduleTrialSessions({ }); }); - // filter instead of preferredTrialCity!? - const cities: string[] = cases.map(c => c.preferredTrialCity!); + // // filter instead of preferredTrialCity!? + // const cities: string[] = cases.map(c => c.preferredTrialCity!); - for (const city of cities) { + const regularCases = cases.filter( + c => c.procedureType === PROCEDURE_TYPES_MAP.regular, + ); + + const smallCases = cases.filter( + c => c.procedureType === PROCEDURE_TYPES_MAP.small, + ); + + const potentialTrialLocations: Set = new Set(); + + const regularCasesByCity = regularCases.reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations.add(currentCase.preferredTrialCity!); + acc[currentCase.preferredTrialCity!].push(currentCase); + + return acc; + }, {}); + + const smallCasesByCity = smallCases.reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations.add(currentCase.preferredTrialCity!); + acc[currentCase.preferredTrialCity!].push(currentCase); + + return acc; + }, {}); + + for (const city of potentialTrialLocations) { if (!sessionCountPerCity[city]) { sessionCountPerCity[city] = 0; } @@ -125,31 +155,27 @@ export function scheduleTrialSessions({ calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation ) { - const regularCases = cases.filter( - c => - c.procedureType === PROCEDURE_TYPES_MAP.regular && - c.preferredTrialCity === city, - ); - - const smallCases = cases.filter( - c => - c.procedureType === PROCEDURE_TYPES_MAP.small && - c.preferredTrialCity === city, - ); - let regularCaseSliceSize; let smallCaseSliceSize; + let numberOfRegularCasesForCity = regularCasesByCity[city].length; + let numberOfSmallCasesForCity = smallCasesByCity[city].length; if ( - regularCases.length >= calendaringConfig.regularCaseMinimumQuantity || - smallCases.length >= calendaringConfig.smallCaseMinimumQuantity + numberOfRegularCasesForCity >= + calendaringConfig.regularCaseMinimumQuantity || + numberOfSmallCasesForCity >= + calendaringConfig.smallCaseMinimumQuantity ) { if ( - regularCases.length >= calendaringConfig.regularCaseMinimumQuantity + numberOfRegularCasesForCity >= + calendaringConfig.regularCaseMinimumQuantity ) { regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - const casesToBeAdded = regularCases.slice(0, regularCaseSliceSize); + const casesToBeAdded = regularCasesByCity[city].splice( + 0, + regularCaseSliceSize, + ); addTrialSession({ cases: casesToBeAdded, @@ -161,10 +187,16 @@ export function scheduleTrialSessions({ continue; // Only one session per city per week, so continue to the next city } - if (smallCases.length >= calendaringConfig.smallCaseMinimumQuantity) { + if ( + numberOfSmallCasesForCity >= + calendaringConfig.smallCaseMinimumQuantity + ) { smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - const casesToBeAdded = regularCases.slice(0, smallCaseSliceSize); + const casesToBeAdded = smallCasesByCity[city].splice( + 0, + smallCaseSliceSize, + ); addTrialSession({ cases: casesToBeAdded, @@ -176,12 +208,10 @@ export function scheduleTrialSessions({ continue; // Only one session per city per week, so continue to the next city } } else { - regularCaseSliceSize = 0; - smallCaseSliceSize = 0; // Handle Hybrid Sessions - const remainingRegularCases = - regularCases.slice(regularCaseSliceSize); - const remainingSmallCases = smallCases.slice(smallCaseSliceSize); + const remainingRegularCases = regularCasesByCity[city]; + const remainingSmallCases = smallCasesByCity[city]; + if ( remainingRegularCases.length + remainingSmallCases.length >= calendaringConfig.hybridCaseMinimumQuantity From 3b4dd9cb875158f4fff15e10c118d009a6ce45ca Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 9 Sep 2024 14:39:22 -0400 Subject: [PATCH 013/208] 10275: continue work on scheduling logic --- .../scheduleTrialSessions.test.ts | 23 +- .../trialSessions/scheduleTrialSessions.ts | 223 +++++++++--------- 2 files changed, 127 insertions(+), 119 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index 3a26d3fce41..842d31e3238 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -1,19 +1,19 @@ -import { - getFakeFile, - testPdfDoc, -} from '../../../../../shared/src/business/test/getFakeFile'; +// import { +// getFakeFile, +// testPdfDoc, +// } from '../../../../../shared/src/business/test/getFakeFile'; -import { Case } from '../../../../../shared/src/business/entities/cases/Case'; -import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; +// import { Case } from '../../../../../shared/src/business/entities/cases/Case'; +// import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; import { MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING } from '../../../../../shared/src/test/mockCase'; import { PROCEDURE_TYPES_MAP, - SESSION_TYPES, + // SESSION_TYPES, TRIAL_CITY_STRINGS, } from '@shared/business/entities/EntityConstants'; -import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; +// import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { scheduleTrialSessions } from './scheduleTrialSessions'; -import { serveGeneratedNoticesOnCase } from './serveGeneratedNoticesOnCase'; +// import { serveGeneratedNoticesOnCase } from './serveGeneratedNoticesOnCase'; const mockSpecialSessions = []; const mockCalendaringConfig = { @@ -42,12 +42,13 @@ describe('scheduleTrialSessions', () => { mockCases.push({ ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber: `10${i}-24`, - preferredTrialCity: TRIAL_CITY_STRINGS[i], + preferredTrialCity: TRIAL_CITY_STRINGS[0], procedureType: PROCEDURE_TYPES_MAP.regular, }); } - //console.log('mockCases', mockCases); + // console.log('mockCases', mockCases); + // console.log('mockCases.length', mockCases.length); let params = { calendaringConfig: mockCalendaringConfig, diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index 2189712f33b..197efe1fd2d 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -1,7 +1,9 @@ import { FORMATS, addWeeksToDate, + calculateDifferenceInDays, createDateAtStartOfWeekEST, + formatDateString, } from '@shared/business/utilities/DateHandler'; import { PROCEDURE_TYPES_MAP, @@ -84,7 +86,12 @@ export function scheduleTrialSessions({ }): TrialSessionReadyForCalendaring[] { let currentWeek = getMondayOfWeek(startDate); - while (currentWeek <= endDate) { + const differenceInDays = calculateDifferenceInDays( + formatDateString(endDate, FORMATS.ISO), + formatDateString(currentWeek, FORMATS.ISO), + ); + + while (differenceInDays > 0) { const weekOfString = currentWeek; if (!sessionCountPerWeek[weekOfString]) { @@ -108,132 +115,132 @@ export function scheduleTrialSessions({ }); }); - // // filter instead of preferredTrialCity!? - // const cities: string[] = cases.map(c => c.preferredTrialCity!); - - const regularCases = cases.filter( - c => c.procedureType === PROCEDURE_TYPES_MAP.regular, - ); - - const smallCases = cases.filter( - c => c.procedureType === PROCEDURE_TYPES_MAP.small, - ); - const potentialTrialLocations: Set = new Set(); - const regularCasesByCity = regularCases.reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations.add(currentCase.preferredTrialCity!); - acc[currentCase.preferredTrialCity!].push(currentCase); + const regularCasesByCity = cases + .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.regular) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations.add(currentCase.preferredTrialCity!); + acc[currentCase.preferredTrialCity!].push(currentCase); - return acc; - }, {}); + return acc; + }, {}); - const smallCasesByCity = smallCases.reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations.add(currentCase.preferredTrialCity!); - acc[currentCase.preferredTrialCity!].push(currentCase); + const smallCasesByCity = cases + .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.small) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations.add(currentCase.preferredTrialCity!); + acc[currentCase.preferredTrialCity!].push(currentCase); - return acc; - }, {}); + return acc; + }, {}); for (const city of potentialTrialLocations) { - if (!sessionCountPerCity[city]) { - sessionCountPerCity[city] = 0; - } - - if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { - continue; // Skip this city if a session is already scheduled for this week - } - - if ( - sessionCountPerWeek[weekOfString] < - calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. - sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation + // TODO: pick up here (maybe move hybrid handling out of while loop?) + while ( + regularCasesByCity[city]?.length > 0 || + smallCasesByCity[city]?.length > 0 ) { - let regularCaseSliceSize; - let smallCaseSliceSize; - let numberOfRegularCasesForCity = regularCasesByCity[city].length; - let numberOfSmallCasesForCity = smallCasesByCity[city].length; + if (!sessionCountPerCity[city]) { + sessionCountPerCity[city] = 0; + } + + if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { + continue; // Skip this city if a session is already scheduled for this week + } if ( - numberOfRegularCasesForCity >= - calendaringConfig.regularCaseMinimumQuantity || - numberOfSmallCasesForCity >= - calendaringConfig.smallCaseMinimumQuantity + sessionCountPerWeek[weekOfString] < + calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. + sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation ) { - if ( - numberOfRegularCasesForCity >= - calendaringConfig.regularCaseMinimumQuantity - ) { - regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - - const casesToBeAdded = regularCasesByCity[city].splice( - 0, - regularCaseSliceSize, - ); - - addTrialSession({ - cases: casesToBeAdded, - city, - sessionType: SESSION_TYPES.regular, - weekOfString, - }); - - continue; // Only one session per city per week, so continue to the next city - } + let regularCaseSliceSize; + let smallCaseSliceSize; + let numberOfRegularCasesForCity = + regularCasesByCity[city]?.length || 0; + let numberOfSmallCasesForCity = smallCasesByCity[city]?.length || 0; if ( + numberOfRegularCasesForCity >= + calendaringConfig.regularCaseMinimumQuantity || numberOfSmallCasesForCity >= - calendaringConfig.smallCaseMinimumQuantity - ) { - smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - - const casesToBeAdded = smallCasesByCity[city].splice( - 0, - smallCaseSliceSize, - ); - - addTrialSession({ - cases: casesToBeAdded, - city, - sessionType: SESSION_TYPES.small, - weekOfString, - }); - - continue; // Only one session per city per week, so continue to the next city - } - } else { - // Handle Hybrid Sessions - const remainingRegularCases = regularCasesByCity[city]; - const remainingSmallCases = smallCasesByCity[city]; - - if ( - remainingRegularCases.length + remainingSmallCases.length >= - calendaringConfig.hybridCaseMinimumQuantity + calendaringConfig.smallCaseMinimumQuantity ) { - const casesToBeAdded = [ - ...remainingRegularCases, - ...remainingSmallCases, - ].slice(0, calendaringConfig.hybridCaseMaxQuantity); - - addTrialSession({ - cases: casesToBeAdded, - city, - sessionType: SESSION_TYPES.hybrid, - weekOfString, - }); + if ( + numberOfRegularCasesForCity >= + calendaringConfig.regularCaseMinimumQuantity + ) { + regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; + + const casesToBeAdded = regularCasesByCity[city].splice( + 0, + regularCaseSliceSize, + ); + + addTrialSession({ + cases: casesToBeAdded, + city, + sessionType: SESSION_TYPES.regular, + weekOfString, + }); + + continue; // Only one session per city per week, so continue to the next city + } + + if ( + numberOfSmallCasesForCity >= + calendaringConfig.smallCaseMinimumQuantity + ) { + smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; + + const casesToBeAdded = smallCasesByCity[city].splice( + 0, + smallCaseSliceSize, + ); + + addTrialSession({ + cases: casesToBeAdded, + city, + sessionType: SESSION_TYPES.small, + weekOfString, + }); + + continue; // Only one session per city per week, so continue to the next city + } + } else { + // Handle Hybrid Sessions + const remainingRegularCases = regularCasesByCity[city] || []; + const remainingSmallCases = smallCasesByCity[city] || []; + + if ( + remainingRegularCases.length + remainingSmallCases.length >= + calendaringConfig.hybridCaseMinimumQuantity + ) { + const casesToBeAdded = [ + ...remainingRegularCases, + ...remainingSmallCases, + ].slice(0, calendaringConfig.hybridCaseMaxQuantity); + + addTrialSession({ + cases: casesToBeAdded, + city, + sessionType: SESSION_TYPES.hybrid, + weekOfString, + }); + } } } } } - console.log('currentWeek before', currentWeek); + console.debug('currentWeek before', currentWeek); currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week - console.log('currentWeek after', currentWeek); + console.debug('currentWeek after', currentWeek); } return sessions; @@ -241,7 +248,7 @@ export function scheduleTrialSessions({ // Helper function to get the Monday of the week for a given date function getMondayOfWeek(date: string): string { - return createDateAtStartOfWeekEST(date, FORMATS.MMDDYY); // Monday as the first day of the week + return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week } function addTrialSession({ cases, city, sessionType, weekOfString }) { From 56cc5efd80f46b8566c0f61a75dc91c13a775e73 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 9 Sep 2024 16:14:09 -0400 Subject: [PATCH 014/208] 10275: stop infinite looping --- shared/src/business/utilities/DateHandler.ts | 4 +- .../trialSessions/scheduleTrialSessions.ts | 173 +++++++++--------- 2 files changed, 91 insertions(+), 86 deletions(-) diff --git a/shared/src/business/utilities/DateHandler.ts b/shared/src/business/utilities/DateHandler.ts index 7aba0819316..9f841de0058 100644 --- a/shared/src/business/utilities/DateHandler.ts +++ b/shared/src/business/utilities/DateHandler.ts @@ -668,9 +668,9 @@ export const addWeeksToDate = ({ weeksToAdd: number; startDate: string; }): string => { - const parsedDate = DateTime.fromFormat(startDate, FORMATS.MMDDYY); + const parsedDate = DateTime.fromFormat(startDate, FORMATS.ISO); const newDate = parsedDate.plus({ weeks: weeksToAdd }); - return newDate.toFormat(FORMATS.MMDDYY); + return newDate.toFormat(FORMATS.ISO); }; diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index 197efe1fd2d..44fb45443f8 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -86,7 +86,7 @@ export function scheduleTrialSessions({ }): TrialSessionReadyForCalendaring[] { let currentWeek = getMondayOfWeek(startDate); - const differenceInDays = calculateDifferenceInDays( + let differenceInDays = calculateDifferenceInDays( formatDateString(endDate, FORMATS.ISO), formatDateString(currentWeek, FORMATS.ISO), ); @@ -143,103 +143,108 @@ export function scheduleTrialSessions({ for (const city of potentialTrialLocations) { // TODO: pick up here (maybe move hybrid handling out of while loop?) - while ( - regularCasesByCity[city]?.length > 0 || - smallCasesByCity[city]?.length > 0 - ) { - if (!sessionCountPerCity[city]) { - sessionCountPerCity[city] = 0; - } + // while ( + // regularCasesByCity[city]?.length > 0 || + // smallCasesByCity[city]?.length > 0 + // ) { + if (!sessionCountPerCity[city]) { + sessionCountPerCity[city] = 0; + } - if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { - continue; // Skip this city if a session is already scheduled for this week - } + if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { + continue; // Skip this city if a session is already scheduled for this week + } + + if ( + sessionCountPerWeek[weekOfString] < + calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. + sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation + ) { + let regularCaseSliceSize; + let smallCaseSliceSize; + let numberOfRegularCasesForCity = regularCasesByCity[city]?.length || 0; + let numberOfSmallCasesForCity = smallCasesByCity[city]?.length || 0; if ( - sessionCountPerWeek[weekOfString] < - calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. - sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation + numberOfRegularCasesForCity >= + calendaringConfig.regularCaseMinimumQuantity || + numberOfSmallCasesForCity >= + calendaringConfig.smallCaseMinimumQuantity ) { - let regularCaseSliceSize; - let smallCaseSliceSize; - let numberOfRegularCasesForCity = - regularCasesByCity[city]?.length || 0; - let numberOfSmallCasesForCity = smallCasesByCity[city]?.length || 0; - if ( numberOfRegularCasesForCity >= - calendaringConfig.regularCaseMinimumQuantity || + calendaringConfig.regularCaseMinimumQuantity + ) { + regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; + + const casesToBeAdded = regularCasesByCity[city].splice( + 0, + regularCaseSliceSize, + ); + + addTrialSession({ + cases: casesToBeAdded, + city, + sessionType: SESSION_TYPES.regular, + weekOfString, + }); + + continue; // Only one session per city per week, so continue to the next city + } + + if ( numberOfSmallCasesForCity >= - calendaringConfig.smallCaseMinimumQuantity + calendaringConfig.smallCaseMinimumQuantity ) { - if ( - numberOfRegularCasesForCity >= - calendaringConfig.regularCaseMinimumQuantity - ) { - regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - - const casesToBeAdded = regularCasesByCity[city].splice( - 0, - regularCaseSliceSize, - ); - - addTrialSession({ - cases: casesToBeAdded, - city, - sessionType: SESSION_TYPES.regular, - weekOfString, - }); - - continue; // Only one session per city per week, so continue to the next city - } - - if ( - numberOfSmallCasesForCity >= - calendaringConfig.smallCaseMinimumQuantity - ) { - smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - - const casesToBeAdded = smallCasesByCity[city].splice( - 0, - smallCaseSliceSize, - ); - - addTrialSession({ - cases: casesToBeAdded, - city, - sessionType: SESSION_TYPES.small, - weekOfString, - }); - - continue; // Only one session per city per week, so continue to the next city - } - } else { - // Handle Hybrid Sessions - const remainingRegularCases = regularCasesByCity[city] || []; - const remainingSmallCases = smallCasesByCity[city] || []; - - if ( - remainingRegularCases.length + remainingSmallCases.length >= - calendaringConfig.hybridCaseMinimumQuantity - ) { - const casesToBeAdded = [ - ...remainingRegularCases, - ...remainingSmallCases, - ].slice(0, calendaringConfig.hybridCaseMaxQuantity); - - addTrialSession({ - cases: casesToBeAdded, - city, - sessionType: SESSION_TYPES.hybrid, - weekOfString, - }); - } + smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; + + const casesToBeAdded = smallCasesByCity[city].splice( + 0, + smallCaseSliceSize, + ); + + addTrialSession({ + cases: casesToBeAdded, + city, + sessionType: SESSION_TYPES.small, + weekOfString, + }); + + continue; // Only one session per city per week, so continue to the next city } } } + // } + + // Handle Hybrid Sessions + const remainingRegularCases = regularCasesByCity[city] || []; + const remainingSmallCases = smallCasesByCity[city] || []; + + if ( + remainingRegularCases.length + remainingSmallCases.length >= + calendaringConfig.hybridCaseMinimumQuantity + ) { + const casesToBeAdded = [ + ...remainingRegularCases, + ...remainingSmallCases, + ].slice(0, calendaringConfig.hybridCaseMaxQuantity); + + addTrialSession({ + cases: casesToBeAdded, + city, + sessionType: SESSION_TYPES.hybrid, + weekOfString, + }); + } } + console.debug('currentWeek before', currentWeek); currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week + differenceInDays = calculateDifferenceInDays( + formatDateString(endDate, FORMATS.ISO), + formatDateString(currentWeek, FORMATS.ISO), + ); + console.debug('currentWeek after', currentWeek); } From 3484546225157a94f175e1b440ee990c0feac86e Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 9 Sep 2024 16:40:26 -0400 Subject: [PATCH 015/208] 10275: focus on hybrid handling --- .../trialSessions/scheduleTrialSessions.ts | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index 44fb45443f8..53f91da950b 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -8,6 +8,7 @@ import { import { PROCEDURE_TYPES_MAP, SESSION_TYPES, + TrialSessionTypes, } from '../../../../../shared/src/business/entities/EntityConstants'; import { RawTrialSession, @@ -48,7 +49,11 @@ export type EligibleCase = Pick< export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; -const sessions: {}[] = []; +const sessions: { + city: string; + sessionType: TrialSessionTypes; + weekOf: string; +}[] = []; const sessionCountPerWeek: Record = {}; // weekOf -> session count const sessionCountPerCity: Record = {}; // city -> session count const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities @@ -83,7 +88,11 @@ export function scheduleTrialSessions({ hybridCaseMaxQuantity: number; hybridCaseMinimumQuantity: number; }; -}): TrialSessionReadyForCalendaring[] { +}): { + city: string; + sessionType: TrialSessionTypes; + weekOf: string; +}[] { let currentWeek = getMondayOfWeek(startDate); let differenceInDays = calculateDifferenceInDays( @@ -177,13 +186,9 @@ export function scheduleTrialSessions({ ) { regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - const casesToBeAdded = regularCasesByCity[city].splice( - 0, - regularCaseSliceSize, - ); + regularCasesByCity[city].splice(0, regularCaseSliceSize); addTrialSession({ - cases: casesToBeAdded, city, sessionType: SESSION_TYPES.regular, weekOfString, @@ -198,13 +203,9 @@ export function scheduleTrialSessions({ ) { smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - const casesToBeAdded = smallCasesByCity[city].splice( - 0, - smallCaseSliceSize, - ); + smallCasesByCity[city].splice(0, smallCaseSliceSize); addTrialSession({ - cases: casesToBeAdded, city, sessionType: SESSION_TYPES.small, weekOfString, @@ -224,13 +225,19 @@ export function scheduleTrialSessions({ remainingRegularCases.length + remainingSmallCases.length >= calendaringConfig.hybridCaseMinimumQuantity ) { - const casesToBeAdded = [ - ...remainingRegularCases, - ...remainingSmallCases, - ].slice(0, calendaringConfig.hybridCaseMaxQuantity); + // Since the min of reg cases is 40, and the min of small cases is 40, + // and the sum of these two values is below the hybrid case max of 100, + // we can safely assume that if the combination of remaining regular + // cases and remaining small cases is above the minimum of 50, so we can + // assign all of those remaining cases to a hybrid session. + // + // This comment applies to the if statement's condition, as well as to + // the setting of regularCasesByCity[city] and smallCasesByCity[city] to + // empty arrays below. + regularCasesByCity[city] = []; + smallCasesByCity[city] = []; addTrialSession({ - cases: casesToBeAdded, city, sessionType: SESSION_TYPES.hybrid, weekOfString, @@ -256,9 +263,8 @@ function getMondayOfWeek(date: string): string { return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week } -function addTrialSession({ cases, city, sessionType, weekOfString }) { +function addTrialSession({ city, sessionType, weekOfString }) { sessions.push({ - cases, city, sessionType, weekOf: weekOfString, From 90e1fe2c5aab1a5cc8f7b39f5ad48038c4ac238b Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:16:17 -0500 Subject: [PATCH 016/208] 10275: add TODO around potentially inefficient cron job --- .../dynamo/trialSessions/getEligibleCasesForTrialSession.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-api/src/persistence/dynamo/trialSessions/getEligibleCasesForTrialSession.ts b/web-api/src/persistence/dynamo/trialSessions/getEligibleCasesForTrialSession.ts index 8bf8cef396d..0d3e1ad4c9a 100644 --- a/web-api/src/persistence/dynamo/trialSessions/getEligibleCasesForTrialSession.ts +++ b/web-api/src/persistence/dynamo/trialSessions/getEligibleCasesForTrialSession.ts @@ -34,7 +34,8 @@ export const getEligibleCasesForTrialSession = async ({ docketNumbers.push(docketNumber); } }); - + // Why are we fetching the base case record twice?? + // TODO: 10275 const results = await batchGet({ applicationContext, keys: docketNumbers.map(docketNumber => ({ From 39db0e25779f0eca835964020202ed38ad766f9a Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:21:18 -0500 Subject: [PATCH 017/208] 10275: add new (temporary?) getCasesReadyForCalendaring persistence method --- web-api/src/getPersistenceGateway.ts | 2 + .../getCasesReadyForCalendaring.ts | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 web-api/src/persistence/dynamo/trialSessions/getCasesReadyForCalendaring.ts diff --git a/web-api/src/getPersistenceGateway.ts b/web-api/src/getPersistenceGateway.ts index 27dabcf0898..4a39c6bb695 100644 --- a/web-api/src/getPersistenceGateway.ts +++ b/web-api/src/getPersistenceGateway.ts @@ -74,6 +74,7 @@ import { getDocketNumbersByUser, } from './persistence/dynamo/users/getCasesForUser'; import { getCasesMetadataByLeadDocketNumber } from './persistence/dynamo/cases/getCasesMetadataByLeadDocketNumber'; +import { getCasesReadyForCalendaring } from '@web-api/persistence/dynamo/trialSessions/getCasesReadyForCalendaring'; import { getClientId } from './persistence/cognito/getClientId'; import { getColdCases } from './persistence/elasticsearch/reports/getColdCases'; import { getCompletedSectionInboxMessages } from './persistence/elasticsearch/messages/getCompletedSectionInboxMessages'; @@ -240,6 +241,7 @@ const gatewayMethods = { deleteKeyCount, editPractitionerDocument, fetchPendingItems, + getCasesReadyForCalendaring, incrementCounter, incrementKeyCount, markMessageThreadRepliedTo, diff --git a/web-api/src/persistence/dynamo/trialSessions/getCasesReadyForCalendaring.ts b/web-api/src/persistence/dynamo/trialSessions/getCasesReadyForCalendaring.ts new file mode 100644 index 00000000000..c4246a16319 --- /dev/null +++ b/web-api/src/persistence/dynamo/trialSessions/getCasesReadyForCalendaring.ts @@ -0,0 +1,39 @@ +import { batchGet, query } from '../../dynamodbClientService'; + +export const getCasesReadyForCalendaring = async ({ + applicationContext, +}: { + applicationContext: IApplicationContext; +}) => { + const mappings = await query({ + ExpressionAttributeNames: { + '#pk': 'pk', + }, + ExpressionAttributeValues: { + ':pk': 'eligible-for-trial-case-catalog', + }, + KeyConditionExpression: '#pk = :pk', + Limit: 500, + applicationContext, + }); + + let docketNumbers = []; + mappings.map(metadata => { + const { docketNumber } = metadata; + if (docketNumbers.includes(docketNumber)) { + applicationContext.logger.warn( + `Encountered duplicate eligible-for-trial-case-catalog mapping for case ${docketNumber}.`, + ); + } else { + docketNumbers.push(docketNumber); + } + }); + + return await batchGet({ + applicationContext, + keys: docketNumbers.map(docketNumber => ({ + pk: `case|${docketNumber}`, + sk: `case|${docketNumber}`, + })), + }); +}; From 507305ccd74a03db69003c8b08406965953b5180 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:24:49 -0500 Subject: [PATCH 018/208] 10275: add temp script for getting eligible cases, update test statements, clean up logs --- .../run-once-scripts/temp-get-ready-cases.ts | 19 +++++++++++++++++++ .../scheduleTrialSessions.test.ts | 18 +++--------------- .../trialSessions/scheduleTrialSessions.ts | 13 ++----------- 3 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 scripts/run-once-scripts/temp-get-ready-cases.ts diff --git a/scripts/run-once-scripts/temp-get-ready-cases.ts b/scripts/run-once-scripts/temp-get-ready-cases.ts new file mode 100644 index 00000000000..4490575d1a9 --- /dev/null +++ b/scripts/run-once-scripts/temp-get-ready-cases.ts @@ -0,0 +1,19 @@ +import { createApplicationContext } from '@web-api/applicationContext'; +import { environment } from '@web-api/environment'; +import { + getDestinationTableInfo, + requireEnvVars, +} from '../../shared/admin-tools/util'; +import fs from 'fs'; + +requireEnvVars(['ENV']); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + const applicationContext = createApplicationContext(); + + const result = await applicationContext + .getPersistenceGateway() + .getCasesReadyForCalendaring({ applicationContext }); + + fs.writeFileSync('./ourData.json', JSON.stringify(result, null, 2)); +})(); diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index 842d31e3238..764b1603dc0 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -1,21 +1,12 @@ -// import { -// getFakeFile, -// testPdfDoc, -// } from '../../../../../shared/src/business/test/getFakeFile'; - -// import { Case } from '../../../../../shared/src/business/entities/cases/Case'; -// import { DocketEntry } from '../../../../../shared/src/business/entities/DocketEntry'; import { MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING } from '../../../../../shared/src/test/mockCase'; import { PROCEDURE_TYPES_MAP, - // SESSION_TYPES, TRIAL_CITY_STRINGS, } from '@shared/business/entities/EntityConstants'; // import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { scheduleTrialSessions } from './scheduleTrialSessions'; -// import { serveGeneratedNoticesOnCase } from './serveGeneratedNoticesOnCase'; -const mockSpecialSessions = []; +// const mockSpecialSessions = []; const mockCalendaringConfig = { hybridCaseMaxQuantity: 10, hybridCaseMinimumQuantity: 5, @@ -30,7 +21,7 @@ const mockEndDate = '2019-09-22T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; describe('scheduleTrialSessions', () => { - it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity', () => { + it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity given that we only pass cases in one city', () => { const mockCases = [MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING]; const totalNumberOfMockCases = @@ -47,14 +38,11 @@ describe('scheduleTrialSessions', () => { }); } - // console.log('mockCases', mockCases); - // console.log('mockCases.length', mockCases.length); - let params = { calendaringConfig: mockCalendaringConfig, cases: mockCases, endDate: mockEndDate, - specialSessions: mockSpecialSessions, + specialSessions: [], startDate: mockStartDate, }; diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index 53f91da950b..c56fd56be70 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -151,11 +151,6 @@ export function scheduleTrialSessions({ }, {}); for (const city of potentialTrialLocations) { - // TODO: pick up here (maybe move hybrid handling out of while loop?) - // while ( - // regularCasesByCity[city]?.length > 0 || - // smallCasesByCity[city]?.length > 0 - // ) { if (!sessionCountPerCity[city]) { sessionCountPerCity[city] = 0; } @@ -166,7 +161,7 @@ export function scheduleTrialSessions({ if ( sessionCountPerWeek[weekOfString] < - calendaringConfig.maxSessionsPerWeek && // TODO, currently we're going to move to the next city if this limit is reached, and keep checking until we move to the next week. + calendaringConfig.maxSessionsPerWeek && sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation ) { let regularCaseSliceSize; @@ -215,7 +210,6 @@ export function scheduleTrialSessions({ } } } - // } // Handle Hybrid Sessions const remainingRegularCases = regularCasesByCity[city] || []; @@ -228,7 +222,7 @@ export function scheduleTrialSessions({ // Since the min of reg cases is 40, and the min of small cases is 40, // and the sum of these two values is below the hybrid case max of 100, // we can safely assume that if the combination of remaining regular - // cases and remaining small cases is above the minimum of 50, so we can + // cases and remaining small cases is above the minimum of 50, we can // assign all of those remaining cases to a hybrid session. // // This comment applies to the if statement's condition, as well as to @@ -245,14 +239,11 @@ export function scheduleTrialSessions({ } } - console.debug('currentWeek before', currentWeek); currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week differenceInDays = calculateDifferenceInDays( formatDateString(endDate, FORMATS.ISO), formatDateString(currentWeek, FORMATS.ISO), ); - - console.debug('currentWeek after', currentWeek); } return sessions; From 972aec58f90eb7ab48bca4664d46cf66a59b3ccf Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Tue, 10 Sep 2024 17:06:45 -0500 Subject: [PATCH 019/208] 10275 WIP: break scheduling into two separate steps - session bucketing, date assignment. --- .../scheduleTrialSessions.test.ts | 40 ++- .../trialSessions/scheduleTrialSessions.ts | 335 ++++++++++++------ 2 files changed, 262 insertions(+), 113 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index 764b1603dc0..a132858ce4a 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -5,6 +5,7 @@ import { } from '@shared/business/entities/EntityConstants'; // import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; import { scheduleTrialSessions } from './scheduleTrialSessions'; +import mockCases from '../../../../../ourData.json'; // const mockSpecialSessions = []; const mockCalendaringConfig = { @@ -21,23 +22,40 @@ const mockEndDate = '2019-09-22T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; describe('scheduleTrialSessions', () => { - it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity given that we only pass cases in one city', () => { - const mockCases = [MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING]; + it('happy path', () => { + let params = { + calendaringConfig: mockCalendaringConfig, + cases: mockCases, + endDate: mockEndDate, + specialSessions: [], + startDate: mockStartDate, + }; + + let result = scheduleTrialSessions(params); + + // date range - trial sessions should only be scheduled within the provided date range + // e.g. given a 5 week date range, 6 max per week, means a total of 30 sessions per run (given current config/dates) + // We can only schedule 1 session per location per week + // This means that max sessions per run is 5 (maxSessionPerLocation) * number of unique preferred trial cities in test data. + // e.g. this maxes us at 10 sessions and 2 per week. + // We need at least the mi + }); + + it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity', () => { + // for (let i = 0; i < totalNumberOfMockCases; ++i) { + // mockCases.push({ + // ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + // docketNumber: `10${i}-24`, + // preferredTrialCity: TRIAL_CITY_STRINGS[0], + // procedureType: PROCEDURE_TYPES_MAP.regular, + // }); + // } const totalNumberOfMockCases = mockCalendaringConfig.maxSessionsPerWeek * mockCalendaringConfig.regularCaseMaxQuantity + mockCalendaringConfig.regularCaseMinimumQuantity; - for (let i = 0; i < totalNumberOfMockCases; ++i) { - mockCases.push({ - ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, - docketNumber: `10${i}-24`, - preferredTrialCity: TRIAL_CITY_STRINGS[0], - procedureType: PROCEDURE_TYPES_MAP.regular, - }); - } - let params = { calendaringConfig: mockCalendaringConfig, cases: mockCases, diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index c56fd56be70..adcaf3a653a 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -93,75 +93,120 @@ export function scheduleTrialSessions({ sessionType: TrialSessionTypes; weekOf: string; }[] { - let currentWeek = getMondayOfWeek(startDate); - - let differenceInDays = calculateDifferenceInDays( - formatDateString(endDate, FORMATS.ISO), - formatDateString(currentWeek, FORMATS.ISO), - ); + let prospectiveSessions = createProspectiveTrialSessions({ + calendaringConfig, + cases, + }); - while (differenceInDays > 0) { - const weekOfString = currentWeek; + // let scheduledTrialSessions = assignSessionsToWeeks({ + // calendaringConfig, + // endDate, + // prospectiveSessions, + // specialSessions, + // startDate, + // }); - if (!sessionCountPerWeek[weekOfString]) { - sessionCountPerWeek[weekOfString] = 0; - } - - if (!sessionScheduledPerCityPerWeek[weekOfString]) { - sessionScheduledPerCityPerWeek[weekOfString] = new Set(); - } + // return scheduledTrialSessions; + return prospectiveSessions; +} - const specialSessionsForWeek = specialSessions.filter( - s => getMondayOfWeek(s.startDate) === weekOfString, - ); +// Helper function to get the Monday of the week for a given date +function getMondayOfWeek(date: string): string { + return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week +} - specialSessionsForWeek.forEach(session => { - addTrialSession({ - cases: session.caseOrder, - city: session.trialLocation, - sessionType: SESSION_TYPES.special, - weekOfString, - }); - }); +function addTrialSession({ city, sessionType, weekOfString }) { + sessions.push({ + city, + sessionType, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; - const potentialTrialLocations: Set = new Set(); + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week +} - const regularCasesByCity = cases - .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.regular) - .reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations.add(currentCase.preferredTrialCity!); - acc[currentCase.preferredTrialCity!].push(currentCase); +let sessionsV2: { + city: string; + sessionType: TrialSessionTypes; +}[] = []; +function addTrialSessionV2({ city, sessionType }) { + sessionsV2.push({ + city, + sessionType, + }); + sessionCountPerCity[city]++; +} - return acc; - }, {}); +function createProspectiveTrialSessions({ + calendaringConfig = { + hybridCaseMaxQuantity: HYBRID_CASE_MAX_QUANTITY, + hybridCaseMinimumQuantity: HYBRID_CASE_MINIMUM_QUANTITY, + maxSessionsPerLocation: MAX_SESSIONS_PER_LOCATION, + maxSessionsPerWeek: MAX_SESSIONS_PER_WEEK, + regularCaseMaxQuantity: REGULAR_CASE_MAX_QUANTITY, + regularCaseMinimumQuantity: REGULAR_CASE_MINIMUM_QUANTITY, + smallCaseMaxQuantity: SMALL_CASE_MAX_QUANTITY, + smallCaseMinimumQuantity: SMALL_CASE_MINIMUM_QUANTITY, + }, + cases, +}: { + cases: EligibleCase[]; + calendaringConfig: { + maxSessionsPerWeek: number; + maxSessionsPerLocation: number; + regularCaseMinimumQuantity: number; + regularCaseMaxQuantity: number; + smallCaseMinimumQuantity: number; + smallCaseMaxQuantity: number; + hybridCaseMaxQuantity: number; + hybridCaseMinimumQuantity: number; + }; +}): { + city: string; + sessionType: TrialSessionTypes; +}[] { + const potentialTrialLocations: Set = new Set(); - const smallCasesByCity = cases - .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.small) - .reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations.add(currentCase.preferredTrialCity!); - acc[currentCase.preferredTrialCity!].push(currentCase); + const regularCasesByCity = cases + .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.regular) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations.add(currentCase.preferredTrialCity!); + acc[currentCase.preferredTrialCity!].push(currentCase); - return acc; - }, {}); + return acc; + }, {}); - for (const city of potentialTrialLocations) { - if (!sessionCountPerCity[city]) { - sessionCountPerCity[city] = 0; + const smallCasesByCity = cases + .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.small) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; } + potentialTrialLocations.add(currentCase.preferredTrialCity!); + acc[currentCase.preferredTrialCity!].push(currentCase); - if (sessionScheduledPerCityPerWeek[weekOfString].has(city)) { - continue; // Skip this city if a session is already scheduled for this week - } + return acc; + }, {}); + + for (const city of potentialTrialLocations) { + if (!sessionCountPerCity[city]) { + sessionCountPerCity[city] = 0; + } + while ( + regularCasesByCity[city].length >= + calendaringConfig.regularCaseMinimumQuantity || + smallCasesByCity[city].length >= + calendaringConfig.smallCaseMinimumQuantity || + regularCasesByCity[city].length + smallCasesByCity[city]?.length >= + calendaringConfig.hybridCaseMinimumQuantity + ) if ( - sessionCountPerWeek[weekOfString] < - calendaringConfig.maxSessionsPerWeek && sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation ) { let regularCaseSliceSize; @@ -175,6 +220,15 @@ export function scheduleTrialSessions({ numberOfSmallCasesForCity >= calendaringConfig.smallCaseMinimumQuantity ) { + // schedule regular or small + // TODO prioritize the larger backlog + + // Our idea for what needs to change + // split the mega-while into at leaast 2, maybe 3 while + // while there are more regulars than minumum, create regulars + // while there are more smalls than min, create smalls + // with any remaining, create hybrid + if ( numberOfRegularCasesForCity >= calendaringConfig.regularCaseMinimumQuantity @@ -183,13 +237,10 @@ export function scheduleTrialSessions({ regularCasesByCity[city].splice(0, regularCaseSliceSize); - addTrialSession({ + addTrialSessionV2({ city, sessionType: SESSION_TYPES.regular, - weekOfString, }); - - continue; // Only one session per city per week, so continue to the next city } if ( @@ -200,43 +251,140 @@ export function scheduleTrialSessions({ smallCasesByCity[city].splice(0, smallCaseSliceSize); - addTrialSession({ + addTrialSessionV2({ city, sessionType: SESSION_TYPES.small, - weekOfString, }); - - continue; // Only one session per city per week, so continue to the next city } } + // Handle Hybrid Sessions + const remainingRegularCases = regularCasesByCity[city] || []; + const remainingSmallCases = smallCasesByCity[city] || []; + + if ( + remainingRegularCases.length + remainingSmallCases.length >= + calendaringConfig.hybridCaseMinimumQuantity + ) { + // Since the min of reg cases is 40, and the min of small cases is 40, + // and the sum of these two values is below the hybrid case max of 100, + // we can safely assume that if the combination of remaining regular + // cases and remaining small cases is above the minimum of 50, we can + // assign all of those remaining cases to a hybrid session. + // + // This comment applies to the if statement's condition, as well as to + // the setting of regularCasesByCity[city] and smallCasesByCity[city] to + // empty arrays below. + regularCasesByCity[city] = []; + smallCasesByCity[city] = []; + + addTrialSessionV2({ + city, + sessionType: SESSION_TYPES.hybrid, + }); + } } + } - // Handle Hybrid Sessions - const remainingRegularCases = regularCasesByCity[city] || []; - const remainingSmallCases = smallCasesByCity[city] || []; + // TODO don't forget we need to deal with overrides. + return sessionsV2; +} + +function assignSessionsToWeeks({ + calendaringConfig, + endDate, + prospectiveSessions, + specialSessions, + startDate, +}: { + specialSessions: RawTrialSession[]; + prospectiveSessions: { + city: string; + sessionType: TrialSessionTypes; + }[]; + endDate: string; + startDate: string; + calendaringConfig: { + maxSessionsPerWeek: number; + maxSessionsPerLocation: number; + regularCaseMinimumQuantity: number; + regularCaseMaxQuantity: number; + smallCaseMinimumQuantity: number; + smallCaseMaxQuantity: number; + hybridCaseMaxQuantity: number; + hybridCaseMinimumQuantity: number; + }; +}): { + city: string; + sessionType: TrialSessionTypes; + weekOf: string; +}[] { + // -- Prioritize overridden and special sessions that have already been scheduled + // -- Max 1 per location per week. + // -- Max x per week across all locations + + const potentialTrialLocations: Set = new Set(); + prospectiveSessions.forEach(prospectiveSession => { + potentialTrialLocations.add(prospectiveSession.city); + }); + + let currentWeek = getMondayOfWeek(startDate); + + let differenceInDays = calculateDifferenceInDays( + formatDateString(endDate, FORMATS.ISO), + formatDateString(currentWeek, FORMATS.ISO), + ); + + while (differenceInDays > 0) { + const weekOfString = currentWeek; + + if (!sessionCountPerWeek[weekOfString]) { + sessionCountPerWeek[weekOfString] = 0; + } + + if ( + sessionCountPerWeek[weekOfString] < calendaringConfig.maxSessionsPerWeek + ) { + continue; + } + + if (!sessionScheduledPerCityPerWeek[weekOfString]) { + sessionScheduledPerCityPerWeek[weekOfString] = new Set(); + } + + const specialSessionsForWeek = specialSessions.filter( + s => getMondayOfWeek(s.startDate) === weekOfString, + ); + + specialSessionsForWeek.forEach(session => { + addTrialSession({ + city: session.trialLocation, + sessionType: SESSION_TYPES.special, + weekOfString, + }); + }); + + for (const prospectiveSession of prospectiveSessions) { + // do the thing if ( - remainingRegularCases.length + remainingSmallCases.length >= - calendaringConfig.hybridCaseMinimumQuantity + sessionScheduledPerCityPerWeek[weekOfString].has( + prospectiveSession.city, + ) + ) { + continue; // Skip this city if a session is already scheduled for this week + } + + if ( + sessionCountPerCity[prospectiveSession.city] >= + calendaringConfig.maxSessionsPerLocation ) { - // Since the min of reg cases is 40, and the min of small cases is 40, - // and the sum of these two values is below the hybrid case max of 100, - // we can safely assume that if the combination of remaining regular - // cases and remaining small cases is above the minimum of 50, we can - // assign all of those remaining cases to a hybrid session. - // - // This comment applies to the if statement's condition, as well as to - // the setting of regularCasesByCity[city] and smallCasesByCity[city] to - // empty arrays below. - regularCasesByCity[city] = []; - smallCasesByCity[city] = []; - - addTrialSession({ - city, - sessionType: SESSION_TYPES.hybrid, - weekOfString, - }); + continue; } + + addTrialSession({ + ...prospectiveSession, + weekOfString, + }); } currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week @@ -248,20 +396,3 @@ export function scheduleTrialSessions({ return sessions; } - -// Helper function to get the Monday of the week for a given date -function getMondayOfWeek(date: string): string { - return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week -} - -function addTrialSession({ city, sessionType, weekOfString }) { - sessions.push({ - city, - sessionType, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week -} From 92601ea83ac7183d78c842626ef445a547662e44 Mon Sep 17 00:00:00 2001 From: Nate Elliott Date: Wed, 11 Sep 2024 12:44:48 -0500 Subject: [PATCH 020/208] 10275 WIP dealing with infinite loops --- shared/src/business/utilities/DateHandler.ts | 25 ++ .../scheduleTrialSessions.test.ts | 32 +- .../trialSessions/scheduleTrialSessions.ts | 277 ++++++++++-------- 3 files changed, 194 insertions(+), 140 deletions(-) diff --git a/shared/src/business/utilities/DateHandler.ts b/shared/src/business/utilities/DateHandler.ts index 9f841de0058..1499fdd6167 100644 --- a/shared/src/business/utilities/DateHandler.ts +++ b/shared/src/business/utilities/DateHandler.ts @@ -674,3 +674,28 @@ export const addWeeksToDate = ({ return newDate.toFormat(FORMATS.ISO); }; + +export const getWeeksInRange = ({ + endDate, + startDate, +}: { + startDate: string; + endDate: string; +}): string[] => { + // Parse the start and end dates + let start = DateTime.fromISO(startDate).startOf('week'); // Get the Monday of the start week + const end = DateTime.fromISO(endDate).startOf('week'); // Get the Monday of the end week + + const weeks: string[] = []; + + // Loop through each week, adding each Monday to the array + while (start <= end) { + const isoStart = start.toISODate(); + if (isoStart !== null) { + weeks.push(isoStart); // Add the Monday (ISO format) to the array + } + start = start.plus({ weeks: 1 }); // Move to the next Monday + } + + return weeks; +}; diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index a132858ce4a..28c45be0b0e 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -22,24 +22,24 @@ const mockEndDate = '2019-09-22T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; describe('scheduleTrialSessions', () => { - it('happy path', () => { - let params = { - calendaringConfig: mockCalendaringConfig, - cases: mockCases, - endDate: mockEndDate, - specialSessions: [], - startDate: mockStartDate, - }; + // it('happy path', () => { + // let params = { + // calendaringConfig: mockCalendaringConfig, + // cases: mockCases, + // endDate: mockEndDate, + // specialSessions: [], + // startDate: mockStartDate, + // }; - let result = scheduleTrialSessions(params); + // let result = scheduleTrialSessions(params); - // date range - trial sessions should only be scheduled within the provided date range - // e.g. given a 5 week date range, 6 max per week, means a total of 30 sessions per run (given current config/dates) - // We can only schedule 1 session per location per week - // This means that max sessions per run is 5 (maxSessionPerLocation) * number of unique preferred trial cities in test data. - // e.g. this maxes us at 10 sessions and 2 per week. - // We need at least the mi - }); + // // date range - trial sessions should only be scheduled within the provided date range + // // e.g. given a 5 week date range, 6 max per week, means a total of 30 sessions per run (given current config/dates) + // // We can only schedule 1 session per location per week + // // This means that max sessions per run is 5 (maxSessionPerLocation) * number of unique preferred trial cities in test data. + // // e.g. this maxes us at 10 sessions and 2 per week. + // // We need at least the mi + // }); it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity', () => { // for (let i = 0; i < totalNumberOfMockCases; ++i) { diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index adcaf3a653a..dc14dcbfcd9 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -4,6 +4,7 @@ import { calculateDifferenceInDays, createDateAtStartOfWeekEST, formatDateString, + getWeeksInRange, } from '@shared/business/utilities/DateHandler'; import { PROCEDURE_TYPES_MAP, @@ -49,11 +50,6 @@ export type EligibleCase = Pick< export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; -const sessions: { - city: string; - sessionType: TrialSessionTypes; - weekOf: string; -}[] = []; const sessionCountPerWeek: Record = {}; // weekOf -> session count const sessionCountPerCity: Record = {}; // city -> session count const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities @@ -93,21 +89,21 @@ export function scheduleTrialSessions({ sessionType: TrialSessionTypes; weekOf: string; }[] { - let prospectiveSessions = createProspectiveTrialSessions({ + console.log('did we get here'); + const prospectiveSessions = createProspectiveTrialSessions({ calendaringConfig, cases, }); - // let scheduledTrialSessions = assignSessionsToWeeks({ - // calendaringConfig, - // endDate, - // prospectiveSessions, - // specialSessions, - // startDate, - // }); + const scheduledTrialSessions = assignSessionsToWeeks({ + calendaringConfig, + endDate, + prospectiveSessions, + specialSessions, + startDate, + }); - // return scheduledTrialSessions; - return prospectiveSessions; + return scheduledTrialSessions; } // Helper function to get the Monday of the week for a given date @@ -115,24 +111,27 @@ function getMondayOfWeek(date: string): string { return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week } -function addTrialSession({ city, sessionType, weekOfString }) { - sessions.push({ +function addScheduledTrialSession({ + city, + scheduledSessions, + sessionType, + weekOfString, +}) { + scheduledSessions.push({ city, sessionType, weekOf: weekOfString, }); sessionCountPerWeek[weekOfString]++; - sessionCountPerCity[city]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week } -let sessionsV2: { - city: string; - sessionType: TrialSessionTypes; -}[] = []; -function addTrialSessionV2({ city, sessionType }) { - sessionsV2.push({ +function addProspectiveTrialSession({ + city, + prospectiveSessions, + sessionType, +}) { + prospectiveSessions.push({ city, sessionType, }); @@ -167,6 +166,10 @@ function createProspectiveTrialSessions({ city: string; sessionType: TrialSessionTypes; }[] { + let prospectiveSessions: { + city: string; + sessionType: TrialSessionTypes; + }[] = []; const potentialTrialLocations: Set = new Set(); const regularCasesByCity = cases @@ -199,95 +202,123 @@ function createProspectiveTrialSessions({ } while ( - regularCasesByCity[city].length >= - calendaringConfig.regularCaseMinimumQuantity || - smallCasesByCity[city].length >= - calendaringConfig.smallCaseMinimumQuantity || - regularCasesByCity[city].length + smallCasesByCity[city]?.length >= - calendaringConfig.hybridCaseMinimumQuantity - ) + sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation + ) { + let regularCaseSliceSize; + let smallCaseSliceSize; + // One of these arrays will continue to decrease in size until it is smaller than the other, at which point prioritization below will flip. + // For now, we are okay with this -- TODO 10275 confirm + // schedule regular or small + if (regularCasesByCity[city]?.length > smallCasesByCity[city]?.length) { + scheduleRegularCases({ + calendaringConfig, + city, + prospectiveSessions, + regularCaseSliceSize, + regularCasesByCity, + }); + scheduleSmallCases({ + calendaringConfig, + city, + prospectiveSessions, + smallCaseSliceSize, + smallCasesByCity, + }); + } else { + scheduleSmallCases({ + calendaringConfig, + city, + prospectiveSessions, + smallCaseSliceSize, + smallCasesByCity, + }); + scheduleRegularCases({ + calendaringConfig, + city, + prospectiveSessions, + regularCaseSliceSize, + regularCasesByCity, + }); + } + + // Handle Hybrid Sessions + const remainingRegularCases = regularCasesByCity[city] || []; + const remainingSmallCases = smallCasesByCity[city] || []; + if ( - sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation + remainingRegularCases?.length + remainingSmallCases.length >= + calendaringConfig.hybridCaseMinimumQuantity ) { - let regularCaseSliceSize; - let smallCaseSliceSize; - let numberOfRegularCasesForCity = regularCasesByCity[city]?.length || 0; - let numberOfSmallCasesForCity = smallCasesByCity[city]?.length || 0; - - if ( - numberOfRegularCasesForCity >= - calendaringConfig.regularCaseMinimumQuantity || - numberOfSmallCasesForCity >= - calendaringConfig.smallCaseMinimumQuantity - ) { - // schedule regular or small - // TODO prioritize the larger backlog - - // Our idea for what needs to change - // split the mega-while into at leaast 2, maybe 3 while - // while there are more regulars than minumum, create regulars - // while there are more smalls than min, create smalls - // with any remaining, create hybrid - - if ( - numberOfRegularCasesForCity >= - calendaringConfig.regularCaseMinimumQuantity - ) { - regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - - regularCasesByCity[city].splice(0, regularCaseSliceSize); - - addTrialSessionV2({ - city, - sessionType: SESSION_TYPES.regular, - }); - } - - if ( - numberOfSmallCasesForCity >= - calendaringConfig.smallCaseMinimumQuantity - ) { - smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - - smallCasesByCity[city].splice(0, smallCaseSliceSize); - - addTrialSessionV2({ - city, - sessionType: SESSION_TYPES.small, - }); - } - } - // Handle Hybrid Sessions - const remainingRegularCases = regularCasesByCity[city] || []; - const remainingSmallCases = smallCasesByCity[city] || []; - - if ( - remainingRegularCases.length + remainingSmallCases.length >= - calendaringConfig.hybridCaseMinimumQuantity - ) { - // Since the min of reg cases is 40, and the min of small cases is 40, - // and the sum of these two values is below the hybrid case max of 100, - // we can safely assume that if the combination of remaining regular - // cases and remaining small cases is above the minimum of 50, we can - // assign all of those remaining cases to a hybrid session. - // - // This comment applies to the if statement's condition, as well as to - // the setting of regularCasesByCity[city] and smallCasesByCity[city] to - // empty arrays below. - regularCasesByCity[city] = []; - smallCasesByCity[city] = []; - - addTrialSessionV2({ - city, - sessionType: SESSION_TYPES.hybrid, - }); - } + // Since the min of reg cases is 40, and the min of small cases is 40, + // and the sum of these two values is below the hybrid case max of 100, + // we can safely assume that if the combination of remaining regular + // cases and remaining small cases is above the minimum of 50, we can + // assign all of those remaining cases to a hybrid session. + // + // This comment applies to the if statement's condition, as well as to + // the setting of regularCasesByCity[city] and smallCasesByCity[city] to + // empty arrays below. + regularCasesByCity[city] = []; + smallCasesByCity[city] = []; + + addProspectiveTrialSession({ + city, + prospectiveSessions, + sessionType: SESSION_TYPES.hybrid, + }); } + } } // TODO don't forget we need to deal with overrides. - return sessionsV2; + return prospectiveSessions; +} + +function scheduleRegularCases({ + calendaringConfig, + city, + prospectiveSessions, + regularCasesByCity, + regularCaseSliceSize, +}) { + while ( + (regularCasesByCity[city]?.length || 0) >= + calendaringConfig.regularCaseMinimumQuantity + ) { + regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; + + regularCasesByCity[city].splice(0, regularCaseSliceSize); + + addProspectiveTrialSession({ + city, + prospectiveSessions, + sessionType: SESSION_TYPES.regular, + }); + } +} + +function scheduleSmallCases({ + calendaringConfig, + city, + prospectiveSessions, + smallCasesByCity, + smallCaseSliceSize, +}) { + while ( + (smallCasesByCity[city].length || 0) >= + calendaringConfig.smallCaseMinimumQuantity + ) { + smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; + + smallCasesByCity[city].splice(0, smallCaseSliceSize); + + addProspectiveTrialSession({ + city, + prospectiveSessions, + sessionType: SESSION_TYPES.small, + }); + } } function assignSessionsToWeeks({ @@ -323,19 +354,21 @@ function assignSessionsToWeeks({ // -- Max 1 per location per week. // -- Max x per week across all locations + const scheduledSessions: { + city: string; + sessionType: TrialSessionTypes; + weekOf: string; + }[] = []; + const potentialTrialLocations: Set = new Set(); prospectiveSessions.forEach(prospectiveSession => { potentialTrialLocations.add(prospectiveSession.city); }); - let currentWeek = getMondayOfWeek(startDate); - - let differenceInDays = calculateDifferenceInDays( - formatDateString(endDate, FORMATS.ISO), - formatDateString(currentWeek, FORMATS.ISO), - ); + // Get array of weeks in range to loop through + const weeksToLoop = getWeeksInRange({ endDate, startDate }); - while (differenceInDays > 0) { + for (const currentWeek of weeksToLoop) { const weekOfString = currentWeek; if (!sessionCountPerWeek[weekOfString]) { @@ -343,7 +376,7 @@ function assignSessionsToWeeks({ } if ( - sessionCountPerWeek[weekOfString] < calendaringConfig.maxSessionsPerWeek + sessionCountPerWeek[weekOfString] >= calendaringConfig.maxSessionsPerWeek ) { continue; } @@ -357,8 +390,9 @@ function assignSessionsToWeeks({ ); specialSessionsForWeek.forEach(session => { - addTrialSession({ + addScheduledTrialSession({ city: session.trialLocation, + scheduledSessions, sessionType: SESSION_TYPES.special, weekOfString, }); @@ -381,18 +415,13 @@ function assignSessionsToWeeks({ continue; } - addTrialSession({ + addScheduledTrialSession({ ...prospectiveSession, + scheduledSessions, weekOfString, }); } - - currentWeek = addWeeksToDate({ startDate: currentWeek, weeksToAdd: 1 }); // Move to the next week - differenceInDays = calculateDifferenceInDays( - formatDateString(endDate, FORMATS.ISO), - formatDateString(currentWeek, FORMATS.ISO), - ); } - return sessions; + return scheduledSessions; } From a5d318c872f1bd48db642c161dac05f16a6df4f7 Mon Sep 17 00:00:00 2001 From: Nate Elliott Date: Wed, 11 Sep 2024 17:03:27 -0500 Subject: [PATCH 021/208] 10275 WIP establish first few test cases for createProspectiveTrialSessions --- shared/src/test/mockCase.ts | 4 +- .../scheduleTrialSessions.test.ts | 1 + .../trialSessions/scheduleTrialSessions.ts | 351 +----------------- .../assignSessionsToWeeks.ts | 139 +++++++ .../createProspectiveTrialSessions.test.ts | 163 ++++++++ .../createProspectiveTrialSessions.ts | 198 ++++++++++ 6 files changed, 510 insertions(+), 346 deletions(-) create mode 100644 web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts create mode 100644 web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts create mode 100644 web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts diff --git a/shared/src/test/mockCase.ts b/shared/src/test/mockCase.ts index 03406e57609..9bbfa02e8b0 100644 --- a/shared/src/test/mockCase.ts +++ b/shared/src/test/mockCase.ts @@ -515,8 +515,8 @@ export const MOCK_CAV_CONSOLIDATED_MEMBER_CASE = { status: CASE_STATUS_TYPES.cav, }; -export const MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING = { - ...MOCK_LEAD_CASE_WITH_PAPER_SERVICE, +export const MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING: RawCase = { + ...MOCK_SUBMITTED_CASE, leadDocketNumber: undefined, preferredTrialCity: 'Washington, District of Columbia', procedureType: PROCEDURE_TYPES_MAP.regular, diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts index 28c45be0b0e..852e24996ed 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts @@ -77,6 +77,7 @@ describe('scheduleTrialSessions', () => { ); }); + console.log('weekOfMap', weekOfMap); // expect(result).toEqual([ // { // city: 'City A', diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts index dc14dcbfcd9..af7230e6f43 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts @@ -1,20 +1,13 @@ import { - FORMATS, - addWeeksToDate, - calculateDifferenceInDays, - createDateAtStartOfWeekEST, - formatDateString, - getWeeksInRange, -} from '@shared/business/utilities/DateHandler'; -import { - PROCEDURE_TYPES_MAP, - SESSION_TYPES, - TrialSessionTypes, -} from '../../../../../shared/src/business/entities/EntityConstants'; + EligibleCase, + createProspectiveTrialSessions, +} from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions'; import { RawTrialSession, TrialSession, } from '@shared/business/entities/trialSessions/TrialSession'; +import { TrialSessionTypes } from '../../../../../shared/src/business/entities/EntityConstants'; +import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; // Maximum of 6 sessions per week overall. const MAX_SESSIONS_PER_WEEK = 6; @@ -43,17 +36,8 @@ const HYBRID_CASE_MAX_QUANTITY = 100; // NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid -export type EligibleCase = Pick< - RawCase, - 'preferredTrialCity' | 'procedureType' ->; - export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; -const sessionCountPerWeek: Record = {}; // weekOf -> session count -const sessionCountPerCity: Record = {}; // city -> session count -const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities - export function scheduleTrialSessions({ calendaringConfig = { hybridCaseMaxQuantity: HYBRID_CASE_MAX_QUANTITY, @@ -89,8 +73,7 @@ export function scheduleTrialSessions({ sessionType: TrialSessionTypes; weekOf: string; }[] { - console.log('did we get here'); - const prospectiveSessions = createProspectiveTrialSessions({ + const prospectiveSessionsByCity = createProspectiveTrialSessions({ calendaringConfig, cases, }); @@ -98,330 +81,10 @@ export function scheduleTrialSessions({ const scheduledTrialSessions = assignSessionsToWeeks({ calendaringConfig, endDate, - prospectiveSessions, + prospectiveSessionsByCity, specialSessions, startDate, }); return scheduledTrialSessions; } - -// Helper function to get the Monday of the week for a given date -function getMondayOfWeek(date: string): string { - return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week -} - -function addScheduledTrialSession({ - city, - scheduledSessions, - sessionType, - weekOfString, -}) { - scheduledSessions.push({ - city, - sessionType, - weekOf: weekOfString, - }); - sessionCountPerWeek[weekOfString]++; - sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week -} - -function addProspectiveTrialSession({ - city, - prospectiveSessions, - sessionType, -}) { - prospectiveSessions.push({ - city, - sessionType, - }); - sessionCountPerCity[city]++; -} - -function createProspectiveTrialSessions({ - calendaringConfig = { - hybridCaseMaxQuantity: HYBRID_CASE_MAX_QUANTITY, - hybridCaseMinimumQuantity: HYBRID_CASE_MINIMUM_QUANTITY, - maxSessionsPerLocation: MAX_SESSIONS_PER_LOCATION, - maxSessionsPerWeek: MAX_SESSIONS_PER_WEEK, - regularCaseMaxQuantity: REGULAR_CASE_MAX_QUANTITY, - regularCaseMinimumQuantity: REGULAR_CASE_MINIMUM_QUANTITY, - smallCaseMaxQuantity: SMALL_CASE_MAX_QUANTITY, - smallCaseMinimumQuantity: SMALL_CASE_MINIMUM_QUANTITY, - }, - cases, -}: { - cases: EligibleCase[]; - calendaringConfig: { - maxSessionsPerWeek: number; - maxSessionsPerLocation: number; - regularCaseMinimumQuantity: number; - regularCaseMaxQuantity: number; - smallCaseMinimumQuantity: number; - smallCaseMaxQuantity: number; - hybridCaseMaxQuantity: number; - hybridCaseMinimumQuantity: number; - }; -}): { - city: string; - sessionType: TrialSessionTypes; -}[] { - let prospectiveSessions: { - city: string; - sessionType: TrialSessionTypes; - }[] = []; - const potentialTrialLocations: Set = new Set(); - - const regularCasesByCity = cases - .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.regular) - .reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations.add(currentCase.preferredTrialCity!); - acc[currentCase.preferredTrialCity!].push(currentCase); - - return acc; - }, {}); - - const smallCasesByCity = cases - .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.small) - .reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations.add(currentCase.preferredTrialCity!); - acc[currentCase.preferredTrialCity!].push(currentCase); - - return acc; - }, {}); - - for (const city of potentialTrialLocations) { - if (!sessionCountPerCity[city]) { - sessionCountPerCity[city] = 0; - } - - while ( - sessionCountPerCity[city] < calendaringConfig.maxSessionsPerLocation - ) { - let regularCaseSliceSize; - let smallCaseSliceSize; - // One of these arrays will continue to decrease in size until it is smaller than the other, at which point prioritization below will flip. - // For now, we are okay with this -- TODO 10275 confirm - // schedule regular or small - if (regularCasesByCity[city]?.length > smallCasesByCity[city]?.length) { - scheduleRegularCases({ - calendaringConfig, - city, - prospectiveSessions, - regularCaseSliceSize, - regularCasesByCity, - }); - scheduleSmallCases({ - calendaringConfig, - city, - prospectiveSessions, - smallCaseSliceSize, - smallCasesByCity, - }); - } else { - scheduleSmallCases({ - calendaringConfig, - city, - prospectiveSessions, - smallCaseSliceSize, - smallCasesByCity, - }); - scheduleRegularCases({ - calendaringConfig, - city, - prospectiveSessions, - regularCaseSliceSize, - regularCasesByCity, - }); - } - - // Handle Hybrid Sessions - const remainingRegularCases = regularCasesByCity[city] || []; - const remainingSmallCases = smallCasesByCity[city] || []; - - if ( - remainingRegularCases?.length + remainingSmallCases.length >= - calendaringConfig.hybridCaseMinimumQuantity - ) { - // Since the min of reg cases is 40, and the min of small cases is 40, - // and the sum of these two values is below the hybrid case max of 100, - // we can safely assume that if the combination of remaining regular - // cases and remaining small cases is above the minimum of 50, we can - // assign all of those remaining cases to a hybrid session. - // - // This comment applies to the if statement's condition, as well as to - // the setting of regularCasesByCity[city] and smallCasesByCity[city] to - // empty arrays below. - regularCasesByCity[city] = []; - smallCasesByCity[city] = []; - - addProspectiveTrialSession({ - city, - prospectiveSessions, - sessionType: SESSION_TYPES.hybrid, - }); - } - } - } - - // TODO don't forget we need to deal with overrides. - - return prospectiveSessions; -} - -function scheduleRegularCases({ - calendaringConfig, - city, - prospectiveSessions, - regularCasesByCity, - regularCaseSliceSize, -}) { - while ( - (regularCasesByCity[city]?.length || 0) >= - calendaringConfig.regularCaseMinimumQuantity - ) { - regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; - - regularCasesByCity[city].splice(0, regularCaseSliceSize); - - addProspectiveTrialSession({ - city, - prospectiveSessions, - sessionType: SESSION_TYPES.regular, - }); - } -} - -function scheduleSmallCases({ - calendaringConfig, - city, - prospectiveSessions, - smallCasesByCity, - smallCaseSliceSize, -}) { - while ( - (smallCasesByCity[city].length || 0) >= - calendaringConfig.smallCaseMinimumQuantity - ) { - smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; - - smallCasesByCity[city].splice(0, smallCaseSliceSize); - - addProspectiveTrialSession({ - city, - prospectiveSessions, - sessionType: SESSION_TYPES.small, - }); - } -} - -function assignSessionsToWeeks({ - calendaringConfig, - endDate, - prospectiveSessions, - specialSessions, - startDate, -}: { - specialSessions: RawTrialSession[]; - prospectiveSessions: { - city: string; - sessionType: TrialSessionTypes; - }[]; - endDate: string; - startDate: string; - calendaringConfig: { - maxSessionsPerWeek: number; - maxSessionsPerLocation: number; - regularCaseMinimumQuantity: number; - regularCaseMaxQuantity: number; - smallCaseMinimumQuantity: number; - smallCaseMaxQuantity: number; - hybridCaseMaxQuantity: number; - hybridCaseMinimumQuantity: number; - }; -}): { - city: string; - sessionType: TrialSessionTypes; - weekOf: string; -}[] { - // -- Prioritize overridden and special sessions that have already been scheduled - // -- Max 1 per location per week. - // -- Max x per week across all locations - - const scheduledSessions: { - city: string; - sessionType: TrialSessionTypes; - weekOf: string; - }[] = []; - - const potentialTrialLocations: Set = new Set(); - prospectiveSessions.forEach(prospectiveSession => { - potentialTrialLocations.add(prospectiveSession.city); - }); - - // Get array of weeks in range to loop through - const weeksToLoop = getWeeksInRange({ endDate, startDate }); - - for (const currentWeek of weeksToLoop) { - const weekOfString = currentWeek; - - if (!sessionCountPerWeek[weekOfString]) { - sessionCountPerWeek[weekOfString] = 0; - } - - if ( - sessionCountPerWeek[weekOfString] >= calendaringConfig.maxSessionsPerWeek - ) { - continue; - } - - if (!sessionScheduledPerCityPerWeek[weekOfString]) { - sessionScheduledPerCityPerWeek[weekOfString] = new Set(); - } - - const specialSessionsForWeek = specialSessions.filter( - s => getMondayOfWeek(s.startDate) === weekOfString, - ); - - specialSessionsForWeek.forEach(session => { - addScheduledTrialSession({ - city: session.trialLocation, - scheduledSessions, - sessionType: SESSION_TYPES.special, - weekOfString, - }); - }); - - for (const prospectiveSession of prospectiveSessions) { - // do the thing - if ( - sessionScheduledPerCityPerWeek[weekOfString].has( - prospectiveSession.city, - ) - ) { - continue; // Skip this city if a session is already scheduled for this week - } - - if ( - sessionCountPerCity[prospectiveSession.city] >= - calendaringConfig.maxSessionsPerLocation - ) { - continue; - } - - addScheduledTrialSession({ - ...prospectiveSession, - scheduledSessions, - weekOfString, - }); - } - } - - return scheduledSessions; -} diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts new file mode 100644 index 00000000000..e49ef2501fc --- /dev/null +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts @@ -0,0 +1,139 @@ +import { + FORMATS, + createDateAtStartOfWeekEST, + getWeeksInRange, +} from '@shared/business/utilities/DateHandler'; +import { RawTrialSession } from '@shared/business/entities/trialSessions/TrialSession'; +import { + SESSION_TYPES, + TrialSessionTypes, +} from '@shared/business/entities/EntityConstants'; + +const sessionCountPerWeek: Record = {}; // weekOf -> session count +const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities + +export const assignSessionsToWeeks = ({ + calendaringConfig, + endDate, + prospectiveSessionsByCity, + specialSessions, + startDate, +}: { + specialSessions: RawTrialSession[]; + prospectiveSessionsByCity: Record< + string, + { + city: string; + sessionType: TrialSessionTypes; + }[] + >; + endDate: string; + startDate: string; + calendaringConfig: { + maxSessionsPerWeek: number; + maxSessionsPerLocation: number; + regularCaseMinimumQuantity: number; + regularCaseMaxQuantity: number; + smallCaseMinimumQuantity: number; + smallCaseMaxQuantity: number; + hybridCaseMaxQuantity: number; + hybridCaseMinimumQuantity: number; + }; +}): { + city: string; + sessionType: TrialSessionTypes; + weekOf: string; +}[] => { + // -- Prioritize overridden and special sessions that have already been scheduled + // -- Max 1 per location per week. + // -- Max x per week across all locations + + const scheduledSessions: { + city: string; + sessionType: TrialSessionTypes; + weekOf: string; + }[] = []; + // Get array of weeks in range to loop through + const weeksToLoop = getWeeksInRange({ endDate, startDate }); + + for (const currentWeek of weeksToLoop) { + const weekOfString = currentWeek; + + if (!sessionCountPerWeek[weekOfString]) { + sessionCountPerWeek[weekOfString] = 0; + } + + if ( + sessionCountPerWeek[weekOfString] >= calendaringConfig.maxSessionsPerWeek + ) { + continue; + } + + if (!sessionScheduledPerCityPerWeek[weekOfString]) { + sessionScheduledPerCityPerWeek[weekOfString] = new Set(); + } + + const specialSessionsForWeek = specialSessions.filter( + s => getMondayOfWeek(s.startDate) === weekOfString, + ); + + specialSessionsForWeek.forEach(session => { + addScheduledTrialSession({ + city: session.trialLocation, + scheduledSessions, + sessionType: SESSION_TYPES.special, + weekOfString, + }); + }); + + for (const city in prospectiveSessionsByCity) { + // This is a redundant checks, as we expect the length of the array to have already been trimmed to at most the max + // before entering this function. + if ( + prospectiveSessionsByCity[city].length >= + calendaringConfig.maxSessionsPerLocation + ) { + continue; + } + + for (const prospectiveSession of prospectiveSessionsByCity[city]) { + // do the thing + if ( + sessionScheduledPerCityPerWeek[weekOfString].has( + prospectiveSession.city, + ) + ) { + continue; // Skip this city if a session is already scheduled for this week + } + + addScheduledTrialSession({ + ...prospectiveSession, + scheduledSessions, + weekOfString, + }); + } + } + } + + return scheduledSessions; +}; + +function addScheduledTrialSession({ + city, + scheduledSessions, + sessionType, + weekOfString, +}) { + scheduledSessions.push({ + city, + sessionType, + weekOf: weekOfString, + }); + sessionCountPerWeek[weekOfString]++; + sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week +} + +// Helper function to get the Monday of the week for a given date +function getMondayOfWeek(date: string): string { + return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week +} diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts new file mode 100644 index 00000000000..b40d635ec93 --- /dev/null +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts @@ -0,0 +1,163 @@ +import { MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING } from '../../../../../../shared/src/test/mockCase'; +import { + PROCEDURE_TYPES_MAP, + SESSION_TYPES, + TRIAL_CITY_STRINGS, +} from '@shared/business/entities/EntityConstants'; +import { createProspectiveTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions'; +// import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; + +// const mockSpecialSessions = []; +const defaultMockCalendaringConfig = { + hybridCaseMaxQuantity: 10, + hybridCaseMinimumQuantity: 5, + maxSessionsPerLocation: 5, // Note that we may need to rethink this and maxSessionsPerWeek for testing purposes + maxSessionsPerWeek: 6, + regularCaseMaxQuantity: 10, + regularCaseMinimumQuantity: 4, + smallCaseMaxQuantity: 13, + smallCaseMinimumQuantity: 4, +}; + +describe('createProspectiveTrialSessions', () => { + it( + 'should not schedule more than the max number of sessions for a given city' + + 'when passed more regular cases than maxSessionsPerLocation * regularCaseMaxQuantity', + () => { + const totalNumberOfMockCases = + defaultMockCalendaringConfig.maxSessionsPerLocation * + defaultMockCalendaringConfig.regularCaseMaxQuantity + + defaultMockCalendaringConfig.regularCaseMaxQuantity; + + const mockCases: RawCase[] = []; + for (let i = 0; i < totalNumberOfMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-24`, + preferredTrialCity: TRIAL_CITY_STRINGS[0], + procedureType: PROCEDURE_TYPES_MAP.regular, + }); + } + + const result = createProspectiveTrialSessions({ + calendaringConfig: defaultMockCalendaringConfig, + cases: mockCases, + }); + + expect(result[TRIAL_CITY_STRINGS[0]].length).toEqual( + defaultMockCalendaringConfig.maxSessionsPerLocation, + ); + }, + ); + + it( + 'should appropriately divide cases into regular, small, and hybrid sessions, and prioritizes small cases' + + 'when all the cases are in one location and there are more small cases than regular', + () => { + const totalNumberOfSmallMockCases = + defaultMockCalendaringConfig.smallCaseMaxQuantity + + defaultMockCalendaringConfig.hybridCaseMinimumQuantity / 2; + const totalNumberOfRegularMockCases = + defaultMockCalendaringConfig.regularCaseMaxQuantity + + defaultMockCalendaringConfig.hybridCaseMinimumQuantity / 2; + + const mockCases: RawCase[] = []; + for (let i = 0; i < totalNumberOfRegularMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-24`, + preferredTrialCity: TRIAL_CITY_STRINGS[0], + procedureType: PROCEDURE_TYPES_MAP.regular, + }); + } + + for (let i = 0; i < totalNumberOfSmallMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-23`, + preferredTrialCity: TRIAL_CITY_STRINGS[0], + procedureType: PROCEDURE_TYPES_MAP.small, + }); + } + + const result = createProspectiveTrialSessions({ + calendaringConfig: defaultMockCalendaringConfig, + cases: mockCases, + }); + + expect(result[TRIAL_CITY_STRINGS[0]][0].sessionType).toEqual( + SESSION_TYPES.small, + ); + expect(result[TRIAL_CITY_STRINGS[0]][1].sessionType).toEqual( + SESSION_TYPES.regular, + ); + expect(result[TRIAL_CITY_STRINGS[0]][2].sessionType).toEqual( + SESSION_TYPES.hybrid, + ); + }, + ); + + it( + 'should appropriately divide cases into regular, small, and hybrid sessions, and prioritizes regular cases' + + 'when all the cases are in one location and there are fewer small cases than regular', + () => { + const mockCalendaringConfig = { + hybridCaseMaxQuantity: 100, + hybridCaseMinimumQuantity: 50, + maxSessionsPerLocation: 50, // Note that we may need to rethink this and maxSessionsPerWeek for testing purposes + maxSessionsPerWeek: 60, + regularCaseMaxQuantity: 100, + regularCaseMinimumQuantity: 40, + smallCaseMaxQuantity: 125, + smallCaseMinimumQuantity: 40, + }; + + const totalNumberOfSmallMockCases = + mockCalendaringConfig.smallCaseMaxQuantity + 11; + const totalNumberOfRegularMockCases = + mockCalendaringConfig.regularCaseMaxQuantity + 39; + + console.log( + 'totalNumberOfRegularMockCases', + totalNumberOfRegularMockCases, + ); + console.log('totalNumberOfSmallMockCases', totalNumberOfSmallMockCases); + + const mockCases: RawCase[] = []; + for (let i = 0; i < totalNumberOfRegularMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-24`, + preferredTrialCity: TRIAL_CITY_STRINGS[0], + procedureType: PROCEDURE_TYPES_MAP.regular, + }); + } + + for (let i = 0; i < totalNumberOfSmallMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-23`, + preferredTrialCity: TRIAL_CITY_STRINGS[0], + procedureType: PROCEDURE_TYPES_MAP.small, + }); + } + + const result = createProspectiveTrialSessions({ + calendaringConfig: mockCalendaringConfig, + cases: mockCases, + }); + + console.log('results', result); + + expect(result[TRIAL_CITY_STRINGS[0]][0].sessionType).toEqual( + SESSION_TYPES.regular, + ); + expect(result[TRIAL_CITY_STRINGS[0]][1].sessionType).toEqual( + SESSION_TYPES.small, + ); + expect(result[TRIAL_CITY_STRINGS[0]][2].sessionType).toEqual( + SESSION_TYPES.hybrid, + ); + }, + ); +}); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts new file mode 100644 index 00000000000..7869746bb8b --- /dev/null +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts @@ -0,0 +1,198 @@ +import { + PROCEDURE_TYPES_MAP, + SESSION_TYPES, + TrialSessionTypes, +} from '@shared/business/entities/EntityConstants'; + +export type EligibleCase = Pick< + RawCase, + 'preferredTrialCity' | 'procedureType' +>; + +export const createProspectiveTrialSessions = ({ + calendaringConfig, + cases, +}: { + cases: EligibleCase[]; + calendaringConfig: { + maxSessionsPerWeek: number; + maxSessionsPerLocation: number; + regularCaseMinimumQuantity: number; + regularCaseMaxQuantity: number; + smallCaseMinimumQuantity: number; + smallCaseMaxQuantity: number; + hybridCaseMaxQuantity: number; + hybridCaseMinimumQuantity: number; + }; +}): Record< + string, + { + city: string; + sessionType: TrialSessionTypes; + }[] +> => { + const potentialTrialLocations: Record< + string, + { + city: string; + sessionType: TrialSessionTypes; + }[] + > = {}; + + const regularCasesByCity = cases + .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.regular) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations[currentCase.preferredTrialCity!] = []; + acc[currentCase.preferredTrialCity!].push(currentCase); + + return acc; + }, {}); + + const smallCasesByCity = cases + .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.small) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + potentialTrialLocations[currentCase.preferredTrialCity!] = []; + acc[currentCase.preferredTrialCity!].push(currentCase); + + return acc; + }, {}); + + for (const city in potentialTrialLocations) { + let regularCaseSliceSize; + let smallCaseSliceSize; + // One of these arrays will continue to decrease in size until it is smaller than the other, at which point prioritization below will flip. + // For now, we are okay with this -- TODO 10275 confirm + // schedule regular or small + if (regularCasesByCity[city]?.length >= smallCasesByCity[city]?.length) { + scheduleRegularCases({ + calendaringConfig, + city, + potentialTrialLocations, + regularCaseSliceSize, + regularCasesByCity, + }); + scheduleSmallCases({ + calendaringConfig, + city, + potentialTrialLocations, + smallCaseSliceSize, + smallCasesByCity, + }); + } else { + scheduleSmallCases({ + calendaringConfig, + city, + potentialTrialLocations, + smallCaseSliceSize, + smallCasesByCity, + }); + scheduleRegularCases({ + calendaringConfig, + city, + potentialTrialLocations, + regularCaseSliceSize, + regularCasesByCity, + }); + } + + // Handle Hybrid Sessions + const remainingRegularCases = regularCasesByCity[city] || []; + const remainingSmallCases = smallCasesByCity[city] || []; + + if ( + remainingRegularCases?.length + remainingSmallCases.length >= + calendaringConfig.hybridCaseMinimumQuantity + ) { + // Since the min of reg cases is 40, and the min of small cases is 40, + // and the sum of these two values is below the hybrid case max of 100, + // we can safely assume that if the combination of remaining regular + // cases and remaining small cases is above the minimum of 50, we can + // assign all of those remaining cases to a hybrid session. + // + // This comment applies to the if statement's condition, as well as to + // the setting of regularCasesByCity[city] and smallCasesByCity[city] to + // empty arrays below. + regularCasesByCity[city] = []; + smallCasesByCity[city] = []; + + addProspectiveTrialSession({ + city, + potentialTrialLocations, + sessionType: SESSION_TYPES.hybrid, + }); + } + } + + // TODO don't forget we need to deal with overrides. + Object.keys(potentialTrialLocations).forEach(city => { + potentialTrialLocations[city] = potentialTrialLocations[city].splice( + 0, + calendaringConfig.maxSessionsPerLocation, + ); + }); + + return potentialTrialLocations; +}; + +function addProspectiveTrialSession({ + city, + potentialTrialLocations, + sessionType, +}) { + potentialTrialLocations[city].push({ + city, + sessionType, + }); +} + +function scheduleRegularCases({ + calendaringConfig, + city, + potentialTrialLocations, + regularCasesByCity, + regularCaseSliceSize, +}) { + while ( + (regularCasesByCity[city]?.length || 0) >= + calendaringConfig.regularCaseMinimumQuantity + ) { + regularCaseSliceSize = calendaringConfig.regularCaseMaxQuantity; + + regularCasesByCity[city].splice(0, regularCaseSliceSize); + + addProspectiveTrialSession({ + city, + potentialTrialLocations, + sessionType: SESSION_TYPES.regular, + }); + } +} + +function scheduleSmallCases({ + calendaringConfig, + city, + potentialTrialLocations, + smallCasesByCity, + smallCaseSliceSize, +}) { + while ( + (smallCasesByCity[city]?.length || 0) >= + calendaringConfig.smallCaseMinimumQuantity + ) { + smallCaseSliceSize = calendaringConfig.smallCaseMaxQuantity; + + smallCasesByCity[city].splice(0, smallCaseSliceSize); + + addProspectiveTrialSession({ + city, + potentialTrialLocations, + sessionType: SESSION_TYPES.small, + }); + } +} From 29f2d7502c04d0b268c8014cf3c76419f109bdf8 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:58:17 -0500 Subject: [PATCH 022/208] 10275 WIP: skeleton for assignSessionsToWeeks test --- .../assignSessionsToWeeks.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts new file mode 100644 index 00000000000..7d06cf948a7 --- /dev/null +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts @@ -0,0 +1,51 @@ +import { + PROCEDURE_TYPES_MAP, + SESSION_TYPES, + TRIAL_CITY_STRINGS, + TrialSessionTypes, +} from '@shared/business/entities/EntityConstants'; +import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; +// import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; + +// const mockSpecialSessions = []; +const defaultMockCalendaringConfig = { + hybridCaseMaxQuantity: 10, + hybridCaseMinimumQuantity: 5, + maxSessionsPerLocation: 5, // Note that we may need to rethink this and maxSessionsPerWeek for testing purposes + maxSessionsPerWeek: 6, + regularCaseMaxQuantity: 10, + regularCaseMinimumQuantity: 4, + smallCaseMaxQuantity: 13, + smallCaseMinimumQuantity: 4, +}; + +const mockEndDate = '2019-09-22T04:00:00.000Z'; +const mockStartDate = '2019-08-22T04:00:00.000Z'; + +describe('assignSessionsToWeeks', () => { + it( + 'should not schedule more than the max number of sessions for a given city' + + 'when passed more regular cases than maxSessionsPerLocation * regularCaseMaxQuantity', + () => { + const mockSessions: Record< + string, + { + city: string; + sessionType: TrialSessionTypes; + }[] + >; + + for (let i = 0; i < 10; ++i) { + const city = i; + mockSessions[city] = { + city: `${i}`, + sessionType: SESSION_TYPES.regular, + }; + } + + const result = assignSessionsToWeeks({}); + + expect(result).toEqual({}); + }, + ); +}); From 41507efb0f86c0b0a9cc5e15752b65b8fbcc6ed3 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 13 Sep 2024 12:04:45 -0400 Subject: [PATCH 023/208] 10275: factor out getCasesByCity function and move population of potentialTrialLocations outside of reducer callback --- .../createProspectiveTrialSessions.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts index 7869746bb8b..6a1e6fa62e6 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts @@ -39,29 +39,15 @@ export const createProspectiveTrialSessions = ({ }[] > = {}; - const regularCasesByCity = cases - .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.regular) - .reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations[currentCase.preferredTrialCity!] = []; - acc[currentCase.preferredTrialCity!].push(currentCase); - - return acc; - }, {}); - - const smallCasesByCity = cases - .filter(c => c.procedureType === PROCEDURE_TYPES_MAP.small) - .reduce((acc, currentCase) => { - if (!acc[currentCase.preferredTrialCity!]) { - acc[currentCase.preferredTrialCity!] = []; - } - potentialTrialLocations[currentCase.preferredTrialCity!] = []; - acc[currentCase.preferredTrialCity!].push(currentCase); + const regularCasesByCity = getCasesByCity(cases, PROCEDURE_TYPES_MAP.regular); + const smallCasesByCity = getCasesByCity(cases, PROCEDURE_TYPES_MAP.small); - return acc; - }, {}); + Object.keys(regularCasesByCity).forEach(city => { + potentialTrialLocations[city] = []; + }); + Object.keys(smallCasesByCity).forEach(city => { + potentialTrialLocations[city] = []; + }); for (const city in potentialTrialLocations) { let regularCaseSliceSize; @@ -140,6 +126,18 @@ export const createProspectiveTrialSessions = ({ return potentialTrialLocations; }; +function getCasesByCity(cases, type) { + return cases + .filter(c => c.procedureType === type) + .reduce((acc, currentCase) => { + if (!acc[currentCase.preferredTrialCity!]) { + acc[currentCase.preferredTrialCity!] = []; + } + acc[currentCase.preferredTrialCity!].push(currentCase); + return acc; + }, {}); +} + function addProspectiveTrialSession({ city, potentialTrialLocations, From 4dda8674dd0a79b7638ad170c75a8cace8c41973 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:00:30 -0500 Subject: [PATCH 024/208] 10275 WIP: add test, remove superfluous pass-through function --- .../scheduleTrialSessions.test.ts | 94 ------------------- .../trialSessions/scheduleTrialSessions.ts | 90 ------------------ .../assignSessionsToWeeks.test.ts | 70 +++++++++----- .../assignSessionsToWeeks.ts | 20 ++-- ...SuggestedTrialSessionCalendarInteractor.ts | 58 +++++++++++- 5 files changed, 112 insertions(+), 220 deletions(-) delete mode 100644 web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts delete mode 100644 web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts deleted file mode 100644 index 852e24996ed..00000000000 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING } from '../../../../../shared/src/test/mockCase'; -import { - PROCEDURE_TYPES_MAP, - TRIAL_CITY_STRINGS, -} from '@shared/business/entities/EntityConstants'; -// import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; -import { scheduleTrialSessions } from './scheduleTrialSessions'; -import mockCases from '../../../../../ourData.json'; - -// const mockSpecialSessions = []; -const mockCalendaringConfig = { - hybridCaseMaxQuantity: 10, - hybridCaseMinimumQuantity: 5, - maxSessionsPerLocation: 5, // Note that we may need to rethink this and maxSessionsPerWeek for testing purposes - maxSessionsPerWeek: 6, - regularCaseMaxQuantity: 10, - regularCaseMinimumQuantity: 4, - smallCaseMaxQuantity: 13, - smallCaseMinimumQuantity: 4, -}; -const mockEndDate = '2019-09-22T04:00:00.000Z'; -const mockStartDate = '2019-08-22T04:00:00.000Z'; - -describe('scheduleTrialSessions', () => { - // it('happy path', () => { - // let params = { - // calendaringConfig: mockCalendaringConfig, - // cases: mockCases, - // endDate: mockEndDate, - // specialSessions: [], - // startDate: mockStartDate, - // }; - - // let result = scheduleTrialSessions(params); - - // // date range - trial sessions should only be scheduled within the provided date range - // // e.g. given a 5 week date range, 6 max per week, means a total of 30 sessions per run (given current config/dates) - // // We can only schedule 1 session per location per week - // // This means that max sessions per run is 5 (maxSessionPerLocation) * number of unique preferred trial cities in test data. - // // e.g. this maxes us at 10 sessions and 2 per week. - // // We need at least the mi - // }); - - it('should not schedule more than the max number of sessions for a given week when passed more regular cases than maxSessionsPerWeek * regularCaseMaxQuantity', () => { - // for (let i = 0; i < totalNumberOfMockCases; ++i) { - // mockCases.push({ - // ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, - // docketNumber: `10${i}-24`, - // preferredTrialCity: TRIAL_CITY_STRINGS[0], - // procedureType: PROCEDURE_TYPES_MAP.regular, - // }); - // } - - const totalNumberOfMockCases = - mockCalendaringConfig.maxSessionsPerWeek * - mockCalendaringConfig.regularCaseMaxQuantity + - mockCalendaringConfig.regularCaseMinimumQuantity; - - let params = { - calendaringConfig: mockCalendaringConfig, - cases: mockCases, - endDate: mockEndDate, - specialSessions: [], - startDate: mockStartDate, - }; - - let result = scheduleTrialSessions(params); - - const weekOfMap = result.reduce((acc, session) => { - acc[session.weekOf] = (acc[session.weekOf] || 0) + 1; - return acc; - }, {}); - - Object.values(weekOfMap).forEach(count => { - expect(count).toBeLessThanOrEqual( - mockCalendaringConfig.maxSessionsPerWeek, - ); - }); - - console.log('weekOfMap', weekOfMap); - // expect(result).toEqual([ - // { - // city: 'City A', - // sessionType: SESSION_TYPES.regular, - // weekOf: '01/01/01', - // }, - // { - // city: 'City B', - // sessionType: SESSION_TYPES.small, - // weekOf: '01/07/01', - // }, - // ]); - }); -}); diff --git a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts deleted file mode 100644 index af7230e6f43..00000000000 --- a/web-api/src/business/useCaseHelper/trialSessions/scheduleTrialSessions.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - EligibleCase, - createProspectiveTrialSessions, -} from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions'; -import { - RawTrialSession, - TrialSession, -} from '@shared/business/entities/trialSessions/TrialSession'; -import { TrialSessionTypes } from '../../../../../shared/src/business/entities/EntityConstants'; -import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; - -// Maximum of 6 sessions per week overall. -const MAX_SESSIONS_PER_WEEK = 6; - -// Maximum of 5 total sessions per location during a term. -const MAX_SESSIONS_PER_LOCATION = 5; - -// Regular Cases: -// Minimum of 40 cases to create a session. -// Maximum of 100 cases per session. -const REGULAR_CASE_MINIMUM_QUANTITY = 40; -const REGULAR_CASE_MAX_QUANTITY = 100; - -// Small Cases: -// Minimum of 40 cases to create a session. -// Maximum of 125 cases per session. -const SMALL_CASE_MINIMUM_QUANTITY = 40; -const SMALL_CASE_MAX_QUANTITY = 125; - -// Hybrid Sessions: -// If neither Small nor Regular categories alone meet the session minimum, -// combine them to reach a minimum of 50 cases. -// Maximum of 100 cases per hybrid session. -const HYBRID_CASE_MINIMUM_QUANTITY = 50; -const HYBRID_CASE_MAX_QUANTITY = 100; - -// NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid - -export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; - -export function scheduleTrialSessions({ - calendaringConfig = { - hybridCaseMaxQuantity: HYBRID_CASE_MAX_QUANTITY, - hybridCaseMinimumQuantity: HYBRID_CASE_MINIMUM_QUANTITY, - maxSessionsPerLocation: MAX_SESSIONS_PER_LOCATION, - maxSessionsPerWeek: MAX_SESSIONS_PER_WEEK, - regularCaseMaxQuantity: REGULAR_CASE_MAX_QUANTITY, - regularCaseMinimumQuantity: REGULAR_CASE_MINIMUM_QUANTITY, - smallCaseMaxQuantity: SMALL_CASE_MAX_QUANTITY, - smallCaseMinimumQuantity: SMALL_CASE_MINIMUM_QUANTITY, - }, - cases, - endDate, - specialSessions, - startDate, -}: { - cases: EligibleCase[]; - specialSessions: RawTrialSession[]; - endDate: string; - startDate: string; - calendaringConfig: { - maxSessionsPerWeek: number; - maxSessionsPerLocation: number; - regularCaseMinimumQuantity: number; - regularCaseMaxQuantity: number; - smallCaseMinimumQuantity: number; - smallCaseMaxQuantity: number; - hybridCaseMaxQuantity: number; - hybridCaseMinimumQuantity: number; - }; -}): { - city: string; - sessionType: TrialSessionTypes; - weekOf: string; -}[] { - const prospectiveSessionsByCity = createProspectiveTrialSessions({ - calendaringConfig, - cases, - }); - - const scheduledTrialSessions = assignSessionsToWeeks({ - calendaringConfig, - endDate, - prospectiveSessionsByCity, - specialSessions, - startDate, - }); - - return scheduledTrialSessions; -} diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts index 7d06cf948a7..ea02ca01776 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts @@ -1,5 +1,4 @@ import { - PROCEDURE_TYPES_MAP, SESSION_TYPES, TRIAL_CITY_STRINGS, TrialSessionTypes, @@ -22,30 +21,49 @@ const defaultMockCalendaringConfig = { const mockEndDate = '2019-09-22T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; +function getMockTrialSessions() { + const mockSessions: Record< + string, + { + city: string; + sessionType: TrialSessionTypes; + }[] + > = {}; + + const numberOfSessions = defaultMockCalendaringConfig.maxSessionsPerWeek + 1; + + for (let i = 0; i < numberOfSessions; ++i) { + mockSessions[TRIAL_CITY_STRINGS[i]] = [ + { + city: `${TRIAL_CITY_STRINGS[i]}`, + sessionType: SESSION_TYPES.regular, + }, + ]; + } + + return mockSessions; +} + describe('assignSessionsToWeeks', () => { - it( - 'should not schedule more than the max number of sessions for a given city' + - 'when passed more regular cases than maxSessionsPerLocation * regularCaseMaxQuantity', - () => { - const mockSessions: Record< - string, - { - city: string; - sessionType: TrialSessionTypes; - }[] - >; - - for (let i = 0; i < 10; ++i) { - const city = i; - mockSessions[city] = { - city: `${i}`, - sessionType: SESSION_TYPES.regular, - }; - } - - const result = assignSessionsToWeeks({}); - - expect(result).toEqual({}); - }, - ); + it('should not schedule more than the maximum number of sessions for a given week', () => { + const mockSessions = getMockTrialSessions(); + + const result = assignSessionsToWeeks({ + calendaringConfig: defaultMockCalendaringConfig, + endDate: mockEndDate, + prospectiveSessionsByCity: mockSessions, + specialSessions: [], + startDate: mockStartDate, + }); + + //TODO: refactor this + const weekOfMap = result.reduce((acc, session) => { + acc[session.weekOf] = (acc[session.weekOf] || 0) + 1; + return acc; + }, {}); + + expect(Object.values(weekOfMap)[0]).toEqual( + defaultMockCalendaringConfig.maxSessionsPerWeek, + ); + }); }); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts index e49ef2501fc..25993d3defd 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts @@ -63,12 +63,6 @@ export const assignSessionsToWeeks = ({ sessionCountPerWeek[weekOfString] = 0; } - if ( - sessionCountPerWeek[weekOfString] >= calendaringConfig.maxSessionsPerWeek - ) { - continue; - } - if (!sessionScheduledPerCityPerWeek[weekOfString]) { sessionScheduledPerCityPerWeek[weekOfString] = new Set(); } @@ -96,14 +90,16 @@ export const assignSessionsToWeeks = ({ continue; } + // Just use the first session! for (const prospectiveSession of prospectiveSessionsByCity[city]) { - // do the thing if ( sessionScheduledPerCityPerWeek[weekOfString].has( prospectiveSession.city, - ) + ) || + sessionCountPerWeek[weekOfString] >= + calendaringConfig.maxSessionsPerWeek ) { - continue; // Skip this city if a session is already scheduled for this week + break; // Skip this city if a session is already scheduled for this week (must allow at most one in this loop) } addScheduledTrialSession({ @@ -111,6 +107,12 @@ export const assignSessionsToWeeks = ({ scheduledSessions, weekOfString, }); + + const index = + prospectiveSessionsByCity[city].indexOf(prospectiveSession); + if (index !== -1) { + prospectiveSessionsByCity[city].splice(index, 1); + } } } } diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index 897651768a6..c631470d08b 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -1,7 +1,39 @@ import { SESSION_TYPES } from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; +import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; +import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; +import { createProspectiveTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions'; import { scheduleTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/scheduleTrialSessions'; +// Maximum of 6 sessions per week overall. +const MAX_SESSIONS_PER_WEEK = 6; + +// Maximum of 5 total sessions per location during a term. +const MAX_SESSIONS_PER_LOCATION = 5; + +// Regular Cases: +// Minimum of 40 cases to create a session. +// Maximum of 100 cases per session. +const REGULAR_CASE_MINIMUM_QUANTITY = 40; +const REGULAR_CASE_MAX_QUANTITY = 100; + +// Small Cases: +// Minimum of 40 cases to create a session. +// Maximum of 125 cases per session. +const SMALL_CASE_MINIMUM_QUANTITY = 40; +const SMALL_CASE_MAX_QUANTITY = 125; + +// Hybrid Sessions: +// If neither Small nor Regular categories alone meet the session minimum, +// combine them to reach a minimum of 50 cases. +// Maximum of 100 cases per hybrid session. +const HYBRID_CASE_MINIMUM_QUANTITY = 50; +const HYBRID_CASE_MAX_QUANTITY = 100; + +// NOTE: will front-load term with trial sessions, and prioritize Regular > Small > Hybrid + +export type TrialSessionReadyForCalendaring = TrialSession & { weekOf: string }; + export const generateSuggestedTrialSessionCalendarInteractor = async ( applicationContext: ServerApplicationContext, { endDate, startDate }: { endDate: string; startDate: string }, @@ -15,10 +47,21 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( // Special sessions already created will be automatically included // If there has been no trial in the last two terms for a location, then add a session if there are any cases. (ignore the minimum rule) // + const calendaringConfig = { + hybridCaseMaxQuantity: HYBRID_CASE_MAX_QUANTITY, + hybridCaseMinimumQuantity: HYBRID_CASE_MINIMUM_QUANTITY, + maxSessionsPerLocation: MAX_SESSIONS_PER_LOCATION, + maxSessionsPerWeek: MAX_SESSIONS_PER_WEEK, + regularCaseMaxQuantity: REGULAR_CASE_MAX_QUANTITY, + regularCaseMinimumQuantity: REGULAR_CASE_MINIMUM_QUANTITY, + smallCaseMaxQuantity: SMALL_CASE_MAX_QUANTITY, + smallCaseMinimumQuantity: SMALL_CASE_MINIMUM_QUANTITY, + }; const cases = await applicationContext .getPersistenceGateway() .getReadyForTrialCases({ applicationContext }); + const sessions = await applicationContext .getPersistenceGateway() .getTrialSessions({ applicationContext }); @@ -27,5 +70,18 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( session => session.sessionType === SESSION_TYPES.special, ); - return scheduleTrialSessions({ cases, endDate, specialSessions, startDate }); + const prospectiveSessionsByCity = createProspectiveTrialSessions({ + calendaringConfig, + cases, + }); + + const scheduledTrialSessions = assignSessionsToWeeks({ + calendaringConfig, + endDate, + prospectiveSessionsByCity, + specialSessions, + startDate, + }); + + return scheduledTrialSessions; }; From 1cdf0b8e5f8be37605fdeeb0205dff813c20bc16 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 16 Sep 2024 15:54:39 -0400 Subject: [PATCH 025/208] 10275: WIP on assignSessionsToWeeks --- .../assignSessionsToWeeks.test.ts | 169 +++++++++++++++++- .../assignSessionsToWeeks.ts | 69 +++++-- 2 files changed, 220 insertions(+), 18 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts index ea02ca01776..a993b791282 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts @@ -1,9 +1,11 @@ +import { MOCK_TRIAL_REGULAR } from '@shared/test/mockTrial'; import { SESSION_TYPES, TRIAL_CITY_STRINGS, TrialSessionTypes, } from '@shared/business/entities/EntityConstants'; import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; +import { getUniqueId } from '@shared/sharedAppContext'; // import { applicationContext } from '../../../../../shared/src/business/test/createTestApplicationContext'; // const mockSpecialSessions = []; @@ -18,7 +20,7 @@ const defaultMockCalendaringConfig = { smallCaseMinimumQuantity: 4, }; -const mockEndDate = '2019-09-22T04:00:00.000Z'; +const mockEndDate = '2019-10-10T04:00:00.000Z'; const mockStartDate = '2019-08-22T04:00:00.000Z'; function getMockTrialSessions() { @@ -44,6 +46,30 @@ function getMockTrialSessions() { return mockSessions; } +function getMockTrialSessionsForSingleCity() { + const mockSessions: Record< + string, + { + city: string; + sessionType: TrialSessionTypes; + }[] + > = {}; + + const numberOfSessions = + defaultMockCalendaringConfig.maxSessionsPerLocation + 1; + + for (let i = 0; i < numberOfSessions; ++i) { + if (!mockSessions[TRIAL_CITY_STRINGS[0]]) { + mockSessions[TRIAL_CITY_STRINGS[0]] = []; + } + mockSessions[TRIAL_CITY_STRINGS[0]].push({ + city: `${TRIAL_CITY_STRINGS[0]}`, + sessionType: SESSION_TYPES.regular, + }); + } + return mockSessions; +} + describe('assignSessionsToWeeks', () => { it('should not schedule more than the maximum number of sessions for a given week', () => { const mockSessions = getMockTrialSessions(); @@ -66,4 +92,145 @@ describe('assignSessionsToWeeks', () => { defaultMockCalendaringConfig.maxSessionsPerWeek, ); }); + + it('should assign no more than the max number of sessions per location when passed more than the max for a given location', () => { + const mockSessions = getMockTrialSessionsForSingleCity(); + console.log('mockSessions', mockSessions); + const result = assignSessionsToWeeks({ + calendaringConfig: defaultMockCalendaringConfig, + endDate: mockEndDate, + prospectiveSessionsByCity: mockSessions, + specialSessions: [], + startDate: mockStartDate, + }); + + //TODO: refactor this + // const weekOfMap = result.reduce((acc, session) => { + // acc[session.weekOf] = (acc[session.weekOf] || 0) + 1; + // return acc; + // }, {}); + console.log('result', result); + console.log('typeof result', typeof result); + + expect(result.length).toEqual( + defaultMockCalendaringConfig.maxSessionsPerLocation, + ); + }); + + it.only('should prioritize special sessions over non-special sessions', () => { + const mockSpecialSessions = [ + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-22T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-29T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-09-05T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-09-12T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-09-19T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-09-26T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + ]; + + const mockSessions = getMockTrialSessionsForSingleCity(); + const result = assignSessionsToWeeks({ + calendaringConfig: defaultMockCalendaringConfig, + endDate: mockEndDate, + prospectiveSessionsByCity: mockSessions, + specialSessions: mockSpecialSessions, + startDate: mockStartDate, + }); + + expect(result.length).toEqual(mockSpecialSessions.length); + + // 5 special sessions at the same location + // 1 non-special session also at that same location + // in the end, we should have 5 special sessions scheduled for that location and that's it + }); + + it('should throw an error when passed multiple special sections in the same location for the same week', () => { + const mockSpecialSessions = [ + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-29T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-29T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-29T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-29T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + { + ...MOCK_TRIAL_REGULAR, + sessionType: SESSION_TYPES.special, + startDate: '2019-08-29T04:00:00.000Z', + trialLocation: TRIAL_CITY_STRINGS[0], + trialSessionId: getUniqueId(), + }, + ]; + + const mockSessions = getMockTrialSessionsForSingleCity(); + + expect(() => { + assignSessionsToWeeks({ + calendaringConfig: defaultMockCalendaringConfig, + endDate: mockEndDate, + prospectiveSessionsByCity: mockSessions, + specialSessions: mockSpecialSessions, + startDate: mockStartDate, + }); + }).toThrow(Error); + }); + // 5 special sessions, all at different locations, in the same week + // 2 non-special sessions, all at different locations, in the same week + // in the end, we should have 5 special and 1 non-special scheduled for that week }); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts index 25993d3defd..d1b6adf837f 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts @@ -9,9 +9,6 @@ import { TrialSessionTypes, } from '@shared/business/entities/EntityConstants'; -const sessionCountPerWeek: Record = {}; // weekOf -> session count -const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities - export const assignSessionsToWeeks = ({ calendaringConfig, endDate, @@ -44,6 +41,8 @@ export const assignSessionsToWeeks = ({ sessionType: TrialSessionTypes; weekOf: string; }[] => { + const sessionCountPerWeek: Record = {}; // weekOf -> session count + const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities // -- Prioritize overridden and special sessions that have already been scheduled // -- Max 1 per location per week. // -- Max x per week across all locations @@ -67,27 +66,64 @@ export const assignSessionsToWeeks = ({ sessionScheduledPerCityPerWeek[weekOfString] = new Set(); } - const specialSessionsForWeek = specialSessions.filter( - s => getMondayOfWeek(s.startDate) === weekOfString, + const specialSessionsForWeek = specialSessions.filter(s => { + return ( + createDateAtStartOfWeekEST(s.startDate, FORMATS.YYYYMMDD) === + weekOfString + ); + }); + + const specialSessionsByLocation = specialSessionsForWeek.reduce( + (acc, session) => { + if (!acc[session.trialLocation!]) { + acc[session.trialLocation!] = []; + } + acc[session.trialLocation!].push(session); + return acc; + }, + {}, ); + for (const location in specialSessionsByLocation) { + if (specialSessionsByLocation[location].length > 1) { + throw new Error( + 'There must only be one special trial session per location per week.', + ); + } + } + specialSessionsForWeek.forEach(session => { addScheduledTrialSession({ city: session.trialLocation, scheduledSessions, + sessionCountPerWeek, + sessionScheduledPerCityPerWeek, sessionType: SESSION_TYPES.special, weekOfString, }); }); for (const city in prospectiveSessionsByCity) { - // This is a redundant checks, as we expect the length of the array to have already been trimmed to at most the max - // before entering this function. - if ( - prospectiveSessionsByCity[city].length >= - calendaringConfig.maxSessionsPerLocation - ) { - continue; + // This is a redundant check, as we expect the length of the array to have + // already been trimmed to at most the max before entering this function. + // TODO lets reeval whether we need or how to do this check + // if ( + // prospectiveSessionsByCity[city].length < + // calendaringConfig.maxSessionsPerLocation + // ) { + // continue; + // } + + if (weeksToLoop.indexOf(currentWeek) === 0) { + // TODO currently, this will incorrectly ignore special sessions beyond the max for the location + // we need to figure out a way to fix this. + prospectiveSessionsByCity[city].unshift( + specialSessionsByLocation[city], + ); + // since we ignore things beyond the max, force prospective array to at most the max + prospectiveSessionsByCity[city] = prospectiveSessionsByCity[ + city + ].splice(0, calendaringConfig.maxSessionsPerLocation); } // Just use the first session! @@ -105,6 +141,8 @@ export const assignSessionsToWeeks = ({ addScheduledTrialSession({ ...prospectiveSession, scheduledSessions, + sessionCountPerWeek, + sessionScheduledPerCityPerWeek, weekOfString, }); @@ -123,6 +161,8 @@ export const assignSessionsToWeeks = ({ function addScheduledTrialSession({ city, scheduledSessions, + sessionCountPerWeek, + sessionScheduledPerCityPerWeek, sessionType, weekOfString, }) { @@ -134,8 +174,3 @@ function addScheduledTrialSession({ sessionCountPerWeek[weekOfString]++; sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week } - -// Helper function to get the Monday of the week for a given date -function getMondayOfWeek(date: string): string { - return createDateAtStartOfWeekEST(date, FORMATS.ISO); // Monday as the first day of the week -} From 9e1ecf06cd35460a396300a30334640f4a64d7fe Mon Sep 17 00:00:00 2001 From: Nate Elliott Date: Mon, 16 Sep 2024 16:45:37 -0500 Subject: [PATCH 026/208] 10275 Tests for special sesssion handling --- .../assignSessionsToWeeks.test.ts | 18 +----- .../assignSessionsToWeeks.ts | 57 ++++++++++++------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts index a993b791282..2ddbf5e4c34 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.test.ts @@ -95,7 +95,6 @@ describe('assignSessionsToWeeks', () => { it('should assign no more than the max number of sessions per location when passed more than the max for a given location', () => { const mockSessions = getMockTrialSessionsForSingleCity(); - console.log('mockSessions', mockSessions); const result = assignSessionsToWeeks({ calendaringConfig: defaultMockCalendaringConfig, endDate: mockEndDate, @@ -104,20 +103,12 @@ describe('assignSessionsToWeeks', () => { startDate: mockStartDate, }); - //TODO: refactor this - // const weekOfMap = result.reduce((acc, session) => { - // acc[session.weekOf] = (acc[session.weekOf] || 0) + 1; - // return acc; - // }, {}); - console.log('result', result); - console.log('typeof result', typeof result); - expect(result.length).toEqual( defaultMockCalendaringConfig.maxSessionsPerLocation, ); }); - it.only('should prioritize special sessions over non-special sessions', () => { + it('should prioritize special sessions over non-special sessions', () => { const mockSpecialSessions = [ { ...MOCK_TRIAL_REGULAR, @@ -154,13 +145,6 @@ describe('assignSessionsToWeeks', () => { trialLocation: TRIAL_CITY_STRINGS[0], trialSessionId: getUniqueId(), }, - { - ...MOCK_TRIAL_REGULAR, - sessionType: SESSION_TYPES.special, - startDate: '2019-09-26T04:00:00.000Z', - trialLocation: TRIAL_CITY_STRINGS[0], - trialSessionId: getUniqueId(), - }, ]; const mockSessions = getMockTrialSessionsForSingleCity(); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts index d1b6adf837f..fb9577ee7ea 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks.ts @@ -42,6 +42,7 @@ export const assignSessionsToWeeks = ({ weekOf: string; }[] => { const sessionCountPerWeek: Record = {}; // weekOf -> session count + const sessionCountPerCity: Record = {}; // trialLocation -> session count const sessionScheduledPerCityPerWeek: Record> = {}; // weekOf -> Set of cities // -- Prioritize overridden and special sessions that have already been scheduled // -- Max 1 per location per week. @@ -52,6 +53,27 @@ export const assignSessionsToWeeks = ({ sessionType: TrialSessionTypes; weekOf: string; }[] = []; + + // check special sessions + const specialSessionsByLocation = specialSessions.reduce((acc, session) => { + if (!acc[session.trialLocation!]) { + acc[session.trialLocation!] = []; + } + acc[session.trialLocation!].push(session); + return acc; + }, {}); + + for (const location in specialSessionsByLocation) { + if ( + specialSessionsByLocation[location].length > + calendaringConfig.maxSessionsPerLocation + ) { + throw new Error( + `Special session count exceeds the max sessions per location for ${location}`, + ); + } + } + // Get array of weeks in range to loop through const weeksToLoop = getWeeksInRange({ endDate, startDate }); @@ -73,7 +95,7 @@ export const assignSessionsToWeeks = ({ ); }); - const specialSessionsByLocation = specialSessionsForWeek.reduce( + const specialSessionsForWeekByLocation = specialSessionsForWeek.reduce( (acc, session) => { if (!acc[session.trialLocation!]) { acc[session.trialLocation!] = []; @@ -84,8 +106,8 @@ export const assignSessionsToWeeks = ({ {}, ); - for (const location in specialSessionsByLocation) { - if (specialSessionsByLocation[location].length > 1) { + for (const location in specialSessionsForWeekByLocation) { + if (specialSessionsForWeekByLocation[location].length > 1) { throw new Error( 'There must only be one special trial session per location per week.', ); @@ -96,6 +118,7 @@ export const assignSessionsToWeeks = ({ addScheduledTrialSession({ city: session.trialLocation, scheduledSessions, + sessionCountPerCity, sessionCountPerWeek, sessionScheduledPerCityPerWeek, sessionType: SESSION_TYPES.special, @@ -106,25 +129,11 @@ export const assignSessionsToWeeks = ({ for (const city in prospectiveSessionsByCity) { // This is a redundant check, as we expect the length of the array to have // already been trimmed to at most the max before entering this function. - // TODO lets reeval whether we need or how to do this check - // if ( - // prospectiveSessionsByCity[city].length < - // calendaringConfig.maxSessionsPerLocation - // ) { - // continue; - // } - - if (weeksToLoop.indexOf(currentWeek) === 0) { - // TODO currently, this will incorrectly ignore special sessions beyond the max for the location - // we need to figure out a way to fix this. - prospectiveSessionsByCity[city].unshift( - specialSessionsByLocation[city], - ); - // since we ignore things beyond the max, force prospective array to at most the max - prospectiveSessionsByCity[city] = prospectiveSessionsByCity[ - city - ].splice(0, calendaringConfig.maxSessionsPerLocation); - } + // since we ignore things beyond the max, force prospective array to at most the max + if (sessionCountPerCity[city] >= calendaringConfig.maxSessionsPerLocation) + continue; + + // Check if we're already at the max for this location // Just use the first session! for (const prospectiveSession of prospectiveSessionsByCity[city]) { @@ -141,6 +150,7 @@ export const assignSessionsToWeeks = ({ addScheduledTrialSession({ ...prospectiveSession, scheduledSessions, + sessionCountPerCity, sessionCountPerWeek, sessionScheduledPerCityPerWeek, weekOfString, @@ -161,16 +171,19 @@ export const assignSessionsToWeeks = ({ function addScheduledTrialSession({ city, scheduledSessions, + sessionCountPerCity, sessionCountPerWeek, sessionScheduledPerCityPerWeek, sessionType, weekOfString, }) { + if (!sessionCountPerCity[city]) sessionCountPerCity[city] = 0; scheduledSessions.push({ city, sessionType, weekOf: weekOfString, }); sessionCountPerWeek[weekOfString]++; + sessionCountPerCity[city]++; sessionScheduledPerCityPerWeek[weekOfString].add(city); // Mark this city as scheduled for the current week } From 1651fa010d1a6c2cd486b35deb06cb029c3507ce Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 18 Sep 2024 13:10:05 -0400 Subject: [PATCH 027/208] 10275: Remove TrialSession.isClosed property and add isClosed method. --- .../entities/trialSessions/TrialSession.ts | 6 ++++-- ...ateSuggestedTrialSessionCalendarInteractor.ts | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/shared/src/business/entities/trialSessions/TrialSession.ts b/shared/src/business/entities/trialSessions/TrialSession.ts index 570050287da..be6b9c26f9e 100644 --- a/shared/src/business/entities/trialSessions/TrialSession.ts +++ b/shared/src/business/entities/trialSessions/TrialSession.ts @@ -86,7 +86,6 @@ export class TrialSession extends JoiValidationEntity { public irsCalendarAdministrator?: string; public irsCalendarAdministratorInfo?: RawIrsCalendarAdministratorInfo; public isCalendared: boolean; - public isClosed?: boolean; public isStartDateWithinNOTTReminderRange?: boolean; public joinPhoneNumber?: string; public judge?: TJudge; @@ -159,7 +158,6 @@ export class TrialSession extends JoiValidationEntity { this.irsCalendarAdministrator = rawSession.irsCalendarAdministrator; this.irsCalendarAdministratorInfo = rawSession.irsCalendarAdministratorInfo; this.isCalendared = rawSession.isCalendared || false; - this.isClosed = rawSession.isClosed || false; this.joinPhoneNumber = rawSession.joinPhoneNumber; this.maxCases = rawSession.maxCases; this.meetingId = rawSession.meetingId; @@ -547,6 +545,10 @@ export class TrialSession extends JoiValidationEntity { addPaperServicePdf(fileId: string, title: string): void { this.paperServicePdfs.push({ fileId, title }); } + + isClosed(): boolean { + return this.sessionStatus === SESSION_STATUS_TYPES.closed; + } } export type RawTrialSession = ExcludeMethods; diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index c631470d08b..364d649ef09 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -1,9 +1,11 @@ -import { SESSION_TYPES } from '../../../../../shared/src/business/entities/EntityConstants'; +import { + SESSION_STATUS_TYPES, + SESSION_TYPES, +} from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; import { createProspectiveTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions'; -import { scheduleTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/scheduleTrialSessions'; // Maximum of 6 sessions per week overall. const MAX_SESSIONS_PER_WEEK = 6; @@ -66,9 +68,13 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( .getPersistenceGateway() .getTrialSessions({ applicationContext }); - const specialSessions = sessions.filter( - session => session.sessionType === SESSION_TYPES.special, - ); + const specialSessions = sessions.filter(session => { + return ( + session.sessionType === SESSION_TYPES.special && + session.isCalendared && + session.sessionStatus !== SESSION_STATUS_TYPES.closed + ); + }); const prospectiveSessionsByCity = createProspectiveTrialSessions({ calendaringConfig, From 315a6332f28a54bb34364be4ad02cb9b9fb1e70e Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 18 Sep 2024 13:48:15 -0400 Subject: [PATCH 028/208] 10275 Test for regular-at-small and fix bug in check --- .../src/business/entities/EntityConstants.ts | 4 ++ .../createProspectiveTrialSessions.test.ts | 53 ++++++++++++------- .../createProspectiveTrialSessions.ts | 9 ++++ 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/shared/src/business/entities/EntityConstants.ts b/shared/src/business/entities/EntityConstants.ts index b72fb1c20c3..9095316d7c9 100644 --- a/shared/src/business/entities/EntityConstants.ts +++ b/shared/src/business/entities/EntityConstants.ts @@ -1331,6 +1331,10 @@ export const TRIAL_CITY_STRINGS = SMALL_CITIES.map( trialLocation => `${trialLocation.city}, ${trialLocation.state}`, ); +export const REGULAR_TRIAL_CITY_STRINGS = COMMON_CITIES.map( + trialLocation => `${trialLocation.city}, ${trialLocation.state}`, +); + export const LEGACY_TRIAL_CITY_STRINGS = LEGACY_TRIAL_CITIES.map( trialLocation => `${trialLocation.city}, ${trialLocation.state}`, ); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts index b40d635ec93..5bd918bf110 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts @@ -18,6 +18,8 @@ const defaultMockCalendaringConfig = { smallCaseMaxQuantity: 13, smallCaseMinimumQuantity: 4, }; +const mockRegularCityString = TRIAL_CITY_STRINGS[TRIAL_CITY_STRINGS.length - 1]; +const mockSmallCityString = TRIAL_CITY_STRINGS[0]; describe('createProspectiveTrialSessions', () => { it( @@ -34,7 +36,7 @@ describe('createProspectiveTrialSessions', () => { mockCases.push({ ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber: `10${i}-24`, - preferredTrialCity: TRIAL_CITY_STRINGS[0], + preferredTrialCity: mockRegularCityString, procedureType: PROCEDURE_TYPES_MAP.regular, }); } @@ -44,7 +46,7 @@ describe('createProspectiveTrialSessions', () => { cases: mockCases, }); - expect(result[TRIAL_CITY_STRINGS[0]].length).toEqual( + expect(result[mockRegularCityString].length).toEqual( defaultMockCalendaringConfig.maxSessionsPerLocation, ); }, @@ -66,7 +68,7 @@ describe('createProspectiveTrialSessions', () => { mockCases.push({ ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber: `10${i}-24`, - preferredTrialCity: TRIAL_CITY_STRINGS[0], + preferredTrialCity: mockRegularCityString, procedureType: PROCEDURE_TYPES_MAP.regular, }); } @@ -75,7 +77,7 @@ describe('createProspectiveTrialSessions', () => { mockCases.push({ ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber: `10${i}-23`, - preferredTrialCity: TRIAL_CITY_STRINGS[0], + preferredTrialCity: mockRegularCityString, procedureType: PROCEDURE_TYPES_MAP.small, }); } @@ -85,13 +87,13 @@ describe('createProspectiveTrialSessions', () => { cases: mockCases, }); - expect(result[TRIAL_CITY_STRINGS[0]][0].sessionType).toEqual( + expect(result[mockRegularCityString][0].sessionType).toEqual( SESSION_TYPES.small, ); - expect(result[TRIAL_CITY_STRINGS[0]][1].sessionType).toEqual( + expect(result[mockRegularCityString][1].sessionType).toEqual( SESSION_TYPES.regular, ); - expect(result[TRIAL_CITY_STRINGS[0]][2].sessionType).toEqual( + expect(result[mockRegularCityString][2].sessionType).toEqual( SESSION_TYPES.hybrid, ); }, @@ -117,18 +119,12 @@ describe('createProspectiveTrialSessions', () => { const totalNumberOfRegularMockCases = mockCalendaringConfig.regularCaseMaxQuantity + 39; - console.log( - 'totalNumberOfRegularMockCases', - totalNumberOfRegularMockCases, - ); - console.log('totalNumberOfSmallMockCases', totalNumberOfSmallMockCases); - const mockCases: RawCase[] = []; for (let i = 0; i < totalNumberOfRegularMockCases; ++i) { mockCases.push({ ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber: `10${i}-24`, - preferredTrialCity: TRIAL_CITY_STRINGS[0], + preferredTrialCity: mockRegularCityString, procedureType: PROCEDURE_TYPES_MAP.regular, }); } @@ -137,7 +133,7 @@ describe('createProspectiveTrialSessions', () => { mockCases.push({ ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, docketNumber: `10${i}-23`, - preferredTrialCity: TRIAL_CITY_STRINGS[0], + preferredTrialCity: mockRegularCityString, procedureType: PROCEDURE_TYPES_MAP.small, }); } @@ -147,17 +143,34 @@ describe('createProspectiveTrialSessions', () => { cases: mockCases, }); - console.log('results', result); - - expect(result[TRIAL_CITY_STRINGS[0]][0].sessionType).toEqual( + expect(result[mockRegularCityString][0].sessionType).toEqual( SESSION_TYPES.regular, ); - expect(result[TRIAL_CITY_STRINGS[0]][1].sessionType).toEqual( + expect(result[mockRegularCityString][1].sessionType).toEqual( SESSION_TYPES.small, ); - expect(result[TRIAL_CITY_STRINGS[0]][2].sessionType).toEqual( + expect(result[mockRegularCityString][2].sessionType).toEqual( SESSION_TYPES.hybrid, ); }, ); + + it('should throw an error when attempting to schedule a regular case at a small city', () => { + const mockCases: RawCase[] = []; + + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: '101-24', + // This is a small-only city + preferredTrialCity: mockSmallCityString, + procedureType: PROCEDURE_TYPES_MAP.regular, + }); + + expect(() => { + createProspectiveTrialSessions({ + calendaringConfig: defaultMockCalendaringConfig, + cases: mockCases, + }); + }).toThrow(Error); + }); }); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts index 6a1e6fa62e6..01cd0112c0b 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts @@ -1,5 +1,6 @@ import { PROCEDURE_TYPES_MAP, + REGULAR_TRIAL_CITY_STRINGS, SESSION_TYPES, TrialSessionTypes, } from '@shared/business/entities/EntityConstants'; @@ -130,6 +131,14 @@ function getCasesByCity(cases, type) { return cases .filter(c => c.procedureType === type) .reduce((acc, currentCase) => { + if ( + type === SESSION_TYPES.regular && + !REGULAR_TRIAL_CITY_STRINGS.includes(currentCase.preferredTrialCity) + ) { + throw new Error( + `Case ${currentCase.docketNumber} cannot be scheduled in ${currentCase.preferredTrialCity} because the session type does not match the trial city`, + ); + } if (!acc[currentCase.preferredTrialCity!]) { acc[currentCase.preferredTrialCity!] = []; } From 6a07eb63845cf6e36d00764d73cc615b4a653145 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 18 Sep 2024 15:09:31 -0400 Subject: [PATCH 029/208] 10275: work on logic to determine which cities have not had a trial session in the past two terms --- .../src/business/entities/EntityConstants.ts | 7 +++ ...SuggestedTrialSessionCalendarInteractor.ts | 53 +++++++++++++++++++ web-api/src/getUtilities.ts | 2 + .../computeTrialSessionFormDataAction.ts | 16 ++---- 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/shared/src/business/entities/EntityConstants.ts b/shared/src/business/entities/EntityConstants.ts index 9095316d7c9..1209176715a 100644 --- a/shared/src/business/entities/EntityConstants.ts +++ b/shared/src/business/entities/EntityConstants.ts @@ -1341,6 +1341,13 @@ export const LEGACY_TRIAL_CITY_STRINGS = LEGACY_TRIAL_CITIES.map( export const SESSION_TERMS = ['Winter', 'Fall', 'Spring', 'Summer']; +export const SESSION_TERMS_BY_MONTH = { + fall: [9, 10, 11, 12], + spring: [4, 5, 6], + summer: [7, 8], + winter: [1, 2, 3], +}; + export const SESSION_TYPES = { regular: 'Regular', small: 'Small', diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index 364d649ef09..93c3d7cb874 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -1,11 +1,14 @@ import { SESSION_STATUS_TYPES, + SESSION_TERMS_BY_MONTH, SESSION_TYPES, + TRIAL_CITY_STRINGS, } from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; import { assignSessionsToWeeks } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/assignSessionsToWeeks'; import { createProspectiveTrialSessions } from '@web-api/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions'; +import { getUtilities } from '@web-api/getUtilities'; // Maximum of 6 sessions per week overall. const MAX_SESSIONS_PER_WEEK = 6; @@ -68,6 +71,9 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( .getPersistenceGateway() .getTrialSessions({ applicationContext }); + // Note (10275): storing trial session data differently would make for a more + // efficient process of determining which sessions are special, calendared, + // and not closed. const specialSessions = sessions.filter(session => { return ( session.sessionType === SESSION_TYPES.special && @@ -76,6 +82,22 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( ); }); + // Note (10275): storing trial session data differently would make for a more + // efficient process of determining which cities were not visited within the + // past two terms. + const previousTwoTerms = getPreviousTwoTerms(startDate); + + const citiesFromLastTwoTerms = sessions.map(session => { + const termString = `${session.term}, ${session.termYear}`; + if (previousTwoTerms.includes(termString)) { + return session.trialLocation; + } + }); + + const citiesThatHaveNotBeenVisitedInTwoTerms = TRIAL_CITY_STRINGS.filter( + city => !citiesFromLastTwoTerms.includes(city), + ); + const prospectiveSessionsByCity = createProspectiveTrialSessions({ calendaringConfig, cases, @@ -91,3 +113,34 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( return scheduledTrialSessions; }; + +const getPreviousTwoTerms = (startDate: string) => { + //TODO: refactor this, maybe? + + const deconstructedDate = getUtilities().deconstructDate(startDate); + + const currentTerm = getTermByMonth(deconstructedDate.month); + + const terms = [ + `fall ${+deconstructedDate.year - 1}`, + `winter ${deconstructedDate.year}`, + `spring ${deconstructedDate.year}`, + `fall ${deconstructedDate.year}`, + ]; + const termsReversed = terms.reverse(); + const termI = terms.findIndex( + t => `${currentTerm} ${deconstructedDate.year}` === t, + ); + const [term1, year1] = termsReversed[termI + 1].split(' '); + const [term2, year2] = termsReversed[termI + 2].split(' '); + + return [`${term1}, ${year1}`, `${term2}, ${year2}`]; +}; + +function getTermByMonth(currentMonth: string) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const term = Object.entries(SESSION_TERMS_BY_MONTH).find(([_, months]) => + months.includes(parseInt(currentMonth)), + ); + return term ? term[0] : 'Unknown term'; +} diff --git a/web-api/src/getUtilities.ts b/web-api/src/getUtilities.ts index 69f45fee431..9a3e049c8ba 100644 --- a/web-api/src/getUtilities.ts +++ b/web-api/src/getUtilities.ts @@ -7,6 +7,7 @@ import { createEndOfDayISO, createISODateString, createStartOfDayISO, + deconstructDate, formatDateString, formatNow, prepareDateFromString, @@ -65,6 +66,7 @@ const utilities = { createEndOfDayISO, createISODateString, createStartOfDayISO, + deconstructDate, documentUrlTranslator, formatDateString, formatJudgeName, diff --git a/web-client/src/presenter/actions/TrialSession/computeTrialSessionFormDataAction.ts b/web-client/src/presenter/actions/TrialSession/computeTrialSessionFormDataAction.ts index 3569bdd9fef..21d28371646 100644 --- a/web-client/src/presenter/actions/TrialSession/computeTrialSessionFormDataAction.ts +++ b/web-client/src/presenter/actions/TrialSession/computeTrialSessionFormDataAction.ts @@ -1,3 +1,4 @@ +import { SESSION_TERMS_BY_MONTH } from '@shared/business/entities/EntityConstants'; import { state } from '@web-client/presenter/app.cerebral'; export const computeTermAndUpdateState = ( @@ -12,20 +13,13 @@ export const computeTermAndUpdateState = ( const monthAsNumber = +date.month; - const termsByMonth = { - fall: [9, 10, 11, 12], - spring: [4, 5, 6], - summer: [7, 8], - winter: [1, 2, 3], - }; - - if (termsByMonth.winter.includes(monthAsNumber)) { + if (SESSION_TERMS_BY_MONTH.winter.includes(monthAsNumber)) { term = 'Winter'; - } else if (termsByMonth.spring.includes(monthAsNumber)) { + } else if (SESSION_TERMS_BY_MONTH.spring.includes(monthAsNumber)) { term = 'Spring'; - } else if (termsByMonth.summer.includes(monthAsNumber)) { + } else if (SESSION_TERMS_BY_MONTH.summer.includes(monthAsNumber)) { term = 'Summer'; - } else if (termsByMonth.fall.includes(monthAsNumber)) { + } else if (SESSION_TERMS_BY_MONTH.fall.includes(monthAsNumber)) { term = 'Fall'; } From 842b3c4a49b68fe73ae8551929e8ddbb26625bc1 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Wed, 18 Sep 2024 16:09:17 -0400 Subject: [PATCH 030/208] 10275: identify where to handle cities that have not been visited in the past two terms --- .../createProspectiveTrialSessions.ts | 24 +++++++++++++++++++ ...SuggestedTrialSessionCalendarInteractor.ts | 12 ++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts index 01cd0112c0b..d3187d0580d 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts @@ -13,6 +13,7 @@ export type EligibleCase = Pick< export const createProspectiveTrialSessions = ({ calendaringConfig, cases, + citiesFromLastTwoTerms, }: { cases: EligibleCase[]; calendaringConfig: { @@ -25,6 +26,7 @@ export const createProspectiveTrialSessions = ({ hybridCaseMaxQuantity: number; hybridCaseMinimumQuantity: number; }; + citiesFromLastTwoTerms: string[]; }): Record< string, { @@ -53,6 +55,10 @@ export const createProspectiveTrialSessions = ({ for (const city in potentialTrialLocations) { let regularCaseSliceSize; let smallCaseSliceSize; + + const cityWasNotVisitedInLastTwoTerms = + !citiesFromLastTwoTerms.includes(city); + // One of these arrays will continue to decrease in size until it is smaller than the other, at which point prioritization below will flip. // For now, we are okay with this -- TODO 10275 confirm // schedule regular or small @@ -60,6 +66,7 @@ export const createProspectiveTrialSessions = ({ scheduleRegularCases({ calendaringConfig, city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, regularCaseSliceSize, regularCasesByCity, @@ -67,6 +74,7 @@ export const createProspectiveTrialSessions = ({ scheduleSmallCases({ calendaringConfig, city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, smallCaseSliceSize, smallCasesByCity, @@ -88,6 +96,15 @@ export const createProspectiveTrialSessions = ({ }); } + // TODO (10275): Handle cities that have not been visited in the past two terms + + // Are there any cities that have not been visited in the last two terms + // that have not yet had any sessions scheduled? For any locations that + // meet this criterion, assemble all cases associated with that location in + // a session, disregarding the minimum quantity rule. The type of each + // session will depend on the sort of cases for that city: i.e., could be a + // regular, small, or hybrid session depending on the cases. + // Handle Hybrid Sessions const remainingRegularCases = regularCasesByCity[city] || []; const remainingSmallCases = smallCasesByCity[city] || []; @@ -110,6 +127,7 @@ export const createProspectiveTrialSessions = ({ addProspectiveTrialSession({ city, + cityWasNotVisitedInLastTwoTerms: false, potentialTrialLocations, sessionType: SESSION_TYPES.hybrid, }); @@ -149,11 +167,13 @@ function getCasesByCity(cases, type) { function addProspectiveTrialSession({ city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, sessionType, }) { potentialTrialLocations[city].push({ city, + cityWasNotVisitedInLastTwoTerms, sessionType, }); } @@ -161,6 +181,7 @@ function addProspectiveTrialSession({ function scheduleRegularCases({ calendaringConfig, city, + cityWasNotVisitedInLastTwoTerms = false, potentialTrialLocations, regularCasesByCity, regularCaseSliceSize, @@ -175,6 +196,7 @@ function scheduleRegularCases({ addProspectiveTrialSession({ city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, sessionType: SESSION_TYPES.regular, }); @@ -184,6 +206,7 @@ function scheduleRegularCases({ function scheduleSmallCases({ calendaringConfig, city, + cityWasNotVisitedInLastTwoTerms = false, potentialTrialLocations, smallCasesByCity, smallCaseSliceSize, @@ -198,6 +221,7 @@ function scheduleSmallCases({ addProspectiveTrialSession({ city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, sessionType: SESSION_TYPES.small, }); diff --git a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts index 93c3d7cb874..4f847d1c3e7 100644 --- a/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts +++ b/web-api/src/business/useCases/trialSessions/generateSuggestedTrialSessionCalendarInteractor.ts @@ -2,7 +2,6 @@ import { SESSION_STATUS_TYPES, SESSION_TERMS_BY_MONTH, SESSION_TYPES, - TRIAL_CITY_STRINGS, } from '../../../../../shared/src/business/entities/EntityConstants'; import { ServerApplicationContext } from '@web-api/applicationContext'; import { TrialSession } from '@shared/business/entities/trialSessions/TrialSession'; @@ -94,13 +93,10 @@ export const generateSuggestedTrialSessionCalendarInteractor = async ( } }); - const citiesThatHaveNotBeenVisitedInTwoTerms = TRIAL_CITY_STRINGS.filter( - city => !citiesFromLastTwoTerms.includes(city), - ); - const prospectiveSessionsByCity = createProspectiveTrialSessions({ calendaringConfig, cases, + citiesFromLastTwoTerms, }); const scheduledTrialSessions = assignSessionsToWeeks({ @@ -128,11 +124,11 @@ const getPreviousTwoTerms = (startDate: string) => { `fall ${deconstructedDate.year}`, ]; const termsReversed = terms.reverse(); - const termI = terms.findIndex( + const termIndex = terms.findIndex( t => `${currentTerm} ${deconstructedDate.year}` === t, ); - const [term1, year1] = termsReversed[termI + 1].split(' '); - const [term2, year2] = termsReversed[termI + 2].split(' '); + const [term1, year1] = termsReversed[termIndex + 1].split(' '); + const [term2, year2] = termsReversed[termIndex + 2].split(' '); return [`${term1}, ${year1}`, `${term2}, ${year2}`]; }; From 92ab7c5443eabdee1cbd27f115bdbccb99b2923d Mon Sep 17 00:00:00 2001 From: Nate Elliott Date: Wed, 18 Sep 2024 17:09:10 -0500 Subject: [PATCH 031/208] 10275 Handle low volume cities --- .../createProspectiveTrialSessions.test.ts | 57 +++++++++++++++++++ .../createProspectiveTrialSessions.ts | 48 +++++++++++++--- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts index 5bd918bf110..1f67e18e347 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.test.ts @@ -44,6 +44,7 @@ describe('createProspectiveTrialSessions', () => { const result = createProspectiveTrialSessions({ calendaringConfig: defaultMockCalendaringConfig, cases: mockCases, + citiesFromLastTwoTerms: TRIAL_CITY_STRINGS, }); expect(result[mockRegularCityString].length).toEqual( @@ -85,6 +86,7 @@ describe('createProspectiveTrialSessions', () => { const result = createProspectiveTrialSessions({ calendaringConfig: defaultMockCalendaringConfig, cases: mockCases, + citiesFromLastTwoTerms: TRIAL_CITY_STRINGS, }); expect(result[mockRegularCityString][0].sessionType).toEqual( @@ -141,6 +143,7 @@ describe('createProspectiveTrialSessions', () => { const result = createProspectiveTrialSessions({ calendaringConfig: mockCalendaringConfig, cases: mockCases, + citiesFromLastTwoTerms: TRIAL_CITY_STRINGS, }); expect(result[mockRegularCityString][0].sessionType).toEqual( @@ -170,7 +173,61 @@ describe('createProspectiveTrialSessions', () => { createProspectiveTrialSessions({ calendaringConfig: defaultMockCalendaringConfig, cases: mockCases, + citiesFromLastTwoTerms: TRIAL_CITY_STRINGS, }); }).toThrow(Error); }); + + it('should ignore minimums and schedule a session for a location that has not been visited in the previous two terms', () => { + const totalNumberOfMockCases = + defaultMockCalendaringConfig.maxSessionsPerLocation * + defaultMockCalendaringConfig.regularCaseMaxQuantity + + defaultMockCalendaringConfig.regularCaseMaxQuantity; + + const indexOfMockLowVolumeCityString = TRIAL_CITY_STRINGS.length - 2; + const mockLowVolumeCityString = + TRIAL_CITY_STRINGS[indexOfMockLowVolumeCityString]; + let mockTrialCitiesFromLastTwoTerms = [...TRIAL_CITY_STRINGS]; + mockTrialCitiesFromLastTwoTerms.splice(indexOfMockLowVolumeCityString, 1); + + const mockCases: RawCase[] = []; + for (let i = 0; i < totalNumberOfMockCases; ++i) { + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: `10${i}-24`, + preferredTrialCity: mockRegularCityString, + procedureType: PROCEDURE_TYPES_MAP.regular, + }); + } + + mockCases.push({ + ...MOCK_CASE_READY_FOR_TRIAL_SESSION_SCHEDULING, + docketNumber: '999-24', + preferredTrialCity: mockLowVolumeCityString, + procedureType: PROCEDURE_TYPES_MAP.regular, + }); + + const result = createProspectiveTrialSessions({ + calendaringConfig: defaultMockCalendaringConfig, + cases: mockCases, + citiesFromLastTwoTerms: mockTrialCitiesFromLastTwoTerms, + }); + + const includedLocations = Object.keys(result); + + expect(result[mockRegularCityString].length).toEqual( + defaultMockCalendaringConfig.maxSessionsPerLocation, + ); + expect(includedLocations).toEqual([ + mockRegularCityString, + mockLowVolumeCityString, + ]); + expect(result[mockLowVolumeCityString].length).toEqual(1); + }); + + // Repeat the test above with combos of small only, small and regular, so that we get + // small, regular, and hybrid sessions for the low volume city. + + // Make sure the value of cityWasNotVisitedInLastTwoTerms for sessions is correct + // for the given city. }); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts index d3187d0580d..69f6b704fa3 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/createProspectiveTrialSessions.ts @@ -83,6 +83,7 @@ export const createProspectiveTrialSessions = ({ scheduleSmallCases({ calendaringConfig, city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, smallCaseSliceSize, smallCasesByCity, @@ -90,21 +91,13 @@ export const createProspectiveTrialSessions = ({ scheduleRegularCases({ calendaringConfig, city, + cityWasNotVisitedInLastTwoTerms, potentialTrialLocations, regularCaseSliceSize, regularCasesByCity, }); } - // TODO (10275): Handle cities that have not been visited in the past two terms - - // Are there any cities that have not been visited in the last two terms - // that have not yet had any sessions scheduled? For any locations that - // meet this criterion, assemble all cases associated with that location in - // a session, disregarding the minimum quantity rule. The type of each - // session will depend on the sort of cases for that city: i.e., could be a - // regular, small, or hybrid session depending on the cases. - // Handle Hybrid Sessions const remainingRegularCases = regularCasesByCity[city] || []; const remainingSmallCases = smallCasesByCity[city] || []; @@ -132,6 +125,43 @@ export const createProspectiveTrialSessions = ({ sessionType: SESSION_TYPES.hybrid, }); } + + // TODO (10275): Handle cities that have not been visited in the past two terms + + // Are there any cities that have not been visited in the last two terms + // that have not yet had any sessions scheduled? For any locations that + // meet this criterion, assemble all cases associated with that location in + // a session, disregarding the minimum quantity rule. The type of each + // session will depend on the sort of cases for that city: i.e., could be a + // regular, small, or hybrid session depending on the cases. + + // if current city is low volume city and has not yet been scheduled, we know it did not meet any minimums above. + // So, add one session, determining the type based on the procedure type of the associated cases. + if ( + cityWasNotVisitedInLastTwoTerms && + potentialTrialLocations[city].length === 0 + ) { + console.log('regular cases for low volume', regularCasesByCity[city]); + console.log('small cases for low volume', smallCasesByCity[city]); + const containsRegularCase = regularCasesByCity[city]?.length > 0; + const containsSmallCase = smallCasesByCity[city]?.length > 0; + const lowVolumeSessionType = + containsRegularCase && containsSmallCase + ? SESSION_TYPES.hybrid + : containsRegularCase + ? SESSION_TYPES.regular + : SESSION_TYPES.small; + + addProspectiveTrialSession({ + city, + cityWasNotVisitedInLastTwoTerms, + potentialTrialLocations, + sessionType: lowVolumeSessionType, + }); + + regularCasesByCity[city] = []; + smallCasesByCity[city] = []; + } } // TODO don't forget we need to deal with overrides. From 9bb55456f954f5155a7534e141beb76c7e40cc4a Mon Sep 17 00:00:00 2001 From: Nate Elliott Date: Fri, 20 Sep 2024 13:04:58 -0500 Subject: [PATCH 032/208] 10275: WIP Begin work on front end wiring --- web-client/src/presenter/presenter.ts | 6 ++++++ .../src/views/TrialSessions/TrialSessions.tsx | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/web-client/src/presenter/presenter.ts b/web-client/src/presenter/presenter.ts index b0c1626252b..adaa6796439 100644 --- a/web-client/src/presenter/presenter.ts +++ b/web-client/src/presenter/presenter.ts @@ -292,6 +292,7 @@ import { openConfirmServeToIrsModalSequence } from './sequences/openConfirmServe import { openCreateCaseDeadlineModalSequence } from './sequences/openCreateCaseDeadlineModalSequence'; import { openCreateMessageModalSequence } from './sequences/openCreateMessageModalSequence'; import { openCreateOrderChooseTypeModalSequence } from './sequences/openCreateOrderChooseTypeModalSequence'; +import { openCreateTermModalSequence } from '@web-client/presenter/sequences/TrialSessions/openCreateTermModalSequence'; import { openDeleteCaseDeadlineModalSequence } from './sequences/openDeleteCaseDeadlineModalSequence'; import { openDeleteCaseNoteConfirmModalSequence } from './sequences/openDeleteCaseNoteConfirmModalSequence'; import { openDeletePractitionerDocumentConfirmModalSequence } from './sequences/openDeletePractitionerDocumentConfirmModalSequence'; @@ -444,6 +445,7 @@ import { submitCourtIssuedDocketEntrySequence } from './sequences/submitCourtIss import { submitCourtIssuedOrderSequence } from './sequences/submitCourtIssuedOrderSequence'; import { submitCreateOrderModalSequence } from './sequences/submitCreateOrderModalSequence'; import { submitCreatePetitionerAccountFormSequence } from '@web-client/presenter/sequences/submitCreatePetitionerAccountFormSequence'; +import { submitCreateTermModalSequence } from '@web-client/presenter/sequences/TrialSessions/submitCreateTermModalSequence'; import { submitEditContactSequence } from './sequences/submitEditContactSequence'; import { submitEditDeficiencyStatisticSequence } from './sequences/submitEditDeficiencyStatisticSequence'; import { submitEditDocketEntryMetaSequence } from './sequences/submitEditDocketEntryMetaSequence'; @@ -1091,6 +1093,8 @@ export const presenterSequences = { openCreateMessageModalSequence as unknown as Function, openCreateOrderChooseTypeModalSequence: openCreateOrderChooseTypeModalSequence as unknown as Function, + openCreateTermModalSequence: + openCreateTermModalSequence as unknown as Function, openDeleteCaseDeadlineModalSequence: openDeleteCaseDeadlineModalSequence as unknown as Function, openDeleteCaseNoteConfirmModalSequence: @@ -1343,6 +1347,8 @@ export const presenterSequences = { submitCreateOrderModalSequence as unknown as Function, submitCreatePetitionerAccountFormSequence: submitCreatePetitionerAccountFormSequence as unknown as Function, + submitCreateTermModalSequence: + submitCreateTermModalSequence as unknown as Function, submitEditContactSequence: submitEditContactSequence as unknown as Function, submitEditDeficiencyStatisticSequence: submitEditDeficiencyStatisticSequence as unknown as Function, diff --git a/web-client/src/views/TrialSessions/TrialSessions.tsx b/web-client/src/views/TrialSessions/TrialSessions.tsx index fc7f48debd4..48f42c1f61c 100644 --- a/web-client/src/views/TrialSessions/TrialSessions.tsx +++ b/web-client/src/views/TrialSessions/TrialSessions.tsx @@ -1,5 +1,6 @@ import { BigHeader } from '../BigHeader'; import { Button } from '../../ustc-ui/Button/Button'; +import { CreateTermModal } from '@web-client/views/CreateTermModal'; import { ErrorNotification } from '../ErrorNotification'; import { SuccessNotification } from '../SuccessNotification'; import { Tab, Tabs } from '../../ustc-ui/Tabs/Tabs'; @@ -12,13 +13,17 @@ import React from 'react'; export const TrialSessions = connect( { defaultTab: state.screenMetadata.trialSessionFilters.status, + openCreateTermModalSequence: sequences.openCreateTermModalSequence, openTrialSessionPlanningModalSequence: sequences.openTrialSessionPlanningModalSequence, + showModal: state.modal.showModal, showNewTrialSession: state.trialSessionsHelper.showNewTrialSession, }, function TrialSessions({ defaultTab, + openCreateTermModalSequence, openTrialSessionPlanningModalSequence, + showModal, showNewTrialSession, }) { return ( @@ -35,6 +40,14 @@ export const TrialSessions = connect( id="trial-sessions-tabs" >
+ + {trialSessionsHelper.showCreateTermButton && ( + + )}
- {showNewTrialSession && ( + {trialSessionsHelper.showNewTrialSession && ( - )} - + + )} From 9ef43b0f30dee7b112692934a1c47f6c8078cd59 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:33:12 -0600 Subject: [PATCH 200/208] 10275: UX feedback: add more specific date validation in create term modal; update warning messages; update cell colors and widths --- shared/src/business/entities/EntityConstants.ts | 2 +- .../trialSessions/GenerateSuggestedTermForm.ts | 6 ++++-- .../trialSessionCalendaring/constraints.ts | 10 +++++----- .../writeTrialSessionDataToExcel.ts | 5 +++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/shared/src/business/entities/EntityConstants.ts b/shared/src/business/entities/EntityConstants.ts index 66d0272cd03..f12fa17ae3e 100644 --- a/shared/src/business/entities/EntityConstants.ts +++ b/shared/src/business/entities/EntityConstants.ts @@ -208,7 +208,7 @@ export const CLOSED_CASE_STATUSES = [ CASE_STATUS_TYPES.closedDismissed, ]; export const SUGGESTED_TRIAL_SESSION_TITLES = { - invalid: 'Create term error.', + invalid: 'Unable to generate suggested term.', success: 'Successfully generated suggested term.', warning: 'Successfully generated suggested term with warnings.', }; diff --git a/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts b/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts index a61618af89c..df03bdd58e3 100644 --- a/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts +++ b/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts @@ -19,12 +19,14 @@ export class GenerateSuggestedTermForm extends JoiValidationEntity { termEndDate: JoiValidationConstants.DATE_RANGE_PICKER_DATE.min( joi.ref('termStartDate'), ) + .greater('now') .required() .messages({ '*': 'Enter date in format MM/DD/YYYY.', 'any.ref': 'Enter a start date', - 'date.min': - 'End date cannot be prior to start date. Enter a valid end date.', + 'date.greater': + 'End date must be after today. Enter a valid end date.', + 'date.min': 'Start date cannot be in the past. Enter a valid date.', }), termName: JoiValidationConstants.STRING.required() .max(100) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts index 4f09910f812..0e64c03993c 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts @@ -62,7 +62,7 @@ export const maxSessionsPerWeekConstraint: Constraint = ({ calendaringConfig.maxSessionsPerWeek; if (!meetsConstraint && session.sessionType === SESSION_TYPES.special) { - return `Special sessions for week of ${formatDateString(session.weekOf, FORMATS.MD)} exceed maximum sessions allowed per week (${session.trialLocation}). \n`; + return `More special sessions than maximum allowed per week: ${formatDateString(session.weekOf, FORMATS.MD)} \n`; //(${session.trialLocation}). } return meetsConstraint; @@ -85,7 +85,7 @@ export const oneSessionPerLocationPerWeekConstraint: Constraint = ({ ].has(session.trialLocation); if (!meetsConstraint && session.sessionType === SESSION_TYPES.special) { - return `There must only be one special trial session per location per week (${session.trialLocation}, ${formatDateString(session.weekOf, FORMATS.MD)}). \n`; + return `More than one special trial per week scheduled: ${session.trialLocation}, ${formatDateString(session.weekOf, FORMATS.MD)}. \n`; } return meetsConstraint; @@ -109,7 +109,7 @@ export const maxSessionsPerLocationConstraint: Constraint = ({ calendaringConfig.maxSessionsPerLocation; if (!meetsConstraint && session.sessionType === SESSION_TYPES.special) { - return `Special session count exceeds the max sessions per location for ${session.trialLocation} (${formatDateString(session.weekOf, FORMATS.MD)}). \n`; + return `More special sessions than maximum allowed per location scheduled: ${session.trialLocation}. \n`; //(${formatDateString(session.weekOf, FORMATS.MD)}) } return meetsConstraint; @@ -132,14 +132,14 @@ export const washingtonDcSpecialConstraint: Constraint = ({ WASHINGTON_DC_SOUTH_STRING, ) ) { - return `There must be no more than two special trial sessions per week in Washington, DC (${formatDateString(session.weekOf, FORMATS.MD)}). \n`; + return `More than two special trial sessions per week: ${WASHINGTON_DC_STRING} ${formatDateString(session.weekOf, FORMATS.MD)}. \n`; } if ( calendarState.sessionCountPerCity[WASHINGTON_DC_SOUTH_STRING] >= calendaringConfig.maxSessionsPerLocation ) { - return `Special sessions in ${WASHINGTON_DC_STRING} exceed the maximum allowed. \n`; + return `More special sessions than maximum allowed per location scheduled: ${WASHINGTON_DC_STRING}. \n`; } return true; diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.ts index 7e6ef047bd3..43110466b29 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.ts @@ -121,6 +121,7 @@ export const writeTrialSessionDataToExcel = async ({ { header: 'Warnings', key: 'warning', + width: 75, }, ]; @@ -241,7 +242,7 @@ const getCellStyle = ( switch (cellValue) { case SESSION_TYPES.hybrid: fill = { - fgColor: { argb: 'ffFDB8AE' }, + fgColor: { argb: 'fffee685' }, pattern: 'solid', type: 'pattern', }; @@ -262,7 +263,7 @@ const getCellStyle = ( break; case SESSION_TYPES.special: fill = { - fgColor: { argb: 'ffD0C3E9' }, + fgColor: { argb: 'ffffbe2e' }, pattern: 'solid', type: 'pattern', }; From 50a2cd666aeb661f413c98349a6af7726fec1f10 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 15 Nov 2024 14:43:50 -0500 Subject: [PATCH 201/208] 10275: address UX feedback --- .../trialSessionCalendaring/generateCalendar.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts index 5c444029175..792525b4dda 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts @@ -93,9 +93,16 @@ export const generateCalendar = ({ return typeof r === 'string'; }); - if (messages.length) { - scheduledTrialSession.ignoresConstraints = true; - } + messages.forEach(message => { + if ( + message.startsWith( + 'More than one special trial per week scheduled:', + ) || + message.startsWith('More than two special trial sessions per week:') + ) { + scheduledTrialSession.ignoresConstraints = true; + } + }); userMessages.push(...messages); @@ -142,7 +149,7 @@ export const generateCalendar = ({ return { caseCountsAndSessionsByCity, - userMessages, + userMessages: [...new Set(userMessages)], }; }; From 00c3691ad37fcc67650e1bf6542df42ca981aca4 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 15 Nov 2024 15:08:06 -0500 Subject: [PATCH 202/208] 10275: update broken tests --- .../trialSessionCalendaring/constraints.test.ts | 13 +++++-------- .../trialSessionCalendaring/constraints.ts | 4 ++-- .../trialSessionCalendaring/generateCalendar.ts | 7 +++++++ .../writeTrialSessionDataToExcel.test.ts | 2 ++ 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.test.ts index 576c527fa3c..36662cf5bac 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.test.ts @@ -185,7 +185,7 @@ describe('constraints', () => { // Assert expect(result).toEqual( - `Special sessions for week of ${dateString} exceed maximum sessions allowed per week (${mockSession.trialLocation}). \n`, + `More special sessions than maximum allowed per week: ${dateString} \n`, ); }); }); @@ -254,7 +254,6 @@ describe('constraints', () => { trialLocation: mockRegularCityString, weekOf: mockWeekString, }; - const dateString = formatDateString(mockSession.weekOf, FORMATS.MD); // Act const result = maxSessionsPerLocationConstraint({ @@ -265,7 +264,7 @@ describe('constraints', () => { // Assert expect(result).toEqual( - `Special session count exceeds the max sessions per location for ${mockSession.trialLocation} (${dateString}). \n`, + `More special sessions than maximum allowed per location scheduled: ${mockSession.trialLocation}. \n`, ); }); }); @@ -345,7 +344,7 @@ describe('constraints', () => { // Assert expect(result).toEqual( - `There must only be one special trial session per location per week (${mockSession.trialLocation}, ${dateString}). \n`, + `More than one special trial per week scheduled: ${mockSession.trialLocation}, ${dateString}. \n`, ); }); }); @@ -470,11 +469,9 @@ describe('constraints', () => { session: mockSession, }); - console.log(result); - // Assert expect(result).toEqual( - `Special sessions in ${WASHINGTON_DC_STRING} exceed the maximum allowed. \n`, + `More special sessions than maximum allowed per location scheduled: ${WASHINGTON_DC_STRING}. \n`, ); }, ); @@ -506,7 +503,7 @@ describe('constraints', () => { // Assert expect(result).toEqual( - `There must be no more than two special trial sessions per week in Washington, DC (${dateString}). \n`, + `More than two special trial sessions per week: ${WASHINGTON_DC_STRING} ${dateString}. \n`, ); }, ); diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts index 0e64c03993c..afc4f6dff6d 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/constraints.ts @@ -62,7 +62,7 @@ export const maxSessionsPerWeekConstraint: Constraint = ({ calendaringConfig.maxSessionsPerWeek; if (!meetsConstraint && session.sessionType === SESSION_TYPES.special) { - return `More special sessions than maximum allowed per week: ${formatDateString(session.weekOf, FORMATS.MD)} \n`; //(${session.trialLocation}). + return `More special sessions than maximum allowed per week: ${formatDateString(session.weekOf, FORMATS.MD)} \n`; } return meetsConstraint; @@ -109,7 +109,7 @@ export const maxSessionsPerLocationConstraint: Constraint = ({ calendaringConfig.maxSessionsPerLocation; if (!meetsConstraint && session.sessionType === SESSION_TYPES.special) { - return `More special sessions than maximum allowed per location scheduled: ${session.trialLocation}. \n`; //(${formatDateString(session.weekOf, FORMATS.MD)}) + return `More special sessions than maximum allowed per location scheduled: ${session.trialLocation}. \n`; } return meetsConstraint; diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts index 792525b4dda..0538b0f2813 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/generateCalendar.ts @@ -93,6 +93,13 @@ export const generateCalendar = ({ return typeof r === 'string'; }); + /** + * Any given item in the messages array represents an ignored constraint. + * For business reasons, not all constraints trigger a formatting change + * in the resulting spreadsheet: therefore, only two specific categories + * of ignored constraints will cause the session to have ignoresConstraints + * set to true. + */ messages.forEach(message => { if ( message.startsWith( diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts index 5f0bbecec29..9552c2133b7 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts @@ -46,6 +46,7 @@ describe('writeTrialSessionDataToExcel', () => { await writeTrialSessionDataToExcel({ caseCountsAndSessionsByCity: mockCaseCountsAndSessionsByCity, incorrectSizeRegularCases: [], + userMessages: [], weeks, }); }); @@ -81,6 +82,7 @@ describe('writeTrialSessionDataToExcel', () => { await writeTrialSessionDataToExcel({ caseCountsAndSessionsByCity: mockCaseCountsAndSessionsByCity, incorrectSizeRegularCases: [], + userMessages: [], weeks, }); }); From 3a1c9744bf1f38e4aa36ddbde7d23428c8345076 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 15 Nov 2024 15:20:36 -0500 Subject: [PATCH 203/208] 10275: update GenerateSuggestedTermForm entity and add test --- .../trialSessions/GenerateSuggestedTermForm.test.ts | 13 +++++++++++++ .../trialSessions/GenerateSuggestedTermForm.ts | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.test.ts b/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.test.ts index 6fa1ee0c1b2..4083b652192 100644 --- a/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.test.ts +++ b/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.test.ts @@ -64,5 +64,18 @@ describe('GenerateSuggestTermForm', () => { termName: 'Term name must be 100 characters or fewer.', }); }); + + it('should fail validation when end date is in the past', () => { + const formEntity = new GenerateSuggestedTermForm({ + termEndDate: yesterdayFormatted, + termName: 'Test Term', + termStartDate: '01/01/2050', + }); + + expect(formEntity.isValid()).toBeFalsy(); + expect(formEntity.getFormattedValidationErrors()).toEqual({ + termEndDate: 'End date cannot be in the past. Enter a valid date.', + }); + }); }); }); diff --git a/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts b/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts index df03bdd58e3..a1e0a7bade8 100644 --- a/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts +++ b/shared/src/business/entities/trialSessions/GenerateSuggestedTermForm.ts @@ -24,9 +24,9 @@ export class GenerateSuggestedTermForm extends JoiValidationEntity { .messages({ '*': 'Enter date in format MM/DD/YYYY.', 'any.ref': 'Enter a start date', - 'date.greater': - 'End date must be after today. Enter a valid end date.', - 'date.min': 'Start date cannot be in the past. Enter a valid date.', + 'date.greater': 'End date cannot be in the past. Enter a valid date.', + 'date.min': + 'End date cannot be prior to start date. Enter a valid end date.', }), termName: JoiValidationConstants.STRING.required() .max(100) From c729fe26351cb4c713ea425f25bd33466b22f82f Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 15 Nov 2024 15:27:42 -0500 Subject: [PATCH 204/208] 10275: add aria-label to ModalDialog component's dialog element --- web-client/src/views/ModalDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web-client/src/views/ModalDialog.tsx b/web-client/src/views/ModalDialog.tsx index 88fb894c0b6..b2667988c64 100644 --- a/web-client/src/views/ModalDialog.tsx +++ b/web-client/src/views/ModalDialog.tsx @@ -105,6 +105,7 @@ export const ModalDialog = ({ Date: Mon, 18 Nov 2024 09:11:46 -0500 Subject: [PATCH 205/208] 10275: begin expanding tests for excel document containing term calendar --- .eslintrc.js | 2 + .../writeTrialSessionDataToExcel.test.ts | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 1febba6bc5c..2853d13b371 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -374,6 +374,7 @@ module.exports = { 'rect', 'reindex', 'renderer', + 'repo', 'rescan', 'restapi', 'riker', @@ -440,6 +441,7 @@ module.exports = { 'wicg', 'workitem', 'workitems', + 'xlsx', 'xpos', 'zendesk', ], diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts index 9552c2133b7..ac9d39e3d0e 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts @@ -1,6 +1,7 @@ import { CaseCountsAndSessionsByCity } from './getDataForCalendaring'; import { SESSION_TYPES } from '@shared/business/entities/EntityConstants'; import { writeTrialSessionDataToExcel } from './writeTrialSessionDataToExcel'; +import ExcelJS from 'exceljs'; const cityWithSpecialSession = 'Portland, OR'; const cities = [ @@ -88,4 +89,54 @@ describe('writeTrialSessionDataToExcel', () => { }); // 10275 TODO: consider writing tests that open xlsx file, inspects worksheets and so on + it('tk', async () => { + // Arrange + let mockCaseCountsAndSessionsByCity: CaseCountsAndSessionsByCity = {}; + for (const city of cities) { + for (const week of weeks) { + const randomType = Math.floor(Math.random() * 3); + if (!mockCaseCountsAndSessionsByCity[city]) { + mockCaseCountsAndSessionsByCity[city] = { + initialRegularCases: 0, + initialSmallCases: 0, + prospectiveSessions: [], + remainingRegularCases: 0, + remainingSmallCases: 0, + scheduledSessions: [], + }; + } + + const sessionType = + city === cityWithSpecialSession + ? SESSION_TYPES.special + : SESSION_TYPES[Object.keys(SESSION_TYPES)[randomType]]; + + mockCaseCountsAndSessionsByCity[city].scheduledSessions.push({ + sessionType, + trialLocation: city, + weekOf: week, + }); + } + } + + // Act + const buffer = await writeTrialSessionDataToExcel({ + caseCountsAndSessionsByCity: mockCaseCountsAndSessionsByCity, + incorrectSizeRegularCases: [], + userMessages: [], + weeks, + }); + + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(buffer); + const worksheet = workbook.getWorksheet('Suggested Session Calendar'); + + // Assert + expect(worksheet!.getCell('A2').text).toEqual('City'); + // 10275 TODO: possible paths for testing: + // - programmatically examine the worksheet and assert + // that everything looks good. + // - check an actual xlsx file into the repo and compare + // the result of this test against that fixture. + }); }); From b64873f1d744331090e1bd8639b3201674c9d7e7 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 18 Nov 2024 14:34:52 -0500 Subject: [PATCH 206/208] 10275: unit tests for writeTrialSessionDataToExcel --- .../test/mockCaseCountsAndSessionsByCity.json | 1267 +++++++++++++++++ .../src/test/mockIncorrectlySizedCases.json | 177 +++ shared/src/test/mockTermSpreadsheet.xlsx | Bin 0 -> 14208 bytes .../writeTrialSessionDataToExcel.test.ts | 275 ++-- 4 files changed, 1596 insertions(+), 123 deletions(-) create mode 100644 shared/src/test/mockCaseCountsAndSessionsByCity.json create mode 100644 shared/src/test/mockIncorrectlySizedCases.json create mode 100644 shared/src/test/mockTermSpreadsheet.xlsx diff --git a/shared/src/test/mockCaseCountsAndSessionsByCity.json b/shared/src/test/mockCaseCountsAndSessionsByCity.json new file mode 100644 index 00000000000..e9980afe6c9 --- /dev/null +++ b/shared/src/test/mockCaseCountsAndSessionsByCity.json @@ -0,0 +1,1267 @@ +{ + "Aberdeen, South Dakota": { + "initialRegularCases": 1, + "initialSmallCases": 3, + "prospectiveSessions": [], + "remainingRegularCases": 1, + "remainingSmallCases": 3, + "scheduledSessions": [] + }, + "Albany, New York": { + "initialRegularCases": 5, + "initialSmallCases": 17, + "prospectiveSessions": [], + "remainingRegularCases": 5, + "remainingSmallCases": 17, + "scheduledSessions": [] + }, + "Albuquerque, New Mexico": { + "initialRegularCases": 44, + "initialSmallCases": 24, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 24, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Albuquerque, New Mexico", + "weekOf": "2025-01-06" + } + ] + }, + "Anchorage, Alaska": { + "initialRegularCases": 24, + "initialSmallCases": 9, + "prospectiveSessions": [], + "remainingRegularCases": 24, + "remainingSmallCases": 9, + "scheduledSessions": [] + }, + "Atlanta, Georgia": { + "initialRegularCases": 280, + "initialSmallCases": 68, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Atlanta, Georgia" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Atlanta, Georgia" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Atlanta, Georgia" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Atlanta, Georgia" + } + ], + "remainingRegularCases": 280, + "remainingSmallCases": 68, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-01-27" + }, + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-02-03" + }, + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-02-17" + }, + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-03-03" + }, + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-03-03", + "ignoresConstraints": true + }, + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-03-10" + }, + { + "sessionType": "Special", + "trialLocation": "Atlanta, Georgia", + "weekOf": "2025-03-17" + } + ] + }, + "Baltimore, Maryland": { + "initialRegularCases": 57, + "initialSmallCases": 42, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Baltimore, Maryland", + "weekOf": "2025-03-10" + }, + { + "sessionType": "Small", + "trialLocation": "Baltimore, Maryland", + "weekOf": "2025-03-17" + } + ] + }, + "Billings, Montana": { + "initialRegularCases": 0, + "initialSmallCases": 2, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 2, + "scheduledSessions": [] + }, + "Birmingham, Alabama": { + "initialRegularCases": 64, + "initialSmallCases": 22, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 22, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Birmingham, Alabama", + "weekOf": "2025-01-20" + } + ] + }, + "Bismarck, North Dakota": { + "initialRegularCases": 0, + "initialSmallCases": 1, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 1, + "scheduledSessions": [] + }, + "Boise, Idaho": { + "initialRegularCases": 44, + "initialSmallCases": 13, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 13, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Boise, Idaho", + "weekOf": "2025-01-06" + } + ] + }, + "Boston, Massachusetts": { + "initialRegularCases": 142, + "initialSmallCases": 60, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Boston, Massachusetts", + "weekOf": "2025-03-10" + }, + { + "sessionType": "Regular", + "trialLocation": "Boston, Massachusetts", + "weekOf": "2025-03-17" + }, + { + "sessionType": "Small", + "trialLocation": "Boston, Massachusetts", + "weekOf": "2025-03-24" + } + ] + }, + "Buffalo, New York": { + "initialRegularCases": 37, + "initialSmallCases": 13, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Buffalo, New York", + "weekOf": "2025-03-31" + } + ] + }, + "Burlington, Vermont": { + "initialRegularCases": 1, + "initialSmallCases": 5, + "prospectiveSessions": [], + "remainingRegularCases": 1, + "remainingSmallCases": 5, + "scheduledSessions": [] + }, + "Charleston, West Virginia": { + "initialRegularCases": 5, + "initialSmallCases": 4, + "prospectiveSessions": [], + "remainingRegularCases": 5, + "remainingSmallCases": 4, + "scheduledSessions": [] + }, + "Cheyenne, Wyoming": { + "initialRegularCases": 0, + "initialSmallCases": 6, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 6, + "scheduledSessions": [] + }, + "Chicago, Illinois": { + "initialRegularCases": 126, + "initialSmallCases": 129, + "prospectiveSessions": [], + "remainingRegularCases": 26, + "remainingSmallCases": 4, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Chicago, Illinois", + "weekOf": "2025-03-10" + }, + { + "sessionType": "Small", + "trialLocation": "Chicago, Illinois", + "weekOf": "2025-02-24" + }, + { + "sessionType": "Regular", + "trialLocation": "Chicago, Illinois", + "weekOf": "2025-03-24" + } + ] + }, + "Cincinnati, Ohio": { + "initialRegularCases": 20, + "initialSmallCases": 12, + "prospectiveSessions": [], + "remainingRegularCases": 20, + "remainingSmallCases": 12, + "scheduledSessions": [] + }, + "Cleveland, Ohio": { + "initialRegularCases": 31, + "initialSmallCases": 12, + "prospectiveSessions": [], + "remainingRegularCases": 31, + "remainingSmallCases": 12, + "scheduledSessions": [] + }, + "Columbia, South Carolina": { + "initialRegularCases": 38, + "initialSmallCases": 6, + "prospectiveSessions": [], + "remainingRegularCases": 38, + "remainingSmallCases": 6, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Columbia, South Carolina", + "weekOf": "2025-01-13" + } + ] + }, + "Columbus, Ohio": { + "initialRegularCases": 63, + "initialSmallCases": 19, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Columbus, Ohio" + } + ], + "remainingRegularCases": 63, + "remainingSmallCases": 19, + "scheduledSessions": [] + }, + "Dallas, Texas": { + "initialRegularCases": 234, + "initialSmallCases": 130, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Dallas, Texas" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Dallas, Texas" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Dallas, Texas" + } + ], + "remainingRegularCases": 234, + "remainingSmallCases": 130, + "scheduledSessions": [] + }, + "Denver, Colorado": { + "initialRegularCases": 103, + "initialSmallCases": 128, + "prospectiveSessions": [], + "remainingRegularCases": 3, + "remainingSmallCases": 3, + "scheduledSessions": [ + { + "sessionType": "Small", + "trialLocation": "Denver, Colorado", + "weekOf": "2025-02-17" + }, + { + "sessionType": "Regular", + "trialLocation": "Denver, Colorado", + "weekOf": "2025-02-24" + } + ] + }, + "Des Moines, Iowa": { + "initialRegularCases": 26, + "initialSmallCases": 25, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Des Moines, Iowa", + "weekOf": "2025-02-24" + } + ] + }, + "Detroit, Michigan": { + "initialRegularCases": 190, + "initialSmallCases": 67, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Detroit, Michigan", + "weekOf": "2025-03-10" + }, + { + "sessionType": "Regular", + "trialLocation": "Detroit, Michigan", + "weekOf": "2025-03-17" + }, + { + "sessionType": "Small", + "trialLocation": "Detroit, Michigan", + "weekOf": "2025-03-24" + } + ] + }, + "El Paso, Texas": { + "initialRegularCases": 49, + "initialSmallCases": 10, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 10, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "El Paso, Texas", + "weekOf": "2025-01-13" + } + ] + }, + "Fresno, California": { + "initialRegularCases": 11, + "initialSmallCases": 48, + "prospectiveSessions": [], + "remainingRegularCases": 11, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Small", + "trialLocation": "Fresno, California", + "weekOf": "2025-01-20" + } + ] + }, + "Hartford, Connecticut": { + "initialRegularCases": 21, + "initialSmallCases": 6, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Hartford, Connecticut", + "weekOf": "2025-01-06" + } + ] + }, + "Helena, Montana": { + "initialRegularCases": 13, + "initialSmallCases": 2, + "prospectiveSessions": [], + "remainingRegularCases": 13, + "remainingSmallCases": 2, + "scheduledSessions": [] + }, + "Honolulu, Hawaii": { + "initialRegularCases": 30, + "initialSmallCases": 12, + "prospectiveSessions": [], + "remainingRegularCases": 30, + "remainingSmallCases": 12, + "scheduledSessions": [] + }, + "Houston, Texas": { + "initialRegularCases": 143, + "initialSmallCases": 28, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Houston, Texas" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Houston, Texas" + } + ], + "remainingRegularCases": 143, + "remainingSmallCases": 28, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Houston, Texas", + "weekOf": "2025-02-17" + } + ] + }, + "Indianapolis, Indiana": { + "initialRegularCases": 25, + "initialSmallCases": 10, + "prospectiveSessions": [], + "remainingRegularCases": 25, + "remainingSmallCases": 10, + "scheduledSessions": [] + }, + "Jackson, Mississippi": { + "initialRegularCases": 39, + "initialSmallCases": 15, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Jackson, Mississippi", + "weekOf": "2025-03-24" + } + ] + }, + "Jacksonville, Florida": { + "initialRegularCases": 30, + "initialSmallCases": 22, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Jacksonville, Florida", + "weekOf": "2025-02-17" + } + ] + }, + "Kansas City, Missouri": { + "initialRegularCases": 38, + "initialSmallCases": 16, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Kansas City, Missouri", + "weekOf": "2025-03-31" + } + ] + }, + "Knoxville, Tennessee": { + "initialRegularCases": 27, + "initialSmallCases": 17, + "prospectiveSessions": [], + "remainingRegularCases": 27, + "remainingSmallCases": 17, + "scheduledSessions": [] + }, + "Las Vegas, Nevada": { + "initialRegularCases": 89, + "initialSmallCases": 25, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 25, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Las Vegas, Nevada", + "weekOf": "2025-03-31" + } + ] + }, + "Little Rock, Arkansas": { + "initialRegularCases": 25, + "initialSmallCases": 19, + "prospectiveSessions": [], + "remainingRegularCases": 25, + "remainingSmallCases": 19, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Little Rock, Arkansas", + "weekOf": "2025-03-03" + } + ] + }, + "Los Angeles, California": { + "initialRegularCases": 411, + "initialSmallCases": 249, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Los Angeles, California" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Los Angeles, California" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Los Angeles, California" + } + ], + "remainingRegularCases": 111, + "remainingSmallCases": 249, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Los Angeles, California", + "weekOf": "2025-03-03" + }, + { + "sessionType": "Special", + "trialLocation": "Los Angeles, California", + "weekOf": "2025-03-17" + }, + { + "sessionType": "Regular", + "trialLocation": "Los Angeles, California", + "weekOf": "2025-01-27" + }, + { + "sessionType": "Regular", + "trialLocation": "Los Angeles, California", + "weekOf": "2025-02-03" + }, + { + "sessionType": "Regular", + "trialLocation": "Los Angeles, California", + "weekOf": "2025-02-10" + } + ] + }, + "Louisville, Kentucky": { + "initialRegularCases": 35, + "initialSmallCases": 33, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Louisville, Kentucky", + "weekOf": "2025-01-06" + } + ] + }, + "Lubbock, Texas": { + "initialRegularCases": 34, + "initialSmallCases": 15, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Lubbock, Texas", + "weekOf": "2025-01-13" + } + ] + }, + "Memphis, Tennessee": { + "initialRegularCases": 32, + "initialSmallCases": 20, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Hybrid", + "trialLocation": "Memphis, Tennessee" + } + ], + "remainingRegularCases": 32, + "remainingSmallCases": 20, + "scheduledSessions": [] + }, + "Miami, Florida": { + "initialRegularCases": 121, + "initialSmallCases": 36, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Miami, Florida", + "weekOf": "2025-03-24" + }, + { + "sessionType": "Regular", + "trialLocation": "Miami, Florida", + "weekOf": "2025-02-17" + }, + { + "sessionType": "Hybrid", + "trialLocation": "Miami, Florida", + "weekOf": "2025-02-24" + } + ] + }, + "Milwaukee, Wisconsin": { + "initialRegularCases": 70, + "initialSmallCases": 51, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Milwaukee, Wisconsin", + "weekOf": "2025-01-20" + }, + { + "sessionType": "Small", + "trialLocation": "Milwaukee, Wisconsin", + "weekOf": "2025-01-27" + } + ] + }, + "Mobile, Alabama": { + "initialRegularCases": 5, + "initialSmallCases": 8, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Mobile, Alabama", + "weekOf": "2024-12-30" + } + ] + }, + "Nashville, Tennessee": { + "initialRegularCases": 70, + "initialSmallCases": 34, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Nashville, Tennessee" + } + ], + "remainingRegularCases": 70, + "remainingSmallCases": 34, + "scheduledSessions": [] + }, + "New Orleans, Louisiana": { + "initialRegularCases": 89, + "initialSmallCases": 57, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "New Orleans, Louisiana", + "weekOf": "2025-03-10" + }, + { + "sessionType": "Small", + "trialLocation": "New Orleans, Louisiana", + "weekOf": "2025-03-17" + } + ] + }, + "New York City, New York": { + "initialRegularCases": 347, + "initialSmallCases": 118, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "New York City, New York" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "New York City, New York" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "New York City, New York" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "New York City, New York" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "New York City, New York" + } + ], + "remainingRegularCases": 347, + "remainingSmallCases": 118, + "scheduledSessions": [] + }, + "Oklahoma City, Oklahoma": { + "initialRegularCases": 40, + "initialSmallCases": 1, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 1, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Oklahoma City, Oklahoma", + "weekOf": "2025-01-13" + } + ] + }, + "Omaha, Nebraska": { + "initialRegularCases": 38, + "initialSmallCases": 12, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Omaha, Nebraska", + "weekOf": "2025-03-31" + } + ] + }, + "Peoria, Illinois": { + "initialRegularCases": 1, + "initialSmallCases": 16, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Peoria, Illinois", + "weekOf": "2024-12-30" + } + ] + }, + "Philadelphia, Pennsylvania": { + "initialRegularCases": 140, + "initialSmallCases": 125, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Philadelphia, Pennsylvania" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Philadelphia, Pennsylvania" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Philadelphia, Pennsylvania" + } + ], + "remainingRegularCases": 140, + "remainingSmallCases": 125, + "scheduledSessions": [] + }, + "Phoenix, Arizona": { + "initialRegularCases": 175, + "initialSmallCases": 42, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Phoenix, Arizona", + "weekOf": "2025-01-06" + }, + { + "sessionType": "Special", + "trialLocation": "Phoenix, Arizona", + "weekOf": "2025-02-10" + }, + { + "sessionType": "Regular", + "trialLocation": "Phoenix, Arizona", + "weekOf": "2025-01-20" + }, + { + "sessionType": "Regular", + "trialLocation": "Phoenix, Arizona", + "weekOf": "2025-01-27" + }, + { + "sessionType": "Small", + "trialLocation": "Phoenix, Arizona", + "weekOf": "2025-02-03" + } + ] + }, + "Pittsburgh, Pennsylvania": { + "initialRegularCases": 97, + "initialSmallCases": 46, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Pittsburgh, Pennsylvania" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Pittsburgh, Pennsylvania" + } + ], + "remainingRegularCases": 97, + "remainingSmallCases": 46, + "scheduledSessions": [] + }, + "Pocatello, Idaho": { + "initialRegularCases": 0, + "initialSmallCases": 5, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Small", + "trialLocation": "Pocatello, Idaho", + "weekOf": "2024-12-30" + } + ] + }, + "Portland, Maine": { + "initialRegularCases": 3, + "initialSmallCases": 9, + "prospectiveSessions": [], + "remainingRegularCases": 3, + "remainingSmallCases": 9, + "scheduledSessions": [] + }, + "Portland, Oregon": { + "initialRegularCases": 76, + "initialSmallCases": 47, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Portland, Oregon" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Portland, Oregon" + } + ], + "remainingRegularCases": 76, + "remainingSmallCases": 47, + "scheduledSessions": [] + }, + "Reno, Nevada": { + "initialRegularCases": 1, + "initialSmallCases": 2, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Reno, Nevada", + "weekOf": "2025-01-06" + } + ] + }, + "Richmond, Virginia": { + "initialRegularCases": 101, + "initialSmallCases": 51, + "prospectiveSessions": [], + "remainingRegularCases": 1, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "Richmond, Virginia", + "weekOf": "2025-01-13" + }, + { + "sessionType": "Small", + "trialLocation": "Richmond, Virginia", + "weekOf": "2025-01-20" + } + ] + }, + "Roanoke, Virginia": { + "initialRegularCases": 1, + "initialSmallCases": 3, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Roanoke, Virginia", + "weekOf": "2024-12-30" + } + ] + }, + "Salt Lake City, Utah": { + "initialRegularCases": 59, + "initialSmallCases": 39, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Salt Lake City, Utah" + } + ], + "remainingRegularCases": 59, + "remainingSmallCases": 39, + "scheduledSessions": [] + }, + "San Antonio, Texas": { + "initialRegularCases": 96, + "initialSmallCases": 27, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "San Antonio, Texas" + } + ], + "remainingRegularCases": 96, + "remainingSmallCases": 27, + "scheduledSessions": [] + }, + "San Diego, California": { + "initialRegularCases": 125, + "initialSmallCases": 120, + "prospectiveSessions": [], + "remainingRegularCases": 25, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "San Diego, California", + "weekOf": "2025-01-27" + }, + { + "sessionType": "Small", + "trialLocation": "San Diego, California", + "weekOf": "2025-02-03" + } + ] + }, + "San Francisco, California": { + "initialRegularCases": 397, + "initialSmallCases": 287, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "San Francisco, California" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "San Francisco, California" + } + ], + "remainingRegularCases": 0, + "remainingSmallCases": 287, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "San Francisco, California", + "weekOf": "2025-03-03" + }, + { + "sessionType": "Regular", + "trialLocation": "San Francisco, California", + "weekOf": "2025-02-03" + }, + { + "sessionType": "Regular", + "trialLocation": "San Francisco, California", + "weekOf": "2025-02-10" + }, + { + "sessionType": "Regular", + "trialLocation": "San Francisco, California", + "weekOf": "2025-02-17" + }, + { + "sessionType": "Regular", + "trialLocation": "San Francisco, California", + "weekOf": "2025-02-24" + } + ] + }, + "Seattle, Washington": { + "initialRegularCases": 155, + "initialSmallCases": 96, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Seattle, Washington" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Seattle, Washington" + }, + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "Seattle, Washington" + } + ], + "remainingRegularCases": 155, + "remainingSmallCases": 96, + "scheduledSessions": [] + }, + "Shreveport, Louisiana": { + "initialRegularCases": 0, + "initialSmallCases": 9, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 9, + "scheduledSessions": [] + }, + "Spokane, Washington": { + "initialRegularCases": 24, + "initialSmallCases": 15, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Hybrid", + "trialLocation": "Spokane, Washington", + "weekOf": "2025-01-13" + } + ] + }, + "St. Louis, Missouri": { + "initialRegularCases": 71, + "initialSmallCases": 40, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Small", + "trialLocation": "St. Louis, Missouri" + } + ], + "remainingRegularCases": 0, + "remainingSmallCases": 40, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "St. Louis, Missouri", + "weekOf": "2025-03-31" + } + ] + }, + "St. Paul, Minnesota": { + "initialRegularCases": 71, + "initialSmallCases": 57, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Regular", + "trialLocation": "St. Paul, Minnesota", + "weekOf": "2025-03-24" + }, + { + "sessionType": "Small", + "trialLocation": "St. Paul, Minnesota", + "weekOf": "2025-03-31" + } + ] + }, + "Syracuse, New York": { + "initialRegularCases": 2, + "initialSmallCases": 18, + "prospectiveSessions": [], + "remainingRegularCases": 2, + "remainingSmallCases": 18, + "scheduledSessions": [] + }, + "Tallahassee, Florida": { + "initialRegularCases": 0, + "initialSmallCases": 3, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Small", + "trialLocation": "Tallahassee, Florida", + "weekOf": "2024-12-30" + } + ] + }, + "Tampa, Florida": { + "initialRegularCases": 73, + "initialSmallCases": 78, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Small", + "trialLocation": "Tampa, Florida", + "weekOf": "2025-02-24" + }, + { + "sessionType": "Regular", + "trialLocation": "Tampa, Florida", + "weekOf": "2025-03-03" + } + ] + }, + "Washington (North), District of Columbia": { + "initialRegularCases": 0, + "initialSmallCases": 0, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Washington (North), District of Columbia", + "weekOf": "2025-02-10" + } + ] + }, + "Washington (South), District of Columbia": { + "initialRegularCases": 143, + "initialSmallCases": 48, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Special", + "trialLocation": "Washington (South), District of Columbia", + "weekOf": "2025-02-10" + }, + { + "sessionType": "Special", + "trialLocation": "Washington (South), District of Columbia", + "weekOf": "2025-02-10", + "ignoresConstraints": true + }, + { + "sessionType": "Regular", + "trialLocation": "Washington (South), District of Columbia", + "weekOf": "2025-01-20" + }, + { + "sessionType": "Regular", + "trialLocation": "Washington (South), District of Columbia", + "weekOf": "2025-01-27" + }, + { + "sessionType": "Small", + "trialLocation": "Washington (South), District of Columbia", + "weekOf": "2025-02-03" + } + ] + }, + "Wichita, Kansas": { + "initialRegularCases": 0, + "initialSmallCases": 1, + "prospectiveSessions": [], + "remainingRegularCases": 0, + "remainingSmallCases": 0, + "scheduledSessions": [ + { + "sessionType": "Small", + "trialLocation": "Wichita, Kansas", + "weekOf": "2024-12-30" + } + ] + }, + "Winston-Salem, North Carolina": { + "initialRegularCases": 77, + "initialSmallCases": 9, + "prospectiveSessions": [ + { + "cityWasNotVisitedInLastTwoTerms": false, + "sessionType": "Regular", + "trialLocation": "Winston-Salem, North Carolina" + } + ], + "remainingRegularCases": 77, + "remainingSmallCases": 9, + "scheduledSessions": [] + } +} diff --git a/shared/src/test/mockIncorrectlySizedCases.json b/shared/src/test/mockIncorrectlySizedCases.json new file mode 100644 index 00000000000..556067dde6a --- /dev/null +++ b/shared/src/test/mockIncorrectlySizedCases.json @@ -0,0 +1,177 @@ +[ + { + "_score": 5.610724, + "docketNumber": "2996-24", + "preferredTrialCity": "Portland, Maine", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "19814-23", + "preferredTrialCity": "Peoria, Illinois", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "5819-23", + "preferredTrialCity": "Syracuse, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "15415-23", + "preferredTrialCity": "Burlington, Vermont", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "10463-24", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "1587-22", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "16407-21", + "preferredTrialCity": "Albany, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "8443-23", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "4524-22", + "preferredTrialCity": "Albany, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "17634-23", + "preferredTrialCity": "Portland, Maine", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "28407-21", + "preferredTrialCity": "Aberdeen, South Dakota", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "5694-24", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "13516-23", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "10036-24", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "16919-23", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "1078-21", + "preferredTrialCity": "Syracuse, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "15537-23", + "preferredTrialCity": "Roanoke, Virginia", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "2834-24", + "preferredTrialCity": "Albany, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "1131-23", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "123-21", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "3854-22", + "preferredTrialCity": "Albany, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "4816-24", + "preferredTrialCity": "Portland, Maine", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "659-24", + "preferredTrialCity": "Albany, New York", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "8593-24", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + }, + { + "_score": 5.610724, + "docketNumber": "18769-23", + "preferredTrialCity": "Fresno, California", + "procedureType": "Regular", + "status": "General Docket - At Issue (Ready for Trial)" + } +] diff --git a/shared/src/test/mockTermSpreadsheet.xlsx b/shared/src/test/mockTermSpreadsheet.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..289d780fd5610ec4bbc7a234c28ed36f5eb87b7d GIT binary patch literal 14208 zcmai51yq$w*9Ig+5RmR}>FyK>5%ADRcXxLqB_#q_aXj+Uawy7 z_50togymxP?3o?Uex?+j-GhAq1qF2?(p)m}Q0}#GSQauuGshv|3pWtdd+yiB|U0<8Bi@8UqrM?9d{}`K7k?jy#^hFS^MO>AINcVlLUN{<8|LV>_$n}wXbW_&G6A(PkB7^RLS^%>qh=KQ@=wczu_ z$FDJa<@?OU=!Ql&UpcqDKF%-RbG+>FT?@>s9u%3bqxyKwI?QQl{nBPL2BeosYNBz~ zOKT`g6zb~WupE{FgH!=OUC=^6w^nFd2&mA8qH3GKrO`ep@?T< zHMg~d?}Gd(T+|T_uGofYxW9Np{jkWtt#%J$5m(EfL+y&8(6pW238u zy}|hptqAYuAjHKbV(}lbiUDWWzf`y&mpQ12#H_Y_q0i+9K5#8cS2=4Tmo6~cV|ake z+E!dQC1k)%2YNDc@i{g$5(YWOXF-5IARr>?$aLiSavLdK&lh{YaL152o(3hfWGWTOLc($%)iNDuiSAmG zIHZ%X-Tmu>2;`4Cv{?vgT&zSxtusuBM~iJ__)qx!t|FUWjIaXxXXEvHc@qcX^~Rf7 zu6CGJbpC91l<(%cGn9j=k&)x?L5rEx+T4x>1vLePg2Me*+ixPQch-%c1u7D#R(Btq z9jPh^q{tAXo1Q#hnoPsr0Bdb!t*N(`KZiquvu8S*Ac6Kj)_&P|RzpH@AtgSSEJ$>0(ad7o>l3o~!`!^)TQKHBrvd)y@Q zw{rriH`^n(p63qWbMQ#bfU!%t&ce;+@@CEK?&8wU&gLa>&HDQI=v2?4xx{lyXs*Zw z%uLdpCMYOiJ)W-fZr6IqczwNfYW8yN=GZAHT(Z3Dv@aof`*~&KjdxA$d?VB0r&_JO zQ~BFX;6VEb(A;Ihu(tkWd2gP?yKZhya3|p$w03HIcGj1fOXD(ub1?x1Enn6EuXVu8 z-7j^Ti>?oYs;_1TPL@y2z(!|B-nH!*W4D9h6`fPw+43Yp^E2% zUOd8r;NwL&8fN)&4}pt@i}D-q`j=L>{aoBS&7f0ZirZsDu$PO6LvtRNkEQilc2w^A z)>(0N4T-!7cpZEt%Pe$DWR+HYRx;78IPc^AY8wO^>GP4_wNCTd0yl#$OA>BxkJgTM zfj9Dtn>{CIxwEIc!XY+<)7k{aHVT z8&qp_FkbFzlSJgDZ&JcM-6v0@u{WMBBw&3j;O6NzmPR~2)C@D&S(b2hwVu>`>!u?N z>=T^x**3d9AxR$;PPsm4*uC)qXU|O}KOFb)R_TJfy)M*AYX_GPg3C)#FX)Z$u_|`P zGifS6soPh2wB4pEkr^(6mB%EeY*?3AKt9nah84t=;t>Bv@|CM{0mYMs!jnhj+7oN{ zA2C~c1in(O<`?UDRxcpdVNuT~))5XQAq!s?-;u-DS0#CIKvC;SwHy74q$fW0i8DG- zTAwB_)zCQ>D6L16ovP#v07@Is6s0=atbBGh`>OkwaLlw=j~)w$92hR@NYIOI))Bx*{fj^?8fJn3D14(FBCR2 z_VP0B-0R7sC;0c^TX@Ke0KTwl94K19gYLz+Zj|o6qcP z5vp`XpOUtu2~Ev%j-8UWrU^=Y;|!RRwxWqlt#lrS3)n=r*-r4&V^9t5^3HmZG-W&7 zAAH##cD9_5;bjb>ERUO%i&b4h4Lz@+t%~cv5vlcxZ)=jdkntybl&(k*zBlGPJemH? zf<}u6w?stX>uM=aa`=$IV?_($@%(;}=kUznNi9}0LxKZ%W?I^irYx1xId)pwn5H%r z(-|-=%~{h_ldmJqGown;l~X3HU_mqM$)b~XZdpNy?Nv*mr-nIHB&@Ay?96Irj5FO| zXOA%1CRFi1=fnswygXtYg z-Bip(ETdD+0(Le67|v)3Biy1v z)G*ZW)X>y$)c2?na-ecxb6|4dbD(qJay+<)lqCnt92~Jx4JBeZ(%!!3m0V($ zmped~J?O8)i)`UzWkF=2W5HwLWr1U%V!>kJWO>NK$TC~+mL!q%lvdeFIH{~gHQyj$ zM~=WymBg^Nq`wS-MfGmPEtD-;Eru;CEtoBDT9gnBu4oZt9awUPJQ!VtAo6bOwoC|a zYECP;jq!lD=^IbOwn*iXCI|(!99B^{5IQXW-ABu43r9;O0z1jgj5zOS8RHzQm=NWu zdO-d(AqjK7g8-wg!*RzzY9B9r3<1j{mj51lD!Xj2CvR7KCPnV#m#HYnd_@69KZoP{ zgYor2MA5C#7|_4H)VZ!|qo>NaeBc;4r?6Zq$JfMP-Pm?*MV=za!pb7c!pJ_5g_lK< zU7Vz-L@!h%ieNQ@-{yENcw*7Ym&)EhPM9>qB*7aUSGeM39~;ssTEN#k;?FNBzq^7k z^bFyN%b!fu;y5Ulq|DnRqnT?J`@Hd5t;8L^t5U!wXWcC=Le!Xa=-wZrBRMshT0O6K z8C#`m&bv@Gb;ZnYP8Rie#TwLycmLQo6s7BU$OL&@+E?1I$G69i&X>+F&o|G{%-76s z)3|-*Ie(+6+8gjEm7HT;?8#TG6XDa!Aj}fLufl(*< zaXWI!hN|g?ctQP|EIrPD^i5-Og4vsNy#yIIYE(0|qT_L-ER^?f2l@z}qKX`bM$=5TI*_cEOa$}oL_A^CI zyq}1h2%Gpl1tIcwHi+ZD%o$lOt*Bb5z@9q=Fjn2Ouv0fxX={@c;*o}YW=uNs>j1vz zAs_BCt9T0sf`NeXeSvgqF4;vGH|+|56ZTyXQu6cgb7S|8b$jHGybnSH?=*sBQ>zeE0 z>uTuMO|b-zI8#5yOXcbg#<;kQ$#gf%H``1r$n|KFJ=2Wd{jK2l5J%gH;uY=;@a`7e<>u1 zIN*D%oB9(j?_{Ut470aB2RN#9t$A0fry99TGt^%)^mz_dnTY-ebbW`N7RSA{4Qr%q z`ZKnYh`UsRL`S)x>d@&*ydINQH0Zyl`RNqnP@`0CoqjP@_Q{)kfhyB%r7BT)8c^*> zte~p=sb|toE$P&+!QleR(}EuVC7U#<>PO~iPzx@>hKl9f0Ht+Tjq>%ao;2n-K^#ht zKczk-tU{-i%lT_>pXZq)IMn6n+GKqkJgC$i8Ve9~yY_#E1N%qW7RO@r!Ja^kyk7a1 z#|AdS;{|eum4-uOag8l8amcZPy5ze584DBM{yOU@_jo{wWZ8TZb7aZx9%rt*xhQ`L zi%@$oNuBzJ`iA=b0vo7Zl@ui~+uxwQ#c`L#Zv>@8HUCl$N%2ncT&ID?r}fRnG=YuV z;d_h3-(Wq|q*>GC8)Bx(L(96cbV-jNK01SQgW14T_( z9CSJYjF4_-0PBGJACgdjDk#q8O??l8{zCL_! z^Ui){Ce5kZ_AEIR`-=N&zZdpYZsf%kUYuvy_`Qxp?Aaa^5>Bskvpv$%cH!7&k9MTL z#3Kv~{ zVYvnBb{~`eE{MZtA1hk@&k}iOwN*dhhTDRW`$CxpL}_A6j{$i?8Y}SVb*@W@DE!wn zZpP`OV^ii)YfF^OKL)EVpM2;#At<%8bfK{mASft)$x{!@17^Z4Vmb z;*^IGe)}Ip<_8)y#kPd1tAri$9YDBa38o}h?@guV(1b&y!kW9#=ab*V{6~a&d7uC< z+RvZ!JIUvnhuB}{XnxPjAEHfyQT2zS#R$K!f?oq={c&yDOn9NnDsHlVV@xJr*KQ@N z^9k$QvNW40-{L5#OTz|TxL4xznhqg+g$T!ym&)w` zKUBgdj@IZrW4|}Vi!fCCD_uMaflg`Q?*`=Y9qJs$+k~BdBW&TPo`TazMwROUztzWo z;57-g?IFzcv+TNPex@JT*(RuidM1XqdvO{rpnY*yApe`D9zDF69p+wfd8}AQLU%{j zW{ys5ZJ62xNdo@|fZee8$4QPqmT-&2Rx}YF&Xc=CzKMXwT8doU{{?(EEIx65kV*Cu z!Tw0ZO4Ff<4~?}Er=Dn_iv{---PE5f;GeV1v6)^MpsI~?u&g6omwJ)3q^psIvk^0= z`P3cy6@)XvpluRG92|aLDEp5xs*BrbhG0vtysn92Z!1vaLA0{qx^#z~3d~=`8*7nI zU_784v3wO!e<2~is%KKr=jZJn{HiV>TShkw$$W^ZhLMh?Y?vq-p;Geyokc&e2M75B zx0_Z?HXvR#M+46TT&C?5J8KrVvlYjn*w~T=rS;#H?JjX4v@8GWeXbJDVL;lCLY#@S z5jv+y?G7ykneiRe!$fbuk$$}w75`5<4JTuqo9aT}%QlQ88fN_b}m7+pyOLy`(o%eRk46+Xx``C=g&ALpoNH zC*JkJ^F^|U`6FF~gCt;6kQ%+R##r@|ZdVa^N6XwFl*j=legn2NR1Cgt1VM z<_vmgDW)!&O=-V3@j#N4WBm-NY$#o>sbr;cKj{3giHn1b!2{wGmr-s?)}T`cS+Vh$ zF(k5)7W^`DB_wtX+D|Fy`|`q<%eNNJu; zUjjC^1W9U)@ut1q4RWumiQb%R#YKIBB3kz+n8Kk)OeHjEr*r!&YaujVUo~AnX(#9u zbK}A9M7nu(wKef3yQIZn>~4W~$Fx?x0;)Mx@(&dZv-1ZIAXL12#J*8s=;UtwfwuB@ zaI_eb@hGAe{$rgM6l`d1Mqe!3>7DfvlJ-)}qZExn5yxWR7ak!C4^mpPGOp=Ur&?j# zo!0EbTFu<=7N|dPeUZiKsIuswG=IL4TYz|q2p(_6ZLv~-dGx3usQ*aJ_O15)LxDe3 zz&ZuQ*%iX%!C`hRFa(@CM`9ZkSyxd<^ytSAun%{slAg6?k2fC z+upq$^tn1{pGu%;c2PCVetfpnTL*Ze5;hb5xeg$!5;hw?SqBhPk(mkar~}BT$jpY1 z)&ZWX^v;C$*8${IdS}CD>HwlD^fTdYbpUA<`q}W|I)H>q{!BQepXVz1v*FWq0C5%b zneeVUz%v!|+3@ko;cc$folT%|MfiCANiHhtc1g1P_4&!#&E|^p(W;Sn`8=uMoagoF z>H6&H;;KBjXwU{YYwdA%I#x4qd9-$LQE(l6)aP990bbg1n)R*#-`JLWHB8yOAFoda zU3ZU!fR=adyefd)d(F4=pe9=a!5M*hEVD&Mf7|NG5^(pLor{B4)_&#fnsGCnfbhKV z+`w)E$j4e}uL;y_cg_ZE5VBgoz)QKV^}4!VwYl89h5oTg{Qa~do!zfAA97MLe*cdf zzii(&e%}J^lUdM$_rRDDy9R?@uD-JR-Or~Tb4GGTEZ~q{Vu(ag@`vQ^;dd9S=ye?s z6`Vh~g<^?2N4`NV#%T)0BPnfx{}H5}UEF=dt2S`FX4$ z(nZ#Igc$;?Mtoe=`0S{Ya;@=0Ws0w5*iux6;+%o@tNC(lbvq>H=G^-YWk7|w*v2*` z@2BEQSjl+=2IBtl2tjMOVX zrt~Vz*ufFbTotWH75dEG@J1|DCQ4*i@M{!juJx8bAcm1wsTAlWSx!%*dUGs9WgT;J z#&k*OBV*D-Uzc~neBjx4ZeElj!D`p>bfUrN%(Bm1g!f>7T6XkWBYng1g3lyB5Y#J8 zNW!?LG(sM?H02k0meE5rTlF;^{@#$=2h!k_zS%}qTXqJzS2aQDwLo%*J{RZ6#Iy7| zT}MZxK|3Vm&X$TD7(*OnU{LVv`5P_MX}GsquXe;_O%k6b4zfnFHnuuOY{HBfe(K*y z(TYUwwAGKtBdhNz)v}ztpt;r;XRAhi9&rV*c=_n02!o}26wuT1t2crXai&Lm)zfFG7;l|as4@SI(?TZOIopmlE} z?E1ZZ=gZa6!Yv2i)Hv|su#Uyo6II($;-K+N-X|8gMzXu)AG6WLNMKw_z{#7>*YEda zRC|-JXJG)I^>PXzZ`;6#2=4KC7Ws3?bW2D3GhKO`YNJ5Gg|m2ip&F}qo&=otCAiN< z6$l9oc4)!x9wcO)Bm6VIDZ*1ta3JxW1^dVNX8#u7Z+fhOENCKo*AM*9)WShL=#+3z zBj3YmkeVtU%f5K?Bybdsvhie_KhvDT+Vd5vhOtM=BC@&waV0swUTR=QRu4nY6J$bs zUPr3ylS9Wdk;g;fJoiW+Ydsq|&0faA$Rg4arsbQb84II;CQ-+8Y(TOZYmyHoVh1`c4D?B zlVqM}rCN*;=E+rtAt(Mdg=sWO9t*VQ0G`);WgLK`YGF~E$YZZ_ zZmQyzvPwBdzJUVd@i^G>h(U!6wYrg~t$AJF&~`TZ zYNw&e-+AGQlzg-K5$-WBO-Qp{d$<=5*b06~#@32YCu@mO|k--3XbwJivC(k_0zSZvn@J6viVeMSD$9B5oz3Kia; z#0=afbjARpL1E~qlgQ>5pLO{j)>wmbjIkB{B|A?Rdne_{3Ixk2sZ1o!^_nU_Ek;(0 zdE=>+7ZS9Wob~#0y=2PBDq13}#$}k~G~FVv!-_p{%)toNbmwc&gBvDMwWko9C`T@4 z%o~0^ygCnC0jHg0(af9%<}v)_|JLVa8p9jLgrj7+PIKGWE;|o7_?=H{MoXEe8HIGd zcyH?nY1mm!e|f-1-qRdb18{-icuygE8p)U85vr{jjX46#!@x_{YnIn*9#EjNcr9*UFK%ORJQAhbJIrHY2 zV}yZSVTseO1HlTDQ+Q&d5iWnF4guYh{vy7rTq=@-+bDM_)P{@ZA0YC_%MOmFMpj0D z---B6%$$>1=E)#;Oz=p)U;cqyn)u%A+s|)@@gVi*Hb5@irae`(x|+iT0L@3mj@Pr+ zR@0<*{KrzyDm#MF;bP^MfM19aka5Q!)+0|^h3LKX!=B6rb58MD5`AedRUHqZOEyoT zo>d^_Kfpn-)ia!&vW8v*^wxS-95NLa%q9?`C#=4>F}HaY zCw~V=xk8|Lw7ZQ!4wGp6ldP&x`%N!+l9<>Eyo69MmmJ0QEYrRBLSW|H@@`nZjzR-J zrYMq!J!R!+?SR(s*UHJ)>Rj(ZypehIbx(=b`;$UNh zH*KEZJK1)~ql2wyI@i((u7L-+*F+b#51Rx8YY*gAVbj*VU1Kw91cLasjbs!*RKBGH zO>0;byvV8K2s7Bvt{>Yjil=-1RQ+WRa&b`!q&*#jE(f0)HD!UIP9?8t{}dzphldf) zF=Y6I;{cYpM|r&w$OjZm@V-(B#EwR|?Z7FHf%vuRxElSFHRhfpU9>h4p zMJjh+Qg$f_IrJwd2sgT25JLX#tQ>^LbvLI8xY-|`<6sKns4OY;V!!d4|S(49_?bUo1IMl2950lyx~AeduQZ3)%h>XNb!m_a4zjW_jpquw8Hka~Tc#5^+83 zQ^?rxd*qJbP%K_^AWrk#bYGakTTWy^R7{UtjX`fA})Z%nuebnfC_`s=WxmZa_cxb3U*V z2z5qO>NUb>wp>bher1JqBcxF?UTAgwIZft40qVX%?_|?EHhamORBiv{IVj_iuMw&< z_)Ad44%tRo%=?zUu&U_*tQ`a%c&!tqNM?tNrYEckwnk<7SXrtD4{nC2Qz& z9t`frsR|?nIKLOMZA9TnShl8Gb|SNWz7-Prk9Ncr_(IKIQRx)WaK(ui&1M72cQ+8s zqb{WsOk?QL0(bKJmq%8{2(q=fapG%oYRA|HiJz`Sm+ApFETmZ&Fjqh{Q8Hng8d5;D zKnkCIXu>#+y@sfPv8uu9?CRrLaiG%pkWG(yeSJ>li$(8tw?4V*hRAN)6VV=o9Vn$w zvivlt7sQ6}^smwH+n>nrKOWb&hpn!062{QG1(g+O#UtW)yN@l+7lz&D$qZrmbfbB% zk8S{i-&I$I1Nby=>|RF^`@sQi5tBB4xm`9USU00OdG7H)_;lx>WMO0^Ne;1zb6-6sq zs6-iZbuJIWULXF&ULD;mjlR*=F;$>7kO$2RQcFBl9m}%Sf>%jjQ@j_cp^e6zRMp5d z%xt)inOapp=`)|f^fs2mj7z${RQoe;3a>-(8!0=xG91F$7h3F87(3~1xG$3OQhL~0 zb!-$(f?rs@>PJ+Kr9Kz2@GQG3{W#z^lFXP54;@g5(ZHGWuHHJ;Dv$IeDgR?&jF_1y za$=q1wv1xz1ecU8WSRdND^PldtzY649bNReMr+kjMyaFp;c`DAK2yXKu3I&k?4IC& z;YML9ylHR@$|dr{W7<&NB$X=qe!S?qd|I`qBo>jglT=eJu$3hShvE}R(`^aiV$zBq zWI>NmZ5P~mH?es{{#2U{Z^LEnvSa*|M%$Pcp0Id}xCGMhl!{OUR{QF*U_Cr< zgu9}C8auZbW?TKW-`_e}FGVsT85SpKLSP77IrKT#%zWP{g`^;P5nGK(cM*Av4ep-P zHBcN*LfW-c<6YMt&0ID$g7AGEVui%Kv?={AEt#5Chta>EKn{gneQ(ED>Zga`3mu8&oY597X)-F`CpcSjvRu#R7w?K&W z8*-}sj7JwwIYYfc(G~M?>KNU!dLMMkfhxM-dA|ygI8(TH-0A(R2zzgXdp8WP<0p%B zbx3P5@>ocYVB3wYnk$=-R$XS_=5R9HCKjy{kGPjCNr0CU7xGMR*T23}!V;bwD7LdH zd^RXUXR5=#`Mgs3ofm>?z1I|0>*)p?LxnRuO6Ao4jMo9VbgNqIZ387Z=izuhA{gY~ zcuRWj6U)O}#haQIxS)$h2>JOHSu&UU{E8A*xQbmE{p#_P+F3j{zK^>^ijtU9w*#;~ z)js>4S(X(BGb|SY6c2cgu5I}^KXly;&kw;6@-lvYoeW&o^$)or;V|KUMA+C=%5TJ1 zl5yKZyy$J{@TKuWDLF@ie*pdJ>|{$oTPN3#{YZt~gfAllBcz}TL#{+++X>&p&*fxj z!}=gNS;B<~D%NeF^_fej^O6lbLo3CH`1yX4&%_o{0iWxUy@sS|()MI2*c^<%QaTQQ zK^_0<=tPvWDCKe9TOYB0c|$6cLO@Mx5<8dR!~aw-ro}VoevJ;I%O^voTe$yC{0waj z6zpwmf4_=3IXfEm5pwI>7m})IkTm_fnVzlf-2yXJ5yJXdkQ?_@aqGD4lQVE()H|cC zH+->>7AgfD*sPG^sl=5c44b_j{XY#(k61^ow6`xO)B#4XDNw8S;nJLpD^q7?%$U3cS%VX*$QEZ&WKE7#~Xx0mX;hHM~F`CJ!mC0m2#> z_k#G0=1<{MV~;ZRGtdW^rX9c3#vUoe!xI*(t4%E2kN14`5h2jdn4mPK5`WlSBu=dV z^UY%3?5iDawAaoLSVbxq;&{HwwLYp(1B^10H37mCrF!Vx0v&bdr9jCZTV5|Vxk|%< zS)-zu;67m{!NoSVXTVs}Q4@MtsR_yk0*vvzUNHONaf?=AV$`QO2Jgus*+r*+iK%Ak~64B->+l=Y|e&Ra=RSFJ|V7s+rWXZk_9b4-l@)H zNgWX&X~MvxwD3VU%Zqv^Z0C{H(>xrHkDuiI#Sji>KZP0iT1ztGAT-^3$Aqm~39}MQ zkz1r^DJjXOCc~;REwAbf4^W`ZfQd{i5Bw?}zCeF#BHEoo`#2~vNH|;4UdhLU^TBqV z+}4pC{-w$B>f%iv0jElxRy-771z^|)Ex8<4BxI_%oszbqL5%fQ!EuueIp)?A4CUwL z4omiSAxIIsJ=NX*?frEg5k&$TFI|(5jhK>?coWB?fiSeS-66KSE^vo+N$%IrSKF<@ zcp^{rXw;iqDULk79qMZ-BBt3(F+Tjn4}&j^Z_Cer z+w^vSM?w(`aRw$N#ZbRj!e6hDN5(#vCt!u_bzft>-7|E-xKGs!pH~@gpBF8*C{Sj? zt#XxJ#*OAJSV8di!B-W{t81a7LD#nv@`GXb0Bo%jUz&$wB8uh`={r{^ zzFlpw)ol=Xgnf+n)D-{2zK24pc)C2A7*f(~ZfdAm7VimO%Driq16ep6yfV)5m1U^- zw!F@|6F*%G-tF8ieafT5d((IlWI@#OxF_a^;!VoxN@H}ZW96*ad-BRSXv@sDUYL)A zg5LO;#=V$`HJILiwTJRCe4fw1&4Kf=Nx53%bq6D2cU`LcqTK_5P0<^q`qC`mEa zJ%l1e7oR3jc89kag6)dH{EG;Z$;DQ@LgrfBtF8-*|k;J)QO QIutq-;O=aQ{jT}{0Wy{`!~g&Q literal 0 HcmV?d00001 diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts index ac9d39e3d0e..7b21272eee0 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts @@ -1,142 +1,171 @@ import { CaseCountsAndSessionsByCity } from './getDataForCalendaring'; -import { SESSION_TYPES } from '@shared/business/entities/EntityConstants'; import { writeTrialSessionDataToExcel } from './writeTrialSessionDataToExcel'; import ExcelJS from 'exceljs'; +import mockCaseCountsAndSessionsByCity from '@shared/test/mockCaseCountsAndSessionsByCity.json'; +import mockIncorrectSizeRegularCases from '@shared/test/mockIncorrectlySizedCases.json'; +import path from 'path'; -const cityWithSpecialSession = 'Portland, OR'; -const cities = [ - 'cityA, AB', - 'cityB, AB', - 'cityC, AB', - 'cityD, AB', - 'cityE, AB', - cityWithSpecialSession, +const mockUserMessages = [ + 'More than two special trial sessions per week: Washington, District of Columbia 2/10. \n', + 'More than one special trial per week scheduled: Atlanta, Georgia, 3/3. \n', + 'More special sessions than maximum allowed per location scheduled: Atlanta, Georgia. \n', +]; + +const mockWeeks = [ + '2024-12-30', + '2025-01-06', + '2025-01-13', + '2025-01-20', + '2025-01-27', + '2025-02-03', + '2025-02-10', + '2025-02-17', + '2025-02-24', + '2025-03-03', + '2025-03-10', + '2025-03-17', + '2025-03-24', + '2025-03-31', ]; -const weeks = ['09/01', '09/08', '09/15', '09/45', '09/89', '09/37']; describe('writeTrialSessionDataToExcel', () => { - it('should produce a vaguely valid xlsx file', async () => { - let mockCaseCountsAndSessionsByCity: CaseCountsAndSessionsByCity = {}; - for (const city of cities) { - for (const week of weeks) { - const randomType = Math.floor(Math.random() * 3); - if (!mockCaseCountsAndSessionsByCity[city]) { - mockCaseCountsAndSessionsByCity[city] = { - initialRegularCases: 0, - initialSmallCases: 0, - prospectiveSessions: [], - remainingRegularCases: 0, - remainingSmallCases: 0, - scheduledSessions: [], - }; - } - - const sessionType = - city === cityWithSpecialSession - ? SESSION_TYPES.special - : SESSION_TYPES[Object.keys(SESSION_TYPES)[randomType]]; - - mockCaseCountsAndSessionsByCity[city].scheduledSessions.push({ - sessionType, - trialLocation: city, - weekOf: week, - }); - } - } - - await writeTrialSessionDataToExcel({ - caseCountsAndSessionsByCity: mockCaseCountsAndSessionsByCity, - incorrectSizeRegularCases: [], - userMessages: [], - weeks, - }); - }); + it('generates an XLSX file that matches the expected fixture', async () => { + // Arrange + const filename = path.join( + process.cwd(), + 'shared/src/test/mockTermSpreadsheet.xlsx', + ); - it('should handle data that produces empty cells gracefully', async () => { - let mockCaseCountsAndSessionsByCity: CaseCountsAndSessionsByCity = {}; - let counter = 1; - for (const city of cities) { - for (const week of weeks) { - counter++; - if (counter % 4 !== 0) { - const randomType = Math.floor(Math.random() * 3); - if (!mockCaseCountsAndSessionsByCity[city]) { - mockCaseCountsAndSessionsByCity[city] = { - initialRegularCases: 0, - initialSmallCases: 0, - prospectiveSessions: [], - remainingRegularCases: 0, - remainingSmallCases: 0, - scheduledSessions: [], - }; - mockCaseCountsAndSessionsByCity[city].scheduledSessions = []; - } - mockCaseCountsAndSessionsByCity[city].scheduledSessions.push({ - sessionType: SESSION_TYPES[Object.keys(SESSION_TYPES)[randomType]], - trialLocation: city, - weekOf: week, - }); - } - } - } - - await writeTrialSessionDataToExcel({ - caseCountsAndSessionsByCity: mockCaseCountsAndSessionsByCity, - incorrectSizeRegularCases: [], - userMessages: [], - weeks, + const expectedWorkbook = new ExcelJS.Workbook(); + await expectedWorkbook.xlsx.readFile(filename); + const expectedSuggestedSessionCalendarWorksheet = + expectedWorkbook.getWorksheet('Suggested Session Calendar'); + const expectedIncorrectlySizedCases = expectedWorkbook.getWorksheet( + 'Incorrectly Sized Cases', + ); + const expectedWarnings = expectedWorkbook.getWorksheet('Warnings'); + + // Act + const buffer = await writeTrialSessionDataToExcel({ + caseCountsAndSessionsByCity: + mockCaseCountsAndSessionsByCity as CaseCountsAndSessionsByCity, + incorrectSizeRegularCases: mockIncorrectSizeRegularCases, + userMessages: mockUserMessages, + weeks: mockWeeks, }); - }); - // 10275 TODO: consider writing tests that open xlsx file, inspects worksheets and so on - it('tk', async () => { - // Arrange - let mockCaseCountsAndSessionsByCity: CaseCountsAndSessionsByCity = {}; - for (const city of cities) { - for (const week of weeks) { - const randomType = Math.floor(Math.random() * 3); - if (!mockCaseCountsAndSessionsByCity[city]) { - mockCaseCountsAndSessionsByCity[city] = { - initialRegularCases: 0, - initialSmallCases: 0, - prospectiveSessions: [], - remainingRegularCases: 0, - remainingSmallCases: 0, - scheduledSessions: [], - }; - } - - const sessionType = - city === cityWithSpecialSession - ? SESSION_TYPES.special - : SESSION_TYPES[Object.keys(SESSION_TYPES)[randomType]]; - - mockCaseCountsAndSessionsByCity[city].scheduledSessions.push({ - sessionType, - trialLocation: city, - weekOf: week, - }); - } - } + const actualWorkbook = new ExcelJS.Workbook(); + await actualWorkbook.xlsx.load(buffer); + const actualSuggestedSessionCalendarWorksheet = actualWorkbook.getWorksheet( + 'Suggested Session Calendar', + ); + const actualIncorrectlySizedCases = actualWorkbook.getWorksheet( + 'Incorrectly Sized Cases', + ); + const actualWarnings = actualWorkbook.getWorksheet('Warnings'); - // Act + // Assert + compareWorksheets( + actualSuggestedSessionCalendarWorksheet!, + expectedSuggestedSessionCalendarWorksheet!, + ); + + compareWorksheets( + actualIncorrectlySizedCases!, + expectedIncorrectlySizedCases!, + ); + + compareWorksheets(actualWarnings!, expectedWarnings!); + }); + + it('generates an XLSX file with only one worksheet when no warnings or incorrectly sized cases are passed', async () => { const buffer = await writeTrialSessionDataToExcel({ - caseCountsAndSessionsByCity: mockCaseCountsAndSessionsByCity, + caseCountsAndSessionsByCity: { + cityA: { + initialRegularCases: 0, + initialSmallCases: 0, + prospectiveSessions: [], + remainingRegularCases: 0, + remainingSmallCases: 0, + scheduledSessions: [], + }, + } as CaseCountsAndSessionsByCity, incorrectSizeRegularCases: [], userMessages: [], - weeks, + weeks: ['01/01'], }); - const workbook = new ExcelJS.Workbook(); - await workbook.xlsx.load(buffer); - const worksheet = workbook.getWorksheet('Suggested Session Calendar'); + const actualWorkbook = new ExcelJS.Workbook(); + await actualWorkbook.xlsx.load(buffer); - // Assert - expect(worksheet!.getCell('A2').text).toEqual('City'); - // 10275 TODO: possible paths for testing: - // - programmatically examine the worksheet and assert - // that everything looks good. - // - check an actual xlsx file into the repo and compare - // the result of this test against that fixture. + const incorrectlySizedCasesWorksheet = actualWorkbook.getWorksheet( + 'Incorrectly Sized Cases', + ); + const warningsWorksheet = actualWorkbook.getWorksheet('Warnings'); + + expect(incorrectlySizedCasesWorksheet).toBeFalsy(); + expect(warningsWorksheet).toBeFalsy(); }); }); + +const compareWorksheets = ( + worksheetOne: ExcelJS.Worksheet, + worksheetTwo: ExcelJS.Worksheet, +) => { + /** + * ExcelJS doesn't even instantiate a cell object if not given data to fill + * the cell. This means, when looping over each cell in a row, if A1 is empty + * but B1 has data, then row 1 starts with "B1", skipping over A1. + * + * Because of this, we first call `eachRow().eachCell()` on the generated worksheet, + * comparing against the expected worksheet. Then, we swap, looping over the + * expected worksheet and comparing against the actual worksheet. + * + * This ensures that when the actual worksheet contains a cell that the + * expected worksheet does not, or when the actual worksheet is missing a cell + * that the expected worksheet has, the test appropriately detects the + * mismatched cell and will fail. + */ + compareWorksheetCells(worksheetOne, worksheetTwo); + compareWorksheetCells(worksheetTwo, worksheetOne); +}; + +const compareWorksheetCells = ( + worksheetOne: ExcelJS.Worksheet, + worksheetTwo: ExcelJS.Worksheet, +) => { + worksheetOne!.eachRow((row, rowIndex) => { + row.eachCell((cell, colIndex) => { + const { + alignment: worksheetOneAlignment, + border: worksheetOneBorder, + fill: worksheetOneFill, + font: worksheetOneFont, + formula: worksheetOneFormula, + numFmt: worksheetOneNumFmt, + text: worksheetOneText, + value: worksheetOneValue, + } = cell; + + const { + alignment: worksheetTwoAlignment, + border: worksheetTwoBorder, + fill: worksheetTwoFill, + font: worksheetTwoFont, + formula: worksheetTwoFormula, + numFmt: worksheetTwoNumFmt, + text: worksheetTwoText, + value: worksheetTwoValue, + } = worksheetTwo!.getCell(rowIndex, colIndex); + + expect(worksheetOneText).toEqual(worksheetTwoText); + expect(worksheetOneValue).toEqual(worksheetTwoValue); + expect(worksheetOneFill).toEqual(worksheetTwoFill); + expect(worksheetOneFormula).toEqual(worksheetTwoFormula); + expect(worksheetOneBorder).toEqual(worksheetTwoBorder); + expect(worksheetOneFont).toEqual(worksheetTwoFont); + expect(worksheetOneAlignment).toEqual(worksheetTwoAlignment); + expect(worksheetOneNumFmt).toEqual(worksheetTwoNumFmt); + }); + }); +}; From 6796d0e41a6c55b0b037ef17a68de0ebf9bc0ec3 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 18 Nov 2024 14:45:07 -0500 Subject: [PATCH 207/208] 10275: make minor formatting change to writeTrialSessionDataToExcel --- .../writeTrialSessionDataToExcel.test.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts index 7b21272eee0..4135d7aeef8 100644 --- a/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts +++ b/web-api/src/business/useCaseHelper/trialSessions/trialSessionCalendaring/writeTrialSessionDataToExcel.test.ts @@ -79,20 +79,27 @@ describe('writeTrialSessionDataToExcel', () => { }); it('generates an XLSX file with only one worksheet when no warnings or incorrectly sized cases are passed', async () => { + // Arrange + const caseCountsAndSessionsByCity = { + cityA: { + initialRegularCases: 0, + initialSmallCases: 0, + prospectiveSessions: [], + remainingRegularCases: 0, + remainingSmallCases: 0, + scheduledSessions: [], + }, + } as CaseCountsAndSessionsByCity; + const incorrectSizeRegularCases = []; + const userMessages = []; + const weeks = ['01/01']; + + // Act const buffer = await writeTrialSessionDataToExcel({ - caseCountsAndSessionsByCity: { - cityA: { - initialRegularCases: 0, - initialSmallCases: 0, - prospectiveSessions: [], - remainingRegularCases: 0, - remainingSmallCases: 0, - scheduledSessions: [], - }, - } as CaseCountsAndSessionsByCity, - incorrectSizeRegularCases: [], - userMessages: [], - weeks: ['01/01'], + caseCountsAndSessionsByCity, + incorrectSizeRegularCases, + userMessages, + weeks, }); const actualWorkbook = new ExcelJS.Workbook(); @@ -103,6 +110,7 @@ describe('writeTrialSessionDataToExcel', () => { ); const warningsWorksheet = actualWorkbook.getWorksheet('Warnings'); + // Assert expect(incorrectlySizedCasesWorksheet).toBeFalsy(); expect(warningsWorksheet).toBeFalsy(); }); From b0c005cf67df3a6f7fe81992eb8a79924c012fb5 Mon Sep 17 00:00:00 2001 From: TomElliottFlexion <66225176+TomElliottFlexion@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:19:47 -0600 Subject: [PATCH 208/208] 10275: update alert wording/grammar --- shared/src/business/entities/EntityConstants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/business/entities/EntityConstants.ts b/shared/src/business/entities/EntityConstants.ts index f12fa17ae3e..12cb28a93f3 100644 --- a/shared/src/business/entities/EntityConstants.ts +++ b/shared/src/business/entities/EntityConstants.ts @@ -208,9 +208,9 @@ export const CLOSED_CASE_STATUSES = [ CASE_STATUS_TYPES.closedDismissed, ]; export const SUGGESTED_TRIAL_SESSION_TITLES = { - invalid: 'Unable to generate suggested term.', + invalid: 'Unable to create term', success: 'Successfully generated suggested term.', - warning: 'Successfully generated suggested term with warnings.', + warning: 'Successfully generated suggested term with warnings', }; export const DOCUMENT_RELATIONSHIPS = {