Skip to content

Commit 11e2abf

Browse files
authored
Merge branch 'main' into recruiter-subscription
2 parents 272a0f4 + 24b84c6 commit 11e2abf

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

__tests__/schema/opportunity.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5953,3 +5953,47 @@ describe('query opportunityPreview', () => {
59535953
expect(res.data.opportunityPreview.result.totalCount).toBe(2);
59545954
});
59555955
});
5956+
5957+
describe('query opportunityStats', () => {
5958+
const OPPORTUNITY_STATS_QUERY = /* GraphQL */ `
5959+
query OpportunityStats($opportunityId: ID!) {
5960+
opportunityStats(opportunityId: $opportunityId) {
5961+
matched
5962+
reached
5963+
considered
5964+
decided
5965+
forReview
5966+
introduced
5967+
}
5968+
}
5969+
`;
5970+
5971+
it('should return stats for opportunity with multiple match statuses', async () => {
5972+
loggedUser = '1';
5973+
5974+
const res = await client.query(OPPORTUNITY_STATS_QUERY, {
5975+
variables: { opportunityId: opportunitiesFixture[0].id },
5976+
});
5977+
5978+
expect(res.errors).toBeFalsy();
5979+
expect(res.data.opportunityStats).toEqual({
5980+
matched: 12_000, // Mock value
5981+
reached: 4, // Total: pending(1), candidate_accepted(1), recruiter_accepted(1), recruiter_rejected(1)
5982+
considered: 3, // All except pending
5983+
decided: 0, // No candidate_rejected in fixture
5984+
forReview: 1, // candidate_accepted
5985+
introduced: 1, // recruiter_accepted
5986+
});
5987+
});
5988+
5989+
it('should deny access if user is not a recruiter for the opportunity', async () => {
5990+
loggedUser = '3'; // User 3 is not a recruiter for opportunity 0
5991+
5992+
const res = await client.query(OPPORTUNITY_STATS_QUERY, {
5993+
variables: { opportunityId: opportunitiesFixture[0].id },
5994+
});
5995+
5996+
expect(res.errors).toBeTruthy();
5997+
expect(res.errors[0].extensions.code).toBe('FORBIDDEN');
5998+
});
5999+
});

src/schema/opportunity.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ export interface GQLOpportunityPreviewConnection {
182182
result: GQLOpportunityPreviewResult;
183183
}
184184

185+
export interface GQLOpportunityStats {
186+
matched: number;
187+
reached: number;
188+
considered: number;
189+
decided: number;
190+
forReview: number;
191+
introduced: number;
192+
}
193+
185194
export const typeDefs = /* GraphQL */ `
186195
${toGQLEnum(OpportunityMatchStatus, 'OpportunityMatchStatus')}
187196
${toGQLEnum(OrganizationLinkType, 'OrganizationLinkType')}
@@ -477,6 +486,38 @@ export const typeDefs = /* GraphQL */ `
477486
result: OpportunityPreviewResult
478487
}
479488
489+
type OpportunityStats {
490+
"""
491+
Mock value for matched candidates
492+
"""
493+
matched: Int!
494+
495+
"""
496+
Total count of all matches regardless of status
497+
"""
498+
reached: Int!
499+
500+
"""
501+
Count of matches that are not pending (all statuses except pending)
502+
"""
503+
considered: Int!
504+
505+
"""
506+
Count of candidate_rejected matches
507+
"""
508+
decided: Int!
509+
510+
"""
511+
Count of candidate_accepted matches
512+
"""
513+
forReview: Int!
514+
515+
"""
516+
Count of recruiter_accepted matches
517+
"""
518+
introduced: Int!
519+
}
520+
480521
extend type Query {
481522
"""
482523
Get the public information about a Opportunity listing
@@ -575,6 +616,16 @@ export const typeDefs = /* GraphQL */ `
575616
"""
576617
opportunityId: ID
577618
): OpportunityPreviewConnection!
619+
620+
"""
621+
Get statistics for an opportunity's matches
622+
"""
623+
opportunityStats(
624+
"""
625+
Id of the Opportunity
626+
"""
627+
opportunityId: ID!
628+
): OpportunityStats! @auth
578629
}
579630
580631
input SalaryExpectationInput {
@@ -1332,6 +1383,75 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
13321383
},
13331384
};
13341385
},
1386+
opportunityStats: async (
1387+
_,
1388+
{ opportunityId }: { opportunityId: string },
1389+
ctx: AuthContext,
1390+
): Promise<GQLOpportunityStats> => {
1391+
// Verify the user has access to this opportunity
1392+
await ensureOpportunityPermissions({
1393+
con: ctx.con.manager,
1394+
userId: ctx.userId,
1395+
opportunityId,
1396+
permission: OpportunityPermissions.UpdateState,
1397+
isTeamMember: ctx.isTeamMember,
1398+
});
1399+
1400+
const result = await queryReadReplica(ctx.con, ({ queryRunner }) =>
1401+
queryRunner.manager
1402+
.getRepository(OpportunityMatch)
1403+
.createQueryBuilder('match')
1404+
.select('COUNT(*)', 'reached')
1405+
.addSelect(
1406+
`COUNT(CASE WHEN match.status != :pending THEN 1 END)`,
1407+
'considered',
1408+
)
1409+
.addSelect(
1410+
`COUNT(CASE WHEN match.status = :candidateRejected THEN 1 END)`,
1411+
'decided',
1412+
)
1413+
.addSelect(
1414+
`COUNT(CASE WHEN match.status = :candidateAccepted THEN 1 END)`,
1415+
'forReview',
1416+
)
1417+
.addSelect(
1418+
`COUNT(CASE WHEN match.status = :recruiterAccepted THEN 1 END)`,
1419+
'introduced',
1420+
)
1421+
.where('match.opportunityId = :opportunityId', { opportunityId })
1422+
.setParameter('pending', OpportunityMatchStatus.Pending)
1423+
.setParameter(
1424+
'candidateRejected',
1425+
OpportunityMatchStatus.CandidateRejected,
1426+
)
1427+
.setParameter(
1428+
'candidateAccepted',
1429+
OpportunityMatchStatus.CandidateAccepted,
1430+
)
1431+
.setParameter(
1432+
'recruiterAccepted',
1433+
OpportunityMatchStatus.RecruiterAccepted,
1434+
)
1435+
.getRawOne<{
1436+
reached: string;
1437+
considered: string;
1438+
decided: string;
1439+
forReview: string;
1440+
introduced: string;
1441+
}>(),
1442+
);
1443+
1444+
// Get counts per status using SQL aggregation
1445+
1446+
return {
1447+
matched: 12_000, // Mock value as requested
1448+
reached: parseInt(result?.reached || '0', 10),
1449+
considered: parseInt(result?.considered || '0', 10),
1450+
decided: parseInt(result?.decided || '0', 10),
1451+
forReview: parseInt(result?.forReview || '0', 10),
1452+
introduced: parseInt(result?.introduced || '0', 10),
1453+
};
1454+
},
13351455
},
13361456
Mutation: {
13371457
updateCandidatePreferences: async (

0 commit comments

Comments
 (0)