From d7eecfb3321114db479f190b975d9009e275f81c Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Tue, 6 Jan 2026 17:13:54 +0530 Subject: [PATCH 1/4] Enhanced Topgear hourly report by adding registration and submission end dates from challenge phases --- sql/reports/topgear/hourly.sql | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/sql/reports/topgear/hourly.sql b/sql/reports/topgear/hourly.sql index 9953bd7..b72fa12 100644 --- a/sql/reports/topgear/hourly.sql +++ b/sql/reports/topgear/hourly.sql @@ -89,6 +89,24 @@ submitter_roles AS ( SELECT id FROM resources."ResourceRole" WHERE COALESCE("nameLower", LOWER(name)) = 'submitter' +), +registration_end AS ( + SELECT + cp."challengeId" AS challenge_id, + MAX(COALESCE(cp."actualEndDate", cp."scheduledEndDate")) AS registration_end_date + FROM challenges."ChallengePhase" cp + JOIN challenges."Phase" p ON p.id = cp."phaseId" + WHERE p.name = 'Registration' + GROUP BY cp."challengeId" +), +submission_end AS ( + SELECT + cp."challengeId" AS challenge_id, + MAX(COALESCE(cp."actualEndDate", cp."scheduledEndDate")) AS submission_end_date + FROM challenges."ChallengePhase" cp + JOIN challenges."Phase" p ON p.id = cp."phaseId" + WHERE p.name IN ('Topcoder Submission', 'Submission') + GROUP BY cp."challengeId" ) SELECT bc."updatedAt" AS modify_date, @@ -99,8 +117,8 @@ SELECT bc.name AS challenge_name, bc.status AS challenge_status, ct.name AS challenge_type, - bc."registrationEndDate" AS registration_end_date, - bc."submissionEndDate" AS submission_end_date, + re.registration_end_date AS registration_end_date, + se.submission_end_date AS submission_end_date, pd.latest_actual_end_date AS completed_date, mt.onsite_efforts AS onsite_efforts, mt.offsite_efforts AS offsite_efforts, @@ -194,6 +212,10 @@ LEFT JOIN tag_list tl ON tl.challenge_id = bc.id LEFT JOIN group_list gl ON gl.challenge_id = bc.id +LEFT JOIN registration_end re + ON re.challenge_id = bc.id +LEFT JOIN submission_end se + ON se.challenge_id = bc.id LEFT JOIN LATERAL ( SELECT MAX(cp."actualEndDate") AS latest_actual_end_date From 3ca48eaec08c47330c3ac07ef8c27e095494970e Mon Sep 17 00:00:00 2001 From: rishabhtc Date: Wed, 7 Jan 2026 18:37:33 +0530 Subject: [PATCH 2/4] Refactor SQL aliases in Topgear hourly report --- sql/reports/topgear/hourly.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sql/reports/topgear/hourly.sql b/sql/reports/topgear/hourly.sql index b72fa12..7e9c77e 100644 --- a/sql/reports/topgear/hourly.sql +++ b/sql/reports/topgear/hourly.sql @@ -117,7 +117,7 @@ SELECT bc.name AS challenge_name, bc.status AS challenge_status, ct.name AS challenge_type, - re.registration_end_date AS registration_end_date, + reg.registration_end_date AS registration_end_date, se.submission_end_date AS submission_end_date, pd.latest_actual_end_date AS completed_date, mt.onsite_efforts AS onsite_efforts, @@ -212,8 +212,8 @@ LEFT JOIN tag_list tl ON tl.challenge_id = bc.id LEFT JOIN group_list gl ON gl.challenge_id = bc.id -LEFT JOIN registration_end re - ON re.challenge_id = bc.id +LEFT JOIN registration_end reg + ON reg.challenge_id = bc.id LEFT JOIN submission_end se ON se.challenge_id = bc.id LEFT JOIN LATERAL ( From 2a6ea8f6370c2fb4aa76157c64517d08e4a7ae16 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 9 Jan 2026 09:29:27 +1100 Subject: [PATCH 3/4] New recent members report for sales --- sql/reports/topcoder/recent-member-data.sql | 269 ++++++++++++++++++ src/reports/report-directory.data.ts | 6 + .../topcoder/dto/recent-member-data.dto.ts | 13 + .../topcoder/topcoder-reports.controller.ts | 11 + .../topcoder/topcoder-reports.service.ts | 106 +++++++ 5 files changed, 405 insertions(+) create mode 100644 sql/reports/topcoder/recent-member-data.sql create mode 100644 src/reports/topcoder/dto/recent-member-data.dto.ts diff --git a/sql/reports/topcoder/recent-member-data.sql b/sql/reports/topcoder/recent-member-data.sql new file mode 100644 index 0000000..be4db0b --- /dev/null +++ b/sql/reports/topcoder/recent-member-data.sql @@ -0,0 +1,269 @@ +WITH params AS ( + SELECT COALESCE(NULLIF($1, '')::timestamptz, TIMESTAMPTZ '2024-01-01') AS start_date +), +registrants AS ( + SELECT DISTINCT r."memberId" AS member_id + FROM resources."Resource" r + JOIN resources."ResourceRole" rr + ON rr.id = r."roleId" + JOIN params p + ON r."createdAt" >= p.start_date + WHERE rr."nameLower" IN ('submitter', 'registrant') +), +latest_payment AS ( + SELECT + p.winnings_id, + MAX(p.version) AS max_version + FROM finance.payment p + GROUP BY p.winnings_id +), +paid_members AS ( + SELECT DISTINCT w.winner_id AS member_id + FROM finance.payment p + JOIN latest_payment lp + ON lp.winnings_id = p.winnings_id + AND lp.max_version = p.version + JOIN finance.winnings w + ON w.winning_id = p.winnings_id + JOIN params pr + ON COALESCE(p.date_paid, p.created_at) >= pr.start_date + WHERE p.payment_status = 'PAID' + AND w.type = 'PAYMENT' +), +eligible_members AS ( + SELECT DISTINCT r.member_id + FROM registrants r + JOIN paid_members p + ON p.member_id = r.member_id +), +role_counts AS ( + SELECT + s."memberId" AS member_id, + c."trackId" AS track_id, + COUNT(DISTINCT s."challengeId") AS submission_count + FROM reviews.submission s + JOIN challenges."Challenge" c + ON c.id = s."challengeId" + JOIN eligible_members em + ON em.member_id = s."memberId" + GROUP BY s."memberId", c."trackId" +), +ranked_roles AS ( + SELECT + rc.*, + ROW_NUMBER() OVER ( + PARTITION BY rc.member_id + ORDER BY rc.submission_count DESC + ) AS rn + FROM role_counts rc +), +member_roles AS ( + SELECT + rr.member_id, + CASE ct.track + WHEN 'DESIGN' THEN 'Design' + WHEN 'DEVELOPMENT' THEN 'Development' + WHEN 'QUALITY_ASSURANCE' THEN 'QA' + WHEN 'DATA_SCIENCE' THEN 'Data Science' + ELSE ct.name + END AS role + FROM ranked_roles rr + JOIN challenges."ChallengeTrack" ct + ON ct.id = rr.track_id + WHERE rr.rn = 1 +), +skills_agg AS ( + SELECT + skill_rows.user_id, + jsonb_agg( + jsonb_build_object( + 'name', skill_rows.skill_name, + 'status', skill_rows.status + ) + ORDER BY skill_rows.skill_name + ) AS skills + FROM ( + SELECT + us.user_id::bigint AS user_id, + sk.name AS skill_name, + CASE + WHEN lower(usl.name) = 'verified' THEN 'verified' + ELSE 'self-assigned' + END AS status, + ROW_NUMBER() OVER ( + PARTITION BY us.user_id, sk.name + ORDER BY CASE + WHEN lower(usl.name) = 'verified' THEN 0 + ELSE 1 + END + ) AS rn + FROM skills.user_skill us + JOIN eligible_members em + ON em.member_id = us.user_id::text + JOIN skills.skill sk + ON sk.id = us.skill_id + AND sk.deleted_at IS NULL + JOIN skills.user_skill_level usl + ON usl.id = us.user_skill_level_id + ) skill_rows + WHERE skill_rows.rn = 1 + GROUP BY skill_rows.user_id +), +work_history AS ( + SELECT + mt."userId" AS user_id, + jsonb_agg( + jsonb_build_object( + 'industry', mw.industry, + 'companyName', mw."companyName", + 'position', mw."position", + 'startDate', mw."startDate", + 'endDate', mw."endDate", + 'working', mw.working + ) + ORDER BY mw."startDate" DESC NULLS LAST + ) AS work_history + FROM members."memberTraits" mt + JOIN members."memberTraitWork" mw + ON mw."memberTraitId" = mt.id + GROUP BY mt."userId" +), +education_history AS ( + SELECT + mt."userId" AS user_id, + jsonb_agg( + jsonb_build_object( + 'collegeName', me."collegeName", + 'degree', me.degree, + 'endYear', me."endYear" + ) + ORDER BY me."endYear" DESC NULLS LAST + ) AS education + FROM members."memberTraits" mt + JOIN members."memberTraitEducation" me + ON me."memberTraitId" = mt.id + GROUP BY mt."userId" +), +trolley_verified AS ( + SELECT DISTINCT uiva.user_id AS member_id, true AS verified + FROM finance.user_identity_verification_associations uiva +), +challenge_wins AS ( + SELECT + cw."userId"::text AS member_id, + COUNT(DISTINCT cw."challengeId") AS challenge_wins + FROM challenges."ChallengeWinner" cw + JOIN challenges."Challenge" c + ON c.id = cw."challengeId" + JOIN challenges."ChallengeType" ct + ON ct.id = c."typeId" + JOIN eligible_members em + ON em.member_id = cw."userId"::text + WHERE cw.placement = 1 + AND COALESCE(ct."isTask", false) = false + GROUP BY cw."userId" +), +task_wins AS ( + SELECT + cw."userId"::text AS member_id, + COUNT(DISTINCT cw."challengeId") AS task_wins + FROM challenges."ChallengeWinner" cw + JOIN challenges."Challenge" c + ON c.id = cw."challengeId" + JOIN challenges."ChallengeType" ct + ON ct.id = c."typeId" + JOIN eligible_members em + ON em.member_id = cw."userId"::text + WHERE cw.placement = 1 + AND COALESCE(ct."isTask", false) = true + GROUP BY cw."userId" +), +registration_counts AS ( + SELECT + r."memberId" AS member_id, + COUNT(DISTINCT r."challengeId") AS registration_count + FROM resources."Resource" r + JOIN resources."ResourceRole" rr + ON rr.id = r."roleId" + JOIN eligible_members em + ON em.member_id = r."memberId" + WHERE rr."nameLower" IN ('submitter', 'registrant') + GROUP BY r."memberId" +), +submissions_over_75 AS ( + SELECT + s."memberId" AS member_id, + COUNT(DISTINCT s.id) AS submissions_over_75 + FROM reviews.submission s + LEFT JOIN reviews.review r + ON r."submissionId" = s.id + JOIN eligible_members em + ON em.member_id = s."memberId" + WHERE s."memberId" IS NOT NULL + AND (s."finalScore" > 75 OR r."finalScore" > 75) + GROUP BY s."memberId" +), +max_rating AS ( + SELECT DISTINCT ON ("userId") + "userId", + rating, + track, + "subTrack", + "ratingColor", + id + FROM members."memberMaxRating" + ORDER BY "userId", rating DESC +) +SELECT + m.handle, + m.email, + COALESCE( + NULLIF(m."homeCountryCode", ''), + NULLIF(m."competitionCountryCode", '') + ) AS country_code, + mr.role, + COALESCE(sk.skills, '[]'::jsonb) AS skills, + CASE + WHEN mmr.id IS NULL THEN NULL + ELSE jsonb_build_object( + 'rating', mmr.rating, + 'track', mmr.track, + 'subTrack', mmr."subTrack", + 'ratingColor', mmr."ratingColor" + ) + END AS ratings, + u.create_date AS member_since, + m."availableForGigs" AS open_to_work, + COALESCE(wh.work_history, '[]'::jsonb) AS work_history, + COALESCE(eh.education, '[]'::jsonb) AS education, + COALESCE(tv.verified, false) AS trolley_id_verified, + COALESCE(cw.challenge_wins, 0) AS challenge_wins, + COALESCE(rc.registration_count, 0) AS registration_count, + COALESCE(so.submissions_over_75, 0) AS submissions_over_75, + COALESCE(tw.task_wins, 0) AS task_wins +FROM eligible_members em +JOIN members.member m + ON m."userId"::text = em.member_id +LEFT JOIN identity."user" u + ON u.user_id::text = em.member_id +LEFT JOIN member_roles mr + ON mr.member_id = em.member_id +LEFT JOIN skills_agg sk + ON sk.user_id = m."userId" +LEFT JOIN max_rating mmr + ON mmr."userId" = m."userId" +LEFT JOIN work_history wh + ON wh.user_id = m."userId" +LEFT JOIN education_history eh + ON eh.user_id = m."userId" +LEFT JOIN trolley_verified tv + ON tv.member_id = em.member_id +LEFT JOIN challenge_wins cw + ON cw.member_id = em.member_id +LEFT JOIN task_wins tw + ON tw.member_id = em.member_id +LEFT JOIN registration_counts rc + ON rc.member_id = em.member_id +LEFT JOIN submissions_over_75 so + ON so.member_id = em.member_id +ORDER BY m.handle; diff --git a/src/reports/report-directory.data.ts b/src/reports/report-directory.data.ts index 1547d0b..b643ed9 100644 --- a/src/reports/report-directory.data.ts +++ b/src/reports/report-directory.data.ts @@ -454,6 +454,12 @@ export const REPORTS_DIRECTORY: ReportsDirectory = { "Member payment accruals for the provided date range (defaults to last 3 months)", [paymentsStartDateParam, paymentsEndDateParam], ), + report( + "Recent Member Data", + "/topcoder/recent-member-data", + "Members who registered and were paid since the start date (defaults to Jan 1, 2024)", + [paymentsStartDateParam], + ), report( "90 Day Member Spend", "/topcoder/90-day-member-spend", diff --git a/src/reports/topcoder/dto/recent-member-data.dto.ts b/src/reports/topcoder/dto/recent-member-data.dto.ts new file mode 100644 index 0000000..81b084b --- /dev/null +++ b/src/reports/topcoder/dto/recent-member-data.dto.ts @@ -0,0 +1,13 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsDateString, IsOptional } from "class-validator"; + +export class RecentMemberDataQueryDto { + @ApiPropertyOptional({ + description: + "Start date (inclusive) for registration/payment filtering in ISO 8601 format", + example: "2024-01-01T00:00:00.000Z", + }) + @IsOptional() + @IsDateString() + startDate?: string; +} diff --git a/src/reports/topcoder/topcoder-reports.controller.ts b/src/reports/topcoder/topcoder-reports.controller.ts index f5edaa5..7f177b5 100644 --- a/src/reports/topcoder/topcoder-reports.controller.ts +++ b/src/reports/topcoder/topcoder-reports.controller.ts @@ -10,6 +10,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import { TopcoderReportsService } from "./topcoder-reports.service"; import { RegistrantCountriesQueryDto } from "./dto/registrant-countries.dto"; import { MemberPaymentAccrualQueryDto } from "./dto/member-payment-accrual.dto"; +import { RecentMemberDataQueryDto } from "./dto/recent-member-data.dto"; import { TopcoderReportsGuard } from "../../auth/guards/topcoder-reports.guard"; import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; @@ -78,6 +79,16 @@ export class TopcoderReportsController { return this.reports.getMemberPaymentAccrual(startDate, endDate); } + @Get("/recent-member-data") + @ApiOperation({ + summary: + "Members who registered and were paid since the start date (defaults to Jan 1, 2024)", + }) + getRecentMemberData(@Query() query: RecentMemberDataQueryDto) { + const { startDate } = query; + return this.reports.getRecentMemberData(startDate); + } + @Get("/90-day-member-spend") @ApiOperation({ summary: "Total gross amount paid to members in the last 90 days", diff --git a/src/reports/topcoder/topcoder-reports.service.ts b/src/reports/topcoder/topcoder-reports.service.ts index 8181113..dcd0040 100644 --- a/src/reports/topcoder/topcoder-reports.service.ts +++ b/src/reports/topcoder/topcoder-reports.service.ts @@ -1,6 +1,7 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { DbService } from "../../db/db.service"; import { SqlLoaderService } from "../../common/sql-loader.service"; +import { alpha3ToCountryName } from "../../common/country.util"; type RegistrantCountriesRow = { handle: string | null; @@ -42,6 +43,49 @@ type MemberPaymentAccrualRow = { user_payment_gross_amount: string | number | null; }; +type RecentMemberDataRow = { + handle: string | null; + email: string | null; + country_code: string | null; + role: string | null; + skills: + | { + name: string | null; + status: string | null; + }[] + | null; + ratings: { + rating?: string | number | null; + track?: string | null; + subTrack?: string | null; + ratingColor?: string | null; + } | null; + member_since: Date | string | null; + open_to_work: boolean | null; + work_history: + | { + industry?: string | null; + companyName?: string | null; + position?: string | null; + startDate?: Date | string | null; + endDate?: Date | string | null; + working?: boolean | null; + }[] + | null; + education: + | { + collegeName?: string | null; + degree?: string | null; + endYear?: string | number | null; + }[] + | null; + trolley_id_verified: boolean | null; + challenge_wins: string | number | null; + task_wins: string | number | null; + registration_count: string | number | null; + submissions_over_75: string | number | null; +}; + @Injectable() export class TopcoderReportsService { constructor( @@ -455,6 +499,68 @@ export class TopcoderReportsService { })); } + async getRecentMemberData(startDate?: string) { + const query = this.sql.load("reports/topcoder/recent-member-data.sql"); + const rows = await this.db.query(query, [ + startDate ?? null, + ]); + + return rows.map((row) => { + const skills = Array.isArray(row.skills) + ? row.skills.map((skill) => ({ + name: skill?.name ?? null, + status: skill?.status ?? null, + })) + : []; + + const ratings = row.ratings + ? { + rating: this.toNullableNumber(row.ratings.rating), + track: row.ratings.track ?? null, + subTrack: row.ratings.subTrack ?? null, + ratingColor: row.ratings.ratingColor ?? null, + } + : null; + + const workHistory = Array.isArray(row.work_history) + ? row.work_history.map((item) => ({ + industry: item?.industry ?? null, + companyName: item?.companyName ?? null, + position: item?.position ?? null, + startDate: this.normalizeDate(item?.startDate ?? null), + endDate: this.normalizeDate(item?.endDate ?? null), + working: item?.working ?? null, + })) + : []; + + const education = Array.isArray(row.education) + ? row.education.map((item) => ({ + collegeName: item?.collegeName ?? null, + degree: item?.degree ?? null, + endYear: this.toNullableNumber(item?.endYear ?? null), + })) + : []; + + return { + handle: row.handle ?? null, + email: row.email ?? null, + country: alpha3ToCountryName(row.country_code), + role: row.role ?? null, + skills, + ratings, + memberSince: this.normalizeDate(row.member_since), + openToWork: row.open_to_work ?? null, + workHistory, + education, + trolleyIdVerified: row.trolley_id_verified ?? false, + challengeWins: Number(row.challenge_wins ?? 0), + taskWins: Number(row.task_wins ?? 0), + registrationCount: Number(row.registration_count ?? 0), + submissionsOver75: Number(row.submissions_over_75 ?? 0), + }; + }); + } + async getRegistrantCountries(challengeId: string) { const query = this.sql.load("reports/topcoder/registrant-countries.sql"); const rows = await this.db.query(query, [ From 042f4a1566084fa4dd1425d5d249091f373ba741 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 9 Jan 2026 15:22:55 +1100 Subject: [PATCH 4/4] Don't return Topgear members --- sql/reports/topcoder/recent-member-data.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/sql/reports/topcoder/recent-member-data.sql b/sql/reports/topcoder/recent-member-data.sql index be4db0b..57f73e1 100644 --- a/sql/reports/topcoder/recent-member-data.sql +++ b/sql/reports/topcoder/recent-member-data.sql @@ -266,4 +266,5 @@ LEFT JOIN registration_counts rc ON rc.member_id = em.member_id LEFT JOIN submissions_over_75 so ON so.member_id = em.member_id +WHERE COALESCE(m.email, '') NOT ILIKE '%@wipro.com%' ORDER BY m.handle;