Skip to content

[INTW26] Build Interview Delegation Algorithm + Interview Group CRUD Endpoints - #127

Merged
mxc-maggiechen merged 3 commits into
mainfrom
INTW26-build-interview-delegation-algorithm
May 11, 2026
Merged

[INTW26] Build Interview Delegation Algorithm + Interview Group CRUD Endpoints#127
mxc-maggiechen merged 3 commits into
mainfrom
INTW26-build-interview-delegation-algorithm

Conversation

@chene0

@chene0 chene0 commented Mar 8, 2026

Copy link
Copy Markdown
Member

Notion ticket link

Build Interview Delegation Algorithm

Implementation description

  • Add service functions, resolvers, and graphql types to implement crud endpoints for interview groups
  • Add service functions, resolvers, and graphql types to implement a delegate interviewers function

Steps to test

Interview Group CRUD

  1. Start the backend with docker compose up
  2. Run the following graphql queries and ensure they run with no errors, as well as the interview_groups table content makes sense after each query
mutation CreateInterviewGroup {
    createInterviewGroup(status: "Availability Pending", schedulingLink: "link") {
        id
        schedulingLink
        status
    }
}

# make sure to note down the id it returns for the next 3 queries
mutation UpdateInterviewGroup {
    updateInterviewGroup(
        id: "<INTERVIEW_GROUP_ID>"
        status: "Ready to Interview"
        schedulingLink: "another_link"
    ) {
        id
        schedulingLink
        status
    }
}
query GetInterviewGroup {
    getInterviewGroup(id: "<INTERVIEW_GROUP_ID>") {
        id
        schedulingLink
        status
    }
}
mutation DeleteInterviewGroup {
    deleteInterviewGroup(id: "<INTERVIEW_GROUP_ID>") {
        id
        schedulingLink
        status
    }
}
mutation BulkCreateInterviewGroups($groups: [BulkCreateInterviewGroupInput!]!) {
    bulkCreateInterviewGroups(groups: $groups) {
        id
        schedulingLink
        status
    }
}

# use the query variables below
 {
    "groups": [
      {
        "status": "Availability Pending"
      },
      {
        "status": "Invites Sent",
        "schedulingLink": "https://calendly.com/interviewer-a"
      },
      {
        "status": "Ready to Interview",
        "schedulingLink": "https://calendly.com/interviewer-b"
      }
    ]
  }

# note the group ids created here for the next query
mutation BulkDeleteInterviewGroups ($ids:[String!]!) {
    bulkDeleteInterviewGroups(ids: $ids) {
        id
        schedulingLink
        status
    }
}

# use the query variables below
{
  "ids": [
    "<INTERVIEW_GROUP_ID_1>",
    "<INTERVIEW_GROUP_ID_2>",
    "<INTERVIEW_GROUP_ID_3>"
  ]
}

Delegation Algorithm

  1. Start the backend with docker compose up
  2. Run the following sql queries separately and in order to create copies of each existing seeded applicant record
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO public.interviewed_applicant_records (id, "applicantRecordId", status, "createdAt", "updatedAt")
SELECT
	gen_random_uuid(),
	id,
	'NeedsReview',
	NOW(),
	NOW()
FROM public.applicant_records
ON CONFLICT ("applicantRecordId") DO NOTHING;
  1. Run the following sql query to create a bunch of dummy users with position "Developer"
INSERT INTO users (first_name, last_name, email, auth_id, role, position, "isActive")
VALUES
  ('Test', 'User1',  'testuser1@test.com',  'test_auth_1',  'User', 'Developer', true),
  ('Test', 'User2',  'testuser2@test.com',  'test_auth_2',  'User', 'Developer', true),
  ('Test', 'User3',  'testuser3@test.com',  'test_auth_3',  'User', 'Developer', true),
  ('Test', 'User4',  'testuser4@test.com',  'test_auth_4',  'User', 'Developer', true),
  ('Test', 'User5',  'testuser5@test.com',  'test_auth_5',  'User', 'Developer', true),
  ('Test', 'User6',  'testuser6@test.com',  'test_auth_6',  'User', 'Developer', true),
  ('Test', 'User7',  'testuser7@test.com',  'test_auth_7',  'User', 'Developer', true),
  ('Test', 'User8',  'testuser8@test.com',  'test_auth_8',  'User', 'Developer', true),
  ('Test', 'User9',  'testuser9@test.com',  'test_auth_9',  'User', 'Developer', true),
  ('Test', 'User10', 'testuser10@test.com', 'test_auth_10', 'User', 'Developer', true),
  ('Test', 'User11', 'testuser11@test.com', 'test_auth_11', 'User', 'Developer', true);
  1. Run the following mutator
mutation DelegateInterviewers {
    delegateInterviewers(positions: ["Developer", "VP Engineering"]) {
        interviewedApplicantRecordId
        interviewerId
        interviewHasConflict
        groupId
    }
}
  1. icl i asked claude to give some queries to test this
-- 1. Each applicant still gets exactly 1 or 2 interviewers (unchanged):
SELECT "interviewedApplicantRecordId", COUNT(*) AS interviewer_count
FROM public.interview_delegations
GROUP BY "interviewedApplicantRecordId"
HAVING COUNT(*) NOT IN (1, 2);
-- Should return no rows

-- 2. Both interviewers for the same applicant share the same group (unchanged):
SELECT "interviewedApplicantRecordId", COUNT(DISTINCT "groupId") AS group_count
FROM public.interview_delegations
GROUP BY "interviewedApplicantRecordId"
HAVING COUNT(DISTINCT "groupId") > 1;
-- Should return no rows

-- 3. No interviewer is ever solo unless the position truly has only 1 interviewer
-- (new — replaces the old "one group per interviewer" check):
SELECT d."interviewerId"
FROM public.interview_delegations d
WHERE d."interviewedApplicantRecordId" IN (
  SELECT "interviewedApplicantRecordId"
  FROM public.interview_delegations
  GROUP BY "interviewedApplicantRecordId"
  HAVING COUNT(*) = 1
)
GROUP BY d."interviewerId";
-- Should only return interviewers from positions with exactly 1 interviewer total.

-- 4. Interviewers spanning multiple groups (proves wrap-around actually occurred for
-- odd counts):
SELECT "interviewerId", COUNT(DISTINCT "groupId") AS group_count
FROM public.interview_delegations
GROUP BY "interviewerId"
HAVING COUNT(DISTINCT "groupId") > 1;
-- For even-count positions → no rows. For odd-count positions → some interviewers
-- should appear here.

-- 5. Load distribution across groups (unchanged):
SELECT "groupId", COUNT(DISTINCT "interviewedApplicantRecordId") AS applicant_count
FROM public.interview_delegations
GROUP BY "groupId"
ORDER BY "groupId";
-- Note that since we delegated for two roles, there's gonna be two pools of count ranges.

-- 6. At most 2 interviewers are assigned per group
SELECT
  "groupId",
  COUNT(DISTINCT "interviewerId") AS interviewer_count
FROM interview_delegations
GROUP BY "groupId"
HAVING COUNT(DISTINCT "interviewerId") > 2;
-- Should return no rows

-- 7. Even distribution among interviewers
WITH interviewer_loads AS (
  SELECT
    d."interviewerId",
    u.position,
    COUNT(DISTINCT d."interviewedApplicantRecordId") AS applicant_count
  FROM public.interview_delegations d
  JOIN public.users u ON u.id = d."interviewerId"
  GROUP BY d."interviewerId", u.position
)
SELECT
  position,
  "interviewerId",
  applicant_count,
  MAX(applicant_count) OVER (PARTITION BY position) - MIN(applicant_count) OVER (PARTITION BY position)
 AS load_spread
FROM interviewer_loads
ORDER BY position, applicant_count DESC;

What should reviewers focus on?

Interview Group CRUD

  • Create and Update accept null schedulingLink
  • If update does not provide a schedulingLink, the row's schedulingLink value should not change

Checklist

  • My PR name is descriptive and in imperative tense
  • My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits
  • I have run the appropriate linter(s)
  • I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR

@chene0
chene0 force-pushed the INTW26-build-interview-delegation-algorithm branch from 1e87ecb to 88f872a Compare March 17, 2026 01:31
@chene0
chene0 force-pushed the INTW26-build-interview-delegation-algorithm branch 2 times, most recently from b2b75ac to 8bce34f Compare March 28, 2026 23:36
@chene0 chene0 changed the title [INTW26] Build Interview Delegation Algorithm [INTW26] Build Interview Delegation Algorithm + Interview Group CRUD Endpoints Mar 31, 2026
Comment thread backend/typescript/models/interviewGroup.model.ts
@chene0
chene0 marked this pull request as ready for review March 31, 2026 22:50
@chene0
chene0 requested a review from mxc-maggiechen March 31, 2026 22:50
@SaqAsh
SaqAsh requested review from SaqAsh and gavxue April 1, 2026 01:33

@SaqAsh SaqAsh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am gonna write up something that cleans a lot of the interviewDashboardService.ts I think @mxc-maggiechen let me know if you like my suggestion better gotta prompt claude to write something that is cleaner for you :)

})
).reduce((map, user) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const pos = user.position!;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe broken type here we don't want to do that

});

interviewedApplicantRecords.forEach((record) => {
/* eslint-disable @typescript-eslint/no-non-null-assertion */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

arr.push(user.id);
map.set(pos, arr);
return map;
}, new Map<string, number[]>());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally, native Map type in javascript is just harder to work with, I prefer using a Record<T,V>

}, new Map<string, number[]>());

// 1. build the FSM
const FSM = new Map<string, [number, (number | undefined)[]]>(

@SaqAsh SaqAsh Apr 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uhh I don't know too much about this type... I think maybe we can do something like defining the (number | undefined)[] as a type or sumn to give it semantic meaning

}
if (userIds.length % 2 !== 0) {
// sentinel value of undefined at the end
userIds.push(undefined);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Comment thread backend/typescript/services/implementations/interviewDashboardServices.ts Outdated
Comment thread backend/typescript/services/implementations/interviewGroupService.ts Outdated
Comment thread backend/typescript/types.ts
@chene0
chene0 requested a review from SaqAsh April 3, 2026 00:46

@SaqAsh SaqAsh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yo you gotta rebase this off of staging (never mind I'm stupid)

@mxc-maggiechen mxc-maggiechen self-assigned this May 7, 2026
@mxc-maggiechen
mxc-maggiechen force-pushed the INTW26-build-interview-delegation-algorithm branch from 1ae9992 to 1da583f Compare May 11, 2026 03:12
@mxc-maggiechen
mxc-maggiechen merged commit f78e8c1 into main May 11, 2026
1 check passed
@mxc-maggiechen
mxc-maggiechen deleted the INTW26-build-interview-delegation-algorithm branch May 11, 2026 03:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants