Skip to content

Commit bf63bdb

Browse files
committed
feat: surface partial sign-ups when pairing session closes unfulfilled
When a pairing session times out without filling all slots, include any teammates who did sign up (grouped by slot) so recruiters can reach out directly. Also extracts a small finalize helper to remove duplicated reply + remove + releaseLock across both close branches.
1 parent f2c1139 commit bf63bdb

2 files changed

Lines changed: 75 additions & 16 deletions

File tree

src/services/PairingSessionCloser.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import { pairingSessionsRepo } from '@repos/pairingSessionsRepo';
44
import { userRepo } from '@repos/userRepo';
55
import { chatService } from '@/services/ChatService';
66
import { App } from '@slack/bolt';
7+
import { WebClient } from '@/slackTypes';
78
import { formatSlot, mention, ul } from '@utils/text';
89
import { reviewLockManager } from '@utils/reviewLockManager';
910
import log from '@utils/log';
1011

12+
async function finalize(client: WebClient, threadId: string, message: string): Promise<void> {
13+
await chatService.replyToReviewThread(client, threadId, message);
14+
await pairingSessionsRepo.remove(threadId);
15+
reviewLockManager.releaseLock(threadId);
16+
}
17+
1118
export function findConfirmedSlots(interview: PairingSession): PairingSlot[] {
1219
return interview.slots.filter(slot =>
1320
isSlotConfirmed(slot, interview.format, interview.teammatesNeededCount),
@@ -51,29 +58,36 @@ export const pairingSessionCloser = {
5158
).values(),
5259
);
5360
const slotLines = confirmedSlots.map(s => formatSlot(s.date, s.startTime, s.endTime));
54-
await chatService.replyToReviewThread(
55-
app.client,
56-
threadId,
61+
const message =
5762
`${mention({ id: interview.requestorId })} Pairing session for *${interview.candidateName}* is ready to schedule!\n\n` +
58-
`*Teammates:* ${teammates.map(t => mention({ id: t.userId })).join(', ')}\n\n` +
59-
`*Available slots (${confirmedSlots.length}):*\n${ul(...slotLines)}`,
60-
);
63+
`*Teammates:* ${teammates.map(t => mention({ id: t.userId })).join(', ')}\n\n` +
64+
`*Available slots (${confirmedSlots.length}):*\n${ul(...slotLines)}`;
6165
await Promise.all(teammates.map(t => userRepo.markNowAsLastPairingReviewedDate(t.userId)));
62-
await pairingSessionsRepo.remove(threadId);
63-
reviewLockManager.releaseLock(threadId);
66+
await finalize(app.client, threadId, message);
6467
return true;
6568
}
6669

6770
const isUnfulfilled = interview.pendingTeammates.length === 0;
6871

6972
if (isUnfulfilled) {
70-
await chatService.replyToReviewThread(
71-
app.client,
72-
threadId,
73-
`${mention({ id: interview.requestorId })} No teammates available to cover all slots for ${interview.candidateName}'s pairing session.`,
74-
);
75-
await pairingSessionsRepo.remove(threadId);
76-
reviewLockManager.releaseLock(threadId);
73+
const slotsWithInterest = interview.slots.filter(s => s.interestedTeammates.length > 0);
74+
const header = `${mention({ id: interview.requestorId })} Couldn't fill all slots for *${interview.candidateName}*'s pairing session.`;
75+
76+
const message =
77+
slotsWithInterest.length > 0
78+
? `${header}\n\n` +
79+
`Some teammates did sign up — you may be able to reach out to them directly:\n` +
80+
ul(
81+
...slotsWithInterest.map(
82+
s =>
83+
`${formatSlot(s.date, s.startTime, s.endTime)}${s.interestedTeammates
84+
.map(t => mention({ id: t.userId }))
85+
.join(', ')}`,
86+
),
87+
)
88+
: `${header} No teammates signed up.`;
89+
90+
await finalize(app.client, threadId, message);
7791
return true;
7892
}
7993

src/services/__tests__/PairingSessionCloser.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,57 @@ describe('PairingSessionCloser', () => {
194194
expect(chatService.replyToReviewThread).toHaveBeenCalledWith(
195195
app.client,
196196
'thread-1',
197-
expect.stringContaining('No teammates available'),
197+
expect.stringContaining('No teammates signed up'),
198198
);
199199
expect(pairingSessionsRepo.remove).toHaveBeenCalledWith('thread-1');
200200
expect(reviewLockManager.releaseLock).toHaveBeenCalledWith('thread-1');
201201
});
202202

203+
it('should list partial sign-ups when unfulfilled but some teammates were interested', async () => {
204+
const slot1 = makeSlot({
205+
id: 'slot-1',
206+
date: '2026-03-31',
207+
startTime: '13:00',
208+
endTime: '15:00',
209+
interestedTeammates: [{ userId: 'u1', acceptedAt: 1, formats: [InterviewFormat.REMOTE] }],
210+
});
211+
const slot2 = makeSlot({
212+
id: 'slot-2',
213+
date: '2026-04-01',
214+
startTime: '10:00',
215+
endTime: '12:00',
216+
interestedTeammates: [],
217+
});
218+
const slot3 = makeSlot({
219+
id: 'slot-3',
220+
date: '2026-04-02',
221+
startTime: '14:00',
222+
endTime: '16:00',
223+
interestedTeammates: [
224+
{ userId: 'u2', acceptedAt: 2, formats: [InterviewFormat.REMOTE] },
225+
{ userId: 'u3', acceptedAt: 3, formats: [InterviewFormat.REMOTE] },
226+
],
227+
});
228+
const interview = makeInterview({
229+
format: InterviewFormat.HYBRID,
230+
pendingTeammates: [],
231+
slots: [slot1, slot2, slot3],
232+
});
233+
pairingSessionsRepo.getByThreadIdOrUndefined = jest.fn().mockResolvedValue(interview);
234+
235+
await pairingSessionCloser.closeIfComplete(app, 'thread-1');
236+
237+
const message = (chatService.replyToReviewThread as jest.Mock).mock.calls[0][2];
238+
expect(message).toContain("Couldn't fill all slots");
239+
expect(message).toContain('Some teammates did sign up');
240+
expect(message).toContain('<@u1>');
241+
expect(message).toContain('<@u2>');
242+
expect(message).toContain('<@u3>');
243+
expect(message).not.toContain('Apr 01');
244+
expect(pairingSessionsRepo.remove).toHaveBeenCalledWith('thread-1');
245+
expect(reviewLockManager.releaseLock).toHaveBeenCalledWith('thread-1');
246+
});
247+
203248
it('should handle a concurrently-closed interview gracefully', async () => {
204249
pairingSessionsRepo.getByThreadIdOrUndefined = jest.fn().mockResolvedValue(undefined);
205250

0 commit comments

Comments
 (0)