1- import { Injectable , NotFoundException } from "@nestjs/common" ;
1+ import { Injectable , NotFoundException , OnModuleInit } from "@nestjs/common" ;
22import { alpha3ToCountryName } from "../../common/country.util" ;
33import { DbService } from "../../db/db.service" ;
44import { MemberSearchBodyDto } from "./dto/member-search.dto" ;
@@ -42,9 +42,35 @@ function formatLocation(location: string): string {
4242}
4343
4444@Injectable ( )
45- export class MemberSearchService {
45+ export class MemberSearchService implements OnModuleInit {
46+ private winEventTypeIds : string [ ] = [ ] ;
47+ private engagementSourceTypeId : string = "" ;
48+
4649 constructor ( private readonly db : DbService ) { }
4750
51+ async onModuleInit ( ) : Promise < void > {
52+ const [ winRows , engRows ] = await Promise . all ( [
53+ this . db . query < { id : string } > (
54+ `SELECT id::text FROM skills.skill_event_type
55+ WHERE name = ANY($1::text[])` ,
56+ [
57+ [
58+ "challenge_win" ,
59+ "challenge_2nd_place" ,
60+ "challenge_3rd_place" ,
61+ "gig_completion" ,
62+ ] ,
63+ ] ,
64+ ) ,
65+ this . db . query < { id : string } > (
66+ `SELECT id::text FROM skills.source_type WHERE name = 'engagement'` ,
67+ [ ] ,
68+ ) ,
69+ ] ) ;
70+ this . winEventTypeIds = winRows . map ( ( r ) => r . id ) ;
71+ this . engagementSourceTypeId = engRows [ 0 ] ?. id ?? "" ;
72+ }
73+
4874 async search ( dto : MemberSearchBodyDto ) : Promise < MemberSearchResponseDto > {
4975 const {
5076 skills,
@@ -79,8 +105,14 @@ export class MemberSearchService {
79105 // Collect CTE bodies (joined later with commas)
80106 const ctes : string [ ] = [ ] ;
81107
108+ // active_members is always first so later CTEs can reference it
109+ ctes . push ( `active_members AS MATERIALIZED (
110+ SELECT m."userId" AS user_id
111+ FROM members.member m
112+ WHERE m.status = 'ACTIVE'
113+ )` ) ;
114+
82115 // Expressions swapped in to the SELECT based on whether skills are requested
83- let skillJoin = "" ;
84116 let matchedSkillsExpr = `'[]'::jsonb` ;
85117 let matchIndexExpr = "0" ;
86118
@@ -93,6 +125,8 @@ export class MemberSearchService {
93125 const pMinWins = p ( minWins ) ;
94126 const pSearchType = p ( skillSearchType ) ;
95127 const pNumSkills = p ( deduped . length ) ;
128+ const pWinTypeIds = p ( this . winEventTypeIds ) ;
129+ const pEngSourceId = p ( this . engagementSourceTypeId ) ;
96130
97131 ctes . push ( `requested_skills AS (
98132 SELECT rs.skill_id, rs.min_wins
@@ -104,13 +138,13 @@ skill_event_stats AS (
104138 se.user_id,
105139 se.skill_id,
106140 COUNT(*) FILTER (
107- WHERE LOWER(set_t.name) IN ('challenge_win', 'challenge_2nd_place', 'challenge_3rd_place', 'gig_completion') OR sest.name='engagement'
141+ WHERE se.skill_event_type_id = ANY(${ pWinTypeIds } ::uuid[])
142+ OR se.source_type_id = ${ pEngSourceId } ::uuid
108143 ) AS wins,
109144 COUNT(*) AS submitted
110145 FROM skills.skill_event se
111- JOIN skills.skill_event_type set_t ON set_t.id = se.skill_event_type_id
112- JOIN skills.source_type sest ON sest.id = se.source_type_id
113146 WHERE se.skill_id = ANY(${ pSkillIds } ::uuid[])
147+ AND se.user_id IN (SELECT user_id FROM active_members)
114148 GROUP BY se.user_id, se.skill_id
115149),
116150deduped_user_skills AS (
@@ -119,6 +153,7 @@ deduped_user_skills AS (
119153 us.skill_id
120154 FROM skills.user_skill us
121155 WHERE us.skill_id = ANY(${ pSkillIds } ::uuid[])
156+ AND us.user_id IN (SELECT user_id FROM active_members)
122157),
123158user_skill_data AS (
124159 SELECT
@@ -182,7 +217,6 @@ user_match_data AS (
182217 GROUP BY usd.user_id
183218)` ) ;
184219
185- skillJoin = `INNER JOIN user_match_data umd ON umd.user_id = m."userId"` ;
186220 matchedSkillsExpr = `umd.matched_skills` ;
187221 matchIndexExpr = `CEIL(
188222 LEAST(
@@ -196,13 +230,13 @@ user_match_data AS (
196230 ctes . push ( `recently_active AS (
197231 SELECT DISTINCT r."memberId"::bigint AS user_id
198232 FROM resources."Resource" r
233+ INNER JOIN active_members am ON am.user_id = r."memberId"::bigint
199234 WHERE r."createdAt" >= NOW() - INTERVAL '3 months'
200- AND r."memberId" ~ '^[0-9]+$'
201235),
202236verified_via_trolley AS (
203237 SELECT DISTINCT tr.user_id::bigint AS user_id
204238 FROM finance.trolley_recipient tr
205- WHERE tr .user_id ~ '^[0-9]+$'
239+ INNER JOIN active_members am ON am .user_id = tr.user_id::bigint
206240),
207241member_address AS (
208242 SELECT DISTINCT ON ("userId")
@@ -237,7 +271,7 @@ member_address AS (
237271 ? [
238272 ...new Set (
239273 countries
240- . map ( ( value ) => String ( value ) . trim ( ) . toLowerCase ( ) )
274+ . map ( ( value ) => String ( value ) . trim ( ) . toUpperCase ( ) )
241275 . filter ( Boolean ) ,
242276 ) ,
243277 ]
@@ -247,9 +281,9 @@ member_address AS (
247281 const pCountries = p ( normalizedCountries ) ;
248282 where . push (
249283 `(
250- LOWER( m."homeCountryCode") = ANY(${ pCountries } ::text[])
251- OR LOWER( m."competitionCountryCode") = ANY(${ pCountries } ::text[])
252- OR LOWER (m.country) = ANY(${ pCountries } ::text[])
284+ m."homeCountryCode" = ANY(${ pCountries } ::text[])
285+ OR m."competitionCountryCode" = ANY(${ pCountries } ::text[])
286+ OR UPPER (m.country) = ANY(${ pCountries } ::text[])
253287 )` ,
254288 ) ;
255289 }
@@ -258,7 +292,7 @@ member_address AS (
258292 ctes . push ( `filtered_members AS (
259293 SELECT m."userId" AS user_id
260294 FROM members.member m
261- ${ skillJoin }
295+ INNER JOIN user_match_data umd ON umd.user_id = m."userId"
262296 WHERE ${ whereClause }
263297)` ) ;
264298
@@ -311,21 +345,19 @@ member_address AS (
311345 FROM skills.user_skill us2
312346 INNER JOIN skills.user_skill_display_mode usdm2 ON usdm2.id = us2.user_skill_display_mode_id
313347 WHERE us2.user_id = m2."userId"
314- AND LOWER( usdm2.name) = 'principal'
348+ AND usdm2.name = 'principal'
315349 )
316350 AND EXISTS (
317351 SELECT 1
318352 FROM skills.user_skill us2
319353 INNER JOIN skills.user_skill_display_mode usdm2 ON usdm2.id = us2.user_skill_display_mode_id
320354 WHERE us2.user_id = m2."userId"
321- AND LOWER( usdm2.name) = 'additional'
355+ AND usdm2.name = 'additional'
322356 )
323357)` ) ;
324358 }
325359
326- // Snapshot param count BEFORE adding pagination — count query stops here
327360 const filterParamCount = params . length ;
328-
329361 const pLimit = p ( limit ) ;
330362 const pOffset = p ( ( page - 1 ) * limit ) ;
331363
@@ -349,10 +381,7 @@ SELECT
349381 TRIM(COALESCE(m."firstName", '') || ' ' || COALESCE(m."lastName", '')) AS name,
350382 m."photoURL" AS "photoUrl",
351383 EXISTS (SELECT 1 FROM recently_active ra WHERE ra.user_id = m."userId") AS "isRecentlyActive",
352- (
353- COALESCE(m.verified, false) = true
354- OR EXISTS (SELECT 1 FROM verified_via_trolley vt WHERE vt.user_id = m."userId")
355- ) AS "isVerified",
384+ (COALESCE(m.verified, false) = true OR vt.user_id IS NOT NULL) AS "isVerified",
356385 COALESCE(m."availableForGigs", false) AS "openToWork",
357386 TRIM(
358387 COALESCE(maddr.city || ' ', '') ||
@@ -363,15 +392,16 @@ SELECT
363392FROM members.member m
364393INNER JOIN filtered_members fm ON fm.user_id = m."userId"
365394${ profileCompleteJoin }
366- ${ skillJoin }
395+ LEFT JOIN user_match_data umd ON umd.user_id = m."userId"
396+ LEFT JOIN verified_via_trolley vt ON vt.user_id = m."userId"
367397LEFT JOIN member_address maddr ON maddr."userId" = m."userId"
368398ORDER BY ${ orderByClause }
369399LIMIT ${ pLimit } OFFSET ${ pOffset } ` ;
370400
371401 const countQuery = `
372- WITH ${ ctesBlock }
373- SELECT COUNT(*)::integer AS total
374- FROM ${ profileComplete === true ? "profile_complete_filtered pcf" : "filtered_members fm" } ` ;
402+ WITH ${ ctesBlock }
403+ SELECT COUNT(*)::integer AS total
404+ FROM ${ profileComplete === true ? "profile_complete_filtered pcf" : "filtered_members fm" } `;
375405
376406 const [ rows , countRows ] = await Promise . all ( [
377407 this . db . query < RawMemberRow > ( dataQuery , params ) ,
0 commit comments