@@ -1068,6 +1068,350 @@ 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 (
1191+ updatedDates [ 1 ] . getTime ( ) ,
1192+ ) ;
1193+ } ) ;
1194+
1195+ it ( 'should return different matches for different users' , async ( ) => {
1196+ loggedUser = '2' ;
1197+
1198+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1199+ variables : {
1200+ first : 10 ,
1201+ } ,
1202+ } ) ;
1203+
1204+ expect ( res . errors ) . toBeFalsy ( ) ;
1205+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1206+
1207+ const match = res . data . userOpportunityMatches . edges [ 0 ] . node ;
1208+ expect ( match . userId ) . toBe ( '2' ) ;
1209+ expect ( match . opportunityId ) . toBe ( '550e8400-e29b-41d4-a716-446655440001' ) ;
1210+ expect ( match . status ) . toBe ( 'candidate_accepted' ) ;
1211+ expect ( match . description . reasoning ) . toBe ( 'Accepted candidate' ) ;
1212+ } ) ;
1213+
1214+ it ( 'should include all match statuses for the user' , async ( ) => {
1215+ loggedUser = '1' ;
1216+
1217+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1218+ variables : {
1219+ first : 10 ,
1220+ } ,
1221+ } ) ;
1222+
1223+ expect ( res . errors ) . toBeFalsy ( ) ;
1224+
1225+ const statuses = res . data . userOpportunityMatches . edges . map (
1226+ ( e : { node : { status : string } } ) => e . node . status ,
1227+ ) ;
1228+
1229+ // User 1 has two pending matches
1230+ expect ( statuses ) . toContain ( 'pending' ) ;
1231+ } ) ;
1232+
1233+ it ( 'should include screening, feedback, and application rank data' , async ( ) => {
1234+ loggedUser = '1' ;
1235+
1236+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1237+ variables : {
1238+ first : 10 ,
1239+ } ,
1240+ } ) ;
1241+
1242+ expect ( res . errors ) . toBeFalsy ( ) ;
1243+
1244+ const matchWithData = res . data . userOpportunityMatches . edges . find (
1245+ ( e : { node : { opportunityId : string } } ) =>
1246+ e . node . opportunityId === '550e8400-e29b-41d4-a716-446655440001' ,
1247+ ) ;
1248+
1249+ expect ( matchWithData . node . screening ) . toEqual ( [
1250+ { screening : 'What is your favorite language?' , answer : 'TypeScript' } ,
1251+ ] ) ;
1252+
1253+ expect ( matchWithData . node . applicationRank ) . toEqual ( {
1254+ score : 85 ,
1255+ description : 'Strong candidate' ,
1256+ warmIntro : null ,
1257+ } ) ;
1258+ } ) ;
1259+
1260+ it ( 'should support pagination with first parameter' , async ( ) => {
1261+ loggedUser = '1' ;
1262+
1263+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1264+ variables : {
1265+ first : 1 ,
1266+ } ,
1267+ } ) ;
1268+
1269+ expect ( res . errors ) . toBeFalsy ( ) ;
1270+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1271+ expect ( res . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe ( true ) ;
1272+ expect ( res . data . userOpportunityMatches . pageInfo . endCursor ) . toBeTruthy ( ) ;
1273+ } ) ;
1274+
1275+ it ( 'should support pagination with after cursor' , async ( ) => {
1276+ loggedUser = '1' ;
1277+
1278+ // Update one match to have a different updatedAt for proper pagination testing
1279+ await con . getRepository ( OpportunityMatch ) . update (
1280+ {
1281+ opportunityId : '550e8400-e29b-41d4-a716-446655440001' ,
1282+ userId : '1' ,
1283+ } ,
1284+ {
1285+ updatedAt : new Date ( '2023-01-08' ) ,
1286+ } ,
1287+ ) ;
1288+
1289+ // Get first page
1290+ const firstPage = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1291+ variables : {
1292+ first : 1 ,
1293+ } ,
1294+ } ) ;
1295+
1296+ expect ( firstPage . errors ) . toBeFalsy ( ) ;
1297+ expect ( firstPage . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1298+ expect ( firstPage . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe (
1299+ true ,
1300+ ) ;
1301+ const firstOpportunityId =
1302+ firstPage . data . userOpportunityMatches . edges [ 0 ] . node . opportunityId ;
1303+ const endCursor = firstPage . data . userOpportunityMatches . pageInfo . endCursor ;
1304+
1305+ // Get second page
1306+ const secondPage = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1307+ variables : {
1308+ first : 10 ,
1309+ after : endCursor ,
1310+ } ,
1311+ } ) ;
1312+
1313+ expect ( secondPage . errors ) . toBeFalsy ( ) ;
1314+ expect ( secondPage . data . userOpportunityMatches . edges ) . toHaveLength ( 1 ) ;
1315+ expect ( secondPage . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe (
1316+ false ,
1317+ ) ;
1318+ // Verify we got different results
1319+ expect (
1320+ secondPage . data . userOpportunityMatches . edges [ 0 ] . node . opportunityId ,
1321+ ) . not . toBe ( firstOpportunityId ) ;
1322+ expect (
1323+ secondPage . data . userOpportunityMatches . pageInfo . hasPreviousPage ,
1324+ ) . toBe ( true ) ;
1325+ } ) ;
1326+
1327+ it ( 'should return empty list for user with no matches' , async ( ) => {
1328+ loggedUser = '5' ; // User with no matches
1329+
1330+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1331+ variables : {
1332+ first : 10 ,
1333+ } ,
1334+ } ) ;
1335+
1336+ expect ( res . errors ) . toBeFalsy ( ) ;
1337+ expect ( res . data . userOpportunityMatches . edges ) . toHaveLength ( 0 ) ;
1338+ expect ( res . data . userOpportunityMatches . pageInfo . hasNextPage ) . toBe ( false ) ;
1339+ } ) ;
1340+
1341+ it ( 'should include user data in the response' , async ( ) => {
1342+ loggedUser = '1' ;
1343+
1344+ const res = await client . query ( GET_USER_OPPORTUNITY_MATCHES_QUERY , {
1345+ variables : {
1346+ first : 10 ,
1347+ } ,
1348+ } ) ;
1349+
1350+ expect ( res . errors ) . toBeFalsy ( ) ;
1351+
1352+ const firstMatch = res . data . userOpportunityMatches . edges [ 0 ] . node ;
1353+ expect ( firstMatch . user ) . toEqual ( {
1354+ id : '1' ,
1355+ name : 'Ido' ,
1356+ } ) ;
1357+ } ) ;
1358+
1359+ it ( 'should expose salaryExpectation to user viewing their own matches' , async ( ) => {
1360+ loggedUser = '1' ;
1361+
1362+ // Add salaryExpectation to user 1's candidate preferences
1363+ await con . getRepository ( UserCandidatePreference ) . upsert (
1364+ {
1365+ userId : '1' ,
1366+ salaryExpectation : {
1367+ min : 100000 ,
1368+ period : SalaryPeriod . ANNUAL ,
1369+ } ,
1370+ } ,
1371+ {
1372+ conflictPaths : [ 'userId' ] ,
1373+ skipUpdateIfNoValuesChanged : true ,
1374+ } ,
1375+ ) ;
1376+
1377+ const GET_USER_MATCHES_WITH_SALARY_QUERY = /* GraphQL */ `
1378+ query GetUserOpportunityMatchesWithSalary($first: Int) {
1379+ userOpportunityMatches(first: $first) {
1380+ edges {
1381+ node {
1382+ userId
1383+ updatedAt
1384+ candidatePreferences {
1385+ status
1386+ role
1387+ salaryExpectation {
1388+ min
1389+ period
1390+ }
1391+ }
1392+ }
1393+ }
1394+ }
1395+ }
1396+ ` ;
1397+
1398+ const res = await client . query ( GET_USER_MATCHES_WITH_SALARY_QUERY , {
1399+ variables : {
1400+ first : 10 ,
1401+ } ,
1402+ } ) ;
1403+
1404+ expect ( res . errors ) . toBeFalsy ( ) ;
1405+
1406+ const firstMatch = res . data . userOpportunityMatches . edges [ 0 ] . node ;
1407+ expect ( firstMatch . userId ) . toBe ( '1' ) ;
1408+ expect ( firstMatch . candidatePreferences . salaryExpectation ) . toEqual ( {
1409+ min : 100000 ,
1410+ period : 1 , // ANNUAL
1411+ } ) ;
1412+ } ) ;
1413+ } ) ;
1414+
10711415describe ( 'query getCandidatePreferences' , ( ) => {
10721416 const QUERY = /* GraphQL */ `
10731417 query GetCandidatePreferences {
0 commit comments