@@ -1068,6 +1068,348 @@ describe('query opportunityMatches', () => {
10681068 } ) ;
10691069} ) ;
10701070
1071+ describe ( 'query userOpportunityMatches' , ( ) => {
1072+ const GET_USER_OPPORTUNITY_MATCHES_QUERY = /* GraphQL */ `
1073+ query GetUserOpportunityMatches($first: Int, $after: String) {
1074+ userOpportunityMatches(first: $first, after: $after) {
1075+ pageInfo {
1076+ hasNextPage
1077+ hasPreviousPage
1078+ endCursor
1079+ startCursor
1080+ }
1081+ edges {
1082+ node {
1083+ userId
1084+ opportunityId
1085+ status
1086+ description {
1087+ reasoning
1088+ }
1089+ screening {
1090+ screening
1091+ answer
1092+ }
1093+ feedback {
1094+ screening
1095+ answer
1096+ }
1097+ applicationRank {
1098+ score
1099+ description
1100+ warmIntro
1101+ }
1102+ user {
1103+ id
1104+ name
1105+ }
1106+ candidatePreferences {
1107+ status
1108+ role
1109+ }
1110+ createdAt
1111+ updatedAt
1112+ }
1113+ }
1114+ }
1115+ }
1116+ ` ;
1117+
1118+ it ( 'should require authentication' , async ( ) => {
1119+ await testQueryErrorCode (
1120+ client ,
1121+ {
1122+ query : GET_USER_OPPORTUNITY_MATCHES_QUERY ,
1123+ variables : {
1124+ first : 10 ,
1125+ } ,
1126+ } ,
1127+ 'UNAUTHENTICATED' ,
1128+ ) ;
1129+ } ) ;
1130+
1131+ it ( 'should return all matches for the authenticated user' , async ( ) => {
1132+ loggedUser = '1' ;
1133+
1134+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1135+ variables : {
1136+ first : 10 ,
1137+ } ,
1138+ } ) ;
1139+
1140+ expect ( res . errors ) . toBeFalsy ( ) ;
1141+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 2 ) ;
1142+
1143+ const opportunityIds = res . data . userOpportunityMatches . edges . map (
1144+ ( e : { node : { opportunityId : string } } ) => e . node . opportunityId ,
1145+ ) ;
1146+
1147+ // User 1 has matches for opportunities 1 and 3
1148+ expect ( opportunityIds ) . toContain ( '550e8400-e29b-41d4-a716-446655440001' ) ;
1149+ expect ( opportunityIds ) . toContain ( '550e8400-e29b-41d4-a716-446655440003' ) ;
1150+
1151+ // All matches should belong to user 1
1152+ const userIds = res . data . userOpportunityMatches . edges . map (
1153+ ( e : { node : { userId : string } } ) => e . node . userId ,
1154+ ) ;
1155+ expect ( userIds . every ( ( id : string ) => id === '1' ) ) . toBe ( true ) ;
1156+ } ) ;
1157+
1158+ it ( 'should return matches ordered by updatedAt DESC' , async ( ) => {
1159+ loggedUser = '2' ;
1160+
1161+ // Add more matches for user 2 with different updatedAt dates
1162+ await saveFixtures ( con , OpportunityMatch , [
1163+ {
1164+ opportunityId : '550e8400-e29b-41d4-a716-446655440002' ,
1165+ userId : '2' ,
1166+ status : OpportunityMatchStatus . Pending ,
1167+ description : { reasoning : 'Newer match' } ,
1168+ screening : [ ] ,
1169+ feedback : [ ] ,
1170+ applicationRank : { } ,
1171+ createdAt : new Date ( '2023-01-10' ) ,
1172+ updatedAt : new Date ( '2023-01-10' ) ,
1173+ } ,
1174+ ] ) ;
1175+
1176+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1177+ variables : {
1178+ first : 10 ,
1179+ } ,
1180+ } ) ;
1181+
1182+ expect ( res . errors ) . toBeFalsy ( ) ;
1183+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 2 ) ;
1184+
1185+ const updatedDates = res . data . userOpportunityMatches . edges . map (
1186+ ( e : { node : { updatedAt : string } } ) => new Date ( e . node . updatedAt ) ,
1187+ ) ;
1188+
1189+ // Verify DESC ordering (most recent first)
1190+ expect ( updatedDates [ 0 ] . getTime ( ) ) . toBeGreaterThan ( updatedDates [ 1 ] . getTime ( ) ) ;
1191+ } ) ;
1192+
1193+ it ( 'should return different matches for different users' , async ( ) => {
1194+ loggedUser = '2' ;
1195+
1196+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1197+ variables : {
1198+ first : 10 ,
1199+ } ,
1200+ } ) ;
1201+
1202+ expect ( res . errors ) . toBeFalsy ( ) ;
1203+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1204+
1205+ const match = res . data . userOpportunityMatches . edges [ 0 ] . node ;
1206+ expect ( match . userId ) . toBe ( '2' ) ;
1207+ expect ( match . opportunityId ) . toBe ( '550e8400-e29b-41d4-a716-446655440001' ) ;
1208+ expect ( match . status ) . toBe ( 'candidate_accepted' ) ;
1209+ expect ( match . description . reasoning ) . toBe ( 'Accepted candidate' ) ;
1210+ } ) ;
1211+
1212+ it ( 'should include all match statuses for the user' , async ( ) => {
1213+ loggedUser = '1' ;
1214+
1215+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1216+ variables : {
1217+ first : 10 ,
1218+ } ,
1219+ } ) ;
1220+
1221+ expect ( res . errors ) . toBeFalsy ( ) ;
1222+
1223+ const statuses = res . data . userOpportunityMatches . edges . map (
1224+ ( e : { node : { status : string } } ) => e . node . status ,
1225+ ) ;
1226+
1227+ // User 1 has two pending matches
1228+ expect ( statuses ) . toContain ( 'pending' ) ;
1229+ } ) ;
1230+
1231+ it ( 'should include screening, feedback, and application rank data' , async ( ) => {
1232+ loggedUser = '1' ;
1233+
1234+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1235+ variables : {
1236+ first : 10 ,
1237+ } ,
1238+ } ) ;
1239+
1240+ expect ( res . errors ) . toBeFalsy ( ) ;
1241+
1242+ const matchWithData = res . data . userOpportunityMatches . edges . find (
1243+ ( e : { node : { opportunityId : string } } ) =>
1244+ e . node . opportunityId === '550e8400-e29b-41d4-a716-446655440001' ,
1245+ ) ;
1246+
1247+ expect ( matchWithData . node . screening ) . toEqual ( [
1248+ { screening : 'What is your favorite language?' , answer : 'TypeScript' } ,
1249+ ] ) ;
1250+
1251+ expect ( matchWithData . node . applicationRank ) . toEqual ( {
1252+ score : 85 ,
1253+ description : 'Strong candidate' ,
1254+ warmIntro : null ,
1255+ } ) ;
1256+ } ) ;
1257+
1258+ it ( 'should support pagination with first parameter' , async ( ) => {
1259+ loggedUser = '1' ;
1260+
1261+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1262+ variables : {
1263+ first : 1 ,
1264+ } ,
1265+ } ) ;
1266+
1267+ expect ( res . errors ) . toBeFalsy ( ) ;
1268+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1269+ expect ( res . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe ( true ) ;
1270+ expect ( res . data . userOpportunityMatches . pageInfo . endCursor ) . toBeTruthy ( ) ;
1271+ } ) ;
1272+
1273+ it ( 'should support pagination with after cursor' , async ( ) => {
1274+ loggedUser = '1' ;
1275+
1276+ // Update one match to have a different updatedAt for proper pagination testing
1277+ await con . getRepository ( OpportunityMatch ) . update (
1278+ {
1279+ opportunityId : '550e8400-e29b-41d4-a716-446655440001' ,
1280+ userId : '1' ,
1281+ } ,
1282+ {
1283+ updatedAt : new Date ( '2023-01-08' ) ,
1284+ } ,
1285+ ) ;
1286+
1287+ // Get first page
1288+ const firstPage = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1289+ variables : {
1290+ first : 1 ,
1291+ } ,
1292+ } ) ;
1293+
1294+ expect ( firstPage . errors ) . toBeFalsy ( ) ;
1295+ expect ( firstPage . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1296+ expect ( firstPage . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe (
1297+ true ,
1298+ ) ;
1299+ const firstOpportunityId =
1300+ firstPage . data . userOpportunityMatches . edges [ 0 ] . node . opportunityId ;
1301+ const endCursor = firstPage . data . userOpportunityMatches . pageInfo . endCursor ;
1302+
1303+ // Get second page
1304+ const secondPage = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1305+ variables : {
1306+ first : 10 ,
1307+ after : endCursor ,
1308+ } ,
1309+ } ) ;
1310+
1311+ expect ( secondPage . errors ) . toBeFalsy ( ) ;
1312+ expect ( secondPage . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1313+ expect ( secondPage . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe (
1314+ false ,
1315+ ) ;
1316+ // Verify we got different results
1317+ expect (
1318+ secondPage . data . userOpportunityMatches . edges [ 0 ] . node . opportunityId ,
1319+ ) . not . toBe ( firstOpportunityId ) ;
1320+ expect (
1321+ secondPage . data . userOpportunityMatches . pageInfo . hasPreviousPage ,
1322+ ) . toBe ( true ) ;
1323+ } ) ;
1324+
1325+ it ( 'should return empty list for user with no matches' , async ( ) => {
1326+ loggedUser = '5' ; // User with no matches
1327+
1328+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1329+ variables : {
1330+ first : 10 ,
1331+ } ,
1332+ } ) ;
1333+
1334+ expect ( res . errors ) . toBeFalsy ( ) ;
1335+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 0 ) ;
1336+ expect ( res . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe ( false ) ;
1337+ } ) ;
1338+
1339+ it ( 'should include user data in the response' , async ( ) => {
1340+ loggedUser = '1' ;
1341+
1342+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1343+ variables : {
1344+ first : 10 ,
1345+ } ,
1346+ } ) ;
1347+
1348+ expect ( res . errors ) . toBeFalsy ( ) ;
1349+
1350+ const firstMatch = res . data . userOpportunityMatches . edges [ 0 ] . node ;
1351+ expect ( firstMatch . user ) . toEqual ( {
1352+ id : '1' ,
1353+ name : 'Ido' ,
1354+ } ) ;
1355+ } ) ;
1356+
1357+ it ( 'should expose salaryExpectation to user viewing their own matches' , async ( ) => {
1358+ loggedUser = '1' ;
1359+
1360+ // Add salaryExpectation to user 1's candidate preferences
1361+ await con . getRepository ( UserCandidatePreference ) . upsert (
1362+ {
1363+ userId : '1' ,
1364+ salaryExpectation : {
1365+ min : 100000 ,
1366+ period : SalaryPeriod . ANNUAL ,
1367+ } ,
1368+ } ,
1369+ {
1370+ conflictPaths : [ 'userId' ] ,
1371+ skipUpdateIfNoValuesChanged : true ,
1372+ } ,
1373+ ) ;
1374+
1375+ const GET_USER_MATCHES_WITH_SALARY_QUERY = /* GraphQL */ `
1376+ query GetUserOpportunityMatchesWithSalary($first: Int) {
1377+ userOpportunityMatches(first: $first) {
1378+ edges {
1379+ node {
1380+ userId
1381+ updatedAt
1382+ candidatePreferences {
1383+ status
1384+ role
1385+ salaryExpectation {
1386+ min
1387+ period
1388+ }
1389+ }
1390+ }
1391+ }
1392+ }
1393+ }
1394+ ` ;
1395+
1396+ const res = await client . query ( GET_USER_MATCHES_WITH_SALARY_QUERY , {
1397+ variables : {
1398+ first : 10 ,
1399+ } ,
1400+ } ) ;
1401+
1402+ expect ( res . errors ) . toBeFalsy ( ) ;
1403+
1404+ const firstMatch = res . data . userOpportunityMatches . edges [ 0 ] . node ;
1405+ expect ( firstMatch . userId ) . toBe ( '1' ) ;
1406+ expect ( firstMatch . candidatePreferences . salaryExpectation ) . toEqual ( {
1407+ min : 100000 ,
1408+ period : 1 , // ANNUAL
1409+ } ) ;
1410+ } ) ;
1411+ } ) ;
1412+
10711413describe ( 'query getCandidatePreferences' , ( ) => {
10721414 const QUERY = /* GraphQL */ `
10731415 query GetCandidatePreferences {
0 commit comments