Skip to content

Commit

Permalink
Merge pull request #178 from OperationSpark/update-facebook-instant-form
Browse files Browse the repository at this point in the history
remove "day" from form
  • Loading branch information
ptbarnum4 authored Oct 8, 2024
2 parents febf1df + 3883e5b commit 3f4deb7
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 139 deletions.
4 changes: 2 additions & 2 deletions pages/api/infoSession/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getConfig } from '@this/config';
import { GooglePlace, LocationType } from '@this/types/signups';

export type DateTime = {
dateTime: string;
dateTime: string | Date;
timeZone: 'America/Chicago';
};

Expand All @@ -18,7 +18,7 @@ export interface ISessionDates {
times: {
start: DateTime;
end: DateTime;
until: string;
until: string | Date;
byday: 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
};
googlePlace: GooglePlace;
Expand Down
2 changes: 1 addition & 1 deletion src/Forms/Form.Workforce.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const WorkforceForm = ({ sessionDates, referredBy }: WorkforceFormProps) => {
cohort: session.cohort,
programId: session.programId,
code: session.code,
startDateTime: session.times.start.dateTime,
startDateTime: session.times.start.dateTime as string,
googlePlace: session.googlePlace,
locationType: session.locationType,
},
Expand Down
126 changes: 63 additions & 63 deletions src/api/helpers/facebookWebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import axios, { isAxiosError } from 'axios';

import { getInfoSessionDates, ISessionDates } from '@this/pages-api/infoSession/dates';
import { parsePhoneNumber } from '@this/src/components/Form/helpers';
import { toDayJs } from '@this/src/helpers/time';
import { getStateFromZipCode } from '@this/src/helpers/zipLookup';
import { Req, ReqMiddleware } from '@this/types/request';
import { FormDataSignup, ISession, ISessionSignup } from '@this/types/signups';
Expand Down Expand Up @@ -223,100 +224,99 @@ export const formatSessionObject = (session: ISessionDates): ISession => {
id: session._id,
programId: session.programId,
cohort: session.cohort,
startDateTime: session.times.start.dateTime,
startDateTime: session.times.start.dateTime as string,
locationType: session.locationType,
googlePlace: session.googlePlace,
code: session.code,
};
};

const extractTimeInfo = (time?: string) => {
if (!time)
return {
hours: 5,
minutes: 30,
amPm: 'PM',
};
const [hour, minuteInfo] = time.split(':');
const hours = Number(hour);
const minutes = Number(minuteInfo?.replaceAll(/\D/g, ''));
const amPm = minuteInfo?.replaceAll(/\d/g, '')?.toLowerCase() ?? 'pm';
return {
hours: isNaN(hours) ? 5 : hours,
minutes: isNaN(minutes) ? 30 : minutes,
amPm,
};
};

const createCohortTimes = (time?: string) => {
const { hours, minutes, amPm } = extractTimeInfo(time);
const getDateFromTime = (time: Date | string, tz?: string) => {
if (time instanceof Date) {
if (tz) {
return toDayJs(time).tz(tz);
}
return toDayJs(time);
}
const [hour, minute] = time.split(':').map((v) => {
const n = parseInt(v);
if (/PM/gi.test(v) && n < 12) {
return n + 12;
}

const mins = minutes || 30;
if (/AM/gi.test(v) && n === 12) {
return 0;
}

const flip = hours === 12;
return n;
});
const d = toDayJs().tz('America/Chicago').hour(hour).minute(minute).second(0).millisecond(0);

return [
`${hours - 1}${flip ? 'am' : amPm}`,
`${hours - 1}-${mins}${flip ? 'am' : amPm}`,
`${hours}${amPm}`,
`${hours}-${mins}${amPm}`,
`${flip ? 1 : hours + 1}${amPm}`,
`${flip ? 1 : hours + 1}-${mins}${amPm}`,
];
if (!d.isValid()) {
return;
}
if (tz) {
return d.tz(tz);
}
return d;
};

/**
* Find the best session based on the day and time provided
* - If no day or time is provided, return the next session
* - If no session is found by day and time, try again with only time, then only day
* @param time - The time to find the best session for (`'12:34AM/PM'`)
*/
export const findBestSession = (
sessions: ISessionDates[],
day?: string,
time?: string,
time?: Date | `${number}:${number}${'AM' | 'PM'}` | string,
): ISession | undefined => {
if (!sessions.length) {
if (!sessions?.length) {
return;
}
const [nextSession] = sessions;

if (!day && !time) {
return formatSessionObject(sessions[0]);
const selectedDate =
time && getDateFromTime(time, nextSession.times?.start?.timeZone || 'America/Chicago');
if (!selectedDate || sessions.length === 1) {
return formatSessionObject(nextSession);
}

const d = day?.toLowerCase() === 'tuesday' ? 'TU' : 'TH';
const selectedHour = selectedDate.hour();

const selectedDay = day ? d : undefined;
const session = sessions.reduce<(ISessionDates & { diff: number }) | null>((best, s) => {
const { dateTime, timeZone } = s.times.start ?? {};
const startTime = toDayJs(dateTime).tz(timeZone);
const diff = startTime.diff(selectedDate, 'minute');
const startHour = startTime.hour();

const cohortTimes = !!time && createCohortTimes(time);
const newSession = { ...s, diff };

const session = sessions.find((s) => {
const byDay = s.times.byday;
const cohort = s.cohort;
if (selectedDay && cohortTimes) {
return byDay === selectedDay && cohortTimes.some((t) => cohort.endsWith(t));
//! ↓ ↓ ↓ Order Matters ↓ ↓ ↓
if (!best && diff >= 0) {
return newSession;
}
if (cohortTimes) {
return cohortTimes.some((t) => cohort.endsWith(t));
if (!best) {
return null;
}
if (selectedDay) {
return byDay === selectedDay;
// Perfect match
if (best.diff === 0) {
return best;
}
});

if (session) return formatSessionObject(session);

if (!day || !time) {
// If no day or time is provided, return so we can try again with only time or day below (this will only hit for the recursive call, which may not be the final return)
return;
}
// If not exact match, check if session hour matches selected hour
if (selectedHour === startHour) {
return newSession;
}
// If the time is closer than the current best session and in the future
if (diff >= 0 && diff < best.diff) {
return best;
}
//! ↑ ↑ ↑ Order Matters ↑ ↑ ↑

// If no session is found by day and time. Try again with only time
const bestTime = findBestSession(sessions, undefined, time);
if (bestTime) return bestTime;
return best;
}, null);

// If no session is found by time, try again with only day
const bestDay = findBestSession(sessions, day);
if (bestDay) return bestDay;
if (session) return formatSessionObject(session);

// If no session is found by day or time, return the next session
return formatSessionObject(sessions[0]);
Expand All @@ -337,7 +337,7 @@ export const formatFacebookPayload = async (
// Change to 'true' if we want to opt in by default
const smsOptIn = 'false' as const;

const session = findBestSession(sessions, fields.day, fields.time);
const session = findBestSession(sessions, fields.time);
const [fullFirstName, ...names] = fields.name.split(' ');
const firstName = stripSpecialChars(fullFirstName);
const lastName = stripSpecialChars(names.pop() ?? '');
Expand Down
94 changes: 28 additions & 66 deletions tests/api/facebookWebhook.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { describe, expect, test } from '@jest/globals';
import { findBestSession } from '@this/src/api/helpers/facebookWebhook';

import { createSessionDates, toSessionObj } from '../support/facebookWebhook';
import { ISessionDates } from '@this/pages-api/infoSession/dates';
import { createSessionDates, setCentralTime, toSessionObj } from '../support/facebookWebhook';

describe('findBestSession', () => {
const tues1230pm = createSessionDates('TU', '12:30PM');
const tues1200pm = createSessionDates('TU', '12:00PM');
const tues530pm = createSessionDates('TU', '5:30PM');
const thurs1230pm = createSessionDates('TH', '12:30PM');
const thurs530pm = createSessionDates('TH', '5:30PM');
const nextTues1200pm = createSessionDates('TU', '12:00PM', 1);
const nextTues530pm = createSessionDates('TU', '5:30PM', 1);

const allSessions = [tues1230pm, tues530pm, thurs1230pm, thurs530pm];
const allSessions = [tues1200pm, tues530pm, nextTues1200pm, nextTues530pm];
const nextWeekSessions = [nextTues1200pm, nextTues530pm];

describe('Find exact match', () => {
const testCases = [
['Tuesday', '12:30PM', tues1230pm],
['Tuesday', '5:30PM', tues530pm],
['Thursday', '12:30PM', thurs1230pm],
['Thursday', '5:30PM', thurs530pm],
[createTime(tues1200pm, 12, 0), tues1200pm, allSessions],
[createTime(tues530pm, 17, 30), tues530pm, allSessions],
[createTime(nextTues1200pm, 12, 0), nextTues1200pm, nextWeekSessions],
[createTime(nextTues530pm, 17, 30), nextTues530pm, nextWeekSessions],
] as const;

testCases.forEach(([day, time, initialSession]) => {
test(`should return correct session for "${day} at ${time}"`, () => {
const got = findBestSession(allSessions, day, time);
testCases.forEach(([time, initialSession, sessions]) => {
test(`should return correct session for time: "${time}"`, () => {
const got = findBestSession(sessions, time);
const expected = toSessionObj(initialSession);
expect(got).toEqual(expected);
});
Expand All @@ -31,68 +33,28 @@ describe('findBestSession', () => {
describe('Find best match', () => {
test('should return next session if no day or time provided', () => {
const got = findBestSession(allSessions);
expect(got).toEqual(toSessionObj(tues1230pm));
});

test('should return best day if no time matches', () => {
const sessions = [tues1230pm, thurs1230pm];
const got = findBestSession(sessions, 'Tuesday', '5:30PM');
const expected = toSessionObj(tues1230pm);
expect(got).toEqual(expected);
});

test('should return best time if no day matches', () => {
const sessions = [thurs1230pm, thurs530pm];
const got = findBestSession(sessions, 'Tuesday', '5:30PM');
const expected = toSessionObj(thurs530pm);
expect(got).toEqual(expected);
});

test('should return best time on different day if time of selected day is unavailable', () => {
const sessions = [tues1230pm, thurs1230pm, thurs530pm];
const got = findBestSession(sessions, 'Tuesday', '5:30PM');
const expected = toSessionObj(thurs530pm);
expect(got).toEqual(expected);
expect(got).toEqual(toSessionObj(tues1200pm));
});

test('should return undefined if no sessions', () => {
const got = findBestSession([], 'Tuesday', '5:30PM');
const time = createTime(tues1200pm, 17, 30);
const got = findBestSession([], time);
expect(got).toBeUndefined();
});

test('should find afternoon session with time that is under by 30 minutes', () => {
const got = findBestSession(allSessions, 'Tuesday', '12:00PM');
const expected = toSessionObj(tues1230pm);
expect(got).toEqual(expected);
});
test('should find afternoon session with time that is under by 1 hour', () => {
const got = findBestSession(allSessions, 'Tuesday', '11:30AM');
const expected = toSessionObj(tues1230pm);
expect(got).toEqual(expected);
});

test('should find evening session with time that is under by 30 minutes', () => {
const got = findBestSession(allSessions, 'Tuesday', '5:00PM');
const expected = toSessionObj(tues530pm);
expect(got).toEqual(expected);
});

test('should find evening session with time that is under by 1 hour', () => {
const got = findBestSession(allSessions, 'Tuesday', '4:30PM');
const expected = toSessionObj(tues530pm);
expect(got).toEqual(expected);
});

test('should find evening session with time that is over by 30 minutes', () => {
const got = findBestSession(allSessions, 'Tuesday', '6:00PM');
const expected = toSessionObj(tues530pm);
expect(got).toEqual(expected);
test('should return next session if no day or time provided', () => {
const got = findBestSession(allSessions);
expect(got).toEqual(toSessionObj(tues1200pm));
});

test('should find evening session with time that is over by 1 hour', () => {
const got = findBestSession(allSessions, 'Tuesday', '6:30PM');
const expected = toSessionObj(tues530pm);
expect(got).toEqual(expected);
test('should return correct time if next session is in future, but before best time (find 5:30pm next week NOT 12pm next week)', () => {
const time = createTime(tues530pm, 17, 30);
const got = findBestSession(nextWeekSessions, time);
expect(got).toEqual(toSessionObj(nextTues530pm));
});
});
});

function createTime(session: ISessionDates, hours: number, minutes: number, weeks?: number) {
return setCentralTime(session.times.start.dateTime, hours, minutes, weeks);
}
Loading

0 comments on commit 3f4deb7

Please sign in to comment.