From d8294bfd9e1823161f035365dc4f7b8e885da96b Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 23 Apr 2026 10:35:36 +0530 Subject: [PATCH 1/9] PM-4922 Completeness optimised query --- src/reports/member/dto/member-search.dto.ts | 8 ++ src/reports/member/member-search.service.ts | 84 +++++++++++++++++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/reports/member/dto/member-search.dto.ts b/src/reports/member/dto/member-search.dto.ts index d84e299..5d89c9b 100644 --- a/src/reports/member/dto/member-search.dto.ts +++ b/src/reports/member/dto/member-search.dto.ts @@ -75,6 +75,14 @@ export class MemberSearchBodyDto { @IsBoolean() verifiedProfile?: boolean; + @ApiPropertyOptional({ + description: + "When true, apply 100% profile completeness checks after lightweight filters are applied.", + }) + @IsOptional() + @IsBoolean() + profileComplete?: boolean; + @ApiPropertyOptional({ description: "Filter by multiple country names or country codes (case-insensitive).", diff --git a/src/reports/member/member-search.service.ts b/src/reports/member/member-search.service.ts index 1d9e38c..0a0f4ca 100644 --- a/src/reports/member/member-search.service.ts +++ b/src/reports/member/member-search.service.ts @@ -52,6 +52,7 @@ export class MemberSearchService { openToWork, recentlyActive, verifiedProfile, + profileComplete, countries, sortBy = "matchIndex", sortOrder = "desc", @@ -203,7 +204,7 @@ member_address AS ( id DESC )`); - // ------------------------------------------------- dynamic WHERE + // ------------------------------------------------- dynamic WHERE (easy filters first) const where: string[] = [`m.status = 'ACTIVE'`]; if (openToWork === true) { @@ -243,6 +244,74 @@ member_address AS ( ); } + const whereClause = where.join(" AND "); + ctes.push(`filtered_members AS ( + SELECT m."userId" AS user_id + FROM members.member m + ${skillJoin} + WHERE ${whereClause} +)`); + + if (profileComplete === true) { + ctes.push(`profile_complete_filtered AS ( + SELECT fm.user_id + FROM filtered_members fm + INNER JOIN members.member m2 ON m2."userId" = fm.user_id + WHERE m2.description IS NOT NULL + AND btrim(m2.description) <> '' + AND m2."homeCountryCode" IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM members."memberAddress" ma2 + WHERE ma2."userId" = m2."userId" + AND ma2.city IS NOT NULL + AND btrim(ma2.city) <> '' + ) + AND EXISTS ( + SELECT 1 + FROM members."memberTraits" mt2 + INNER JOIN members."memberTraitWork" mw2 ON mw2."memberTraitId" = mt2.id + WHERE mt2."userId" = m2."userId" + ) + AND EXISTS ( + SELECT 1 + FROM members."memberTraits" mt2 + INNER JOIN members."memberTraitEducation" me2 ON me2."memberTraitId" = mt2.id + WHERE mt2."userId" = m2."userId" + ) + AND EXISTS ( + SELECT 1 + FROM members."memberTraits" mt2 + INNER JOIN members."memberTraitPersonalization" mtp2 ON mtp2."memberTraitId" = mt2.id + WHERE mt2."userId" = m2."userId" + AND mtp2.key = 'openToWork' + AND mtp2.value IS NOT NULL + AND ( + NOT (mtp2.value::jsonb ? 'availability') + OR ( + mtp2.value::jsonb ? 'availability' + AND mtp2.value::jsonb ? 'preferredRoles' + AND jsonb_array_length(mtp2.value::jsonb -> 'preferredRoles') > 0 + ) + ) + ) + AND EXISTS ( + SELECT 1 + FROM skills.user_skill us2 + INNER JOIN skills.user_skill_display_mode usdm2 ON usdm2.id = us2.user_skill_display_mode_id + WHERE us2.user_id = m2."userId" + AND LOWER(usdm2.name) = 'principal' + ) + AND EXISTS ( + SELECT 1 + FROM skills.user_skill us2 + INNER JOIN skills.user_skill_display_mode usdm2 ON usdm2.id = us2.user_skill_display_mode_id + WHERE us2.user_id = m2."userId" + AND LOWER(usdm2.name) = 'additional' + ) +)`); + } + // Snapshot param count BEFORE adding pagination — count query stops here const filterParamCount = params.length; @@ -251,12 +320,15 @@ member_address AS ( // ---------------------------------------------------------------- queries const ctesBlock = ctes.join(",\n"); - const whereClause = where.join(" AND "); const direction = sortOrder === "asc" ? "ASC" : "DESC"; const orderByClause = sortBy === "handle" ? `m.handle ${direction}, "matchIndex" DESC NULLS LAST` : `"matchIndex" ${direction} NULLS LAST, m.handle ASC`; + const profileCompleteJoin = + profileComplete === true + ? `INNER JOIN profile_complete_filtered pcf ON pcf.user_id = m."userId"` + : ""; const dataQuery = ` WITH ${ctesBlock} @@ -278,18 +350,18 @@ SELECT ${matchedSkillsExpr} AS "matchedSkills", ${matchIndexExpr} AS "matchIndex" FROM members.member m +INNER JOIN filtered_members fm ON fm.user_id = m."userId" +${profileCompleteJoin} ${skillJoin} LEFT JOIN member_address maddr ON maddr."userId" = m."userId" -WHERE ${whereClause} ORDER BY ${orderByClause} LIMIT ${pLimit} OFFSET ${pOffset}`; const countQuery = ` WITH ${ctesBlock} SELECT COUNT(*)::integer AS total -FROM members.member m -${skillJoin} -WHERE ${whereClause}`; +FROM filtered_members fm +${profileComplete === true ? `INNER JOIN profile_complete_filtered pcf ON pcf.user_id = fm.user_id` : ""}`; const [rows, countRows] = await Promise.all([ this.db.query(dataQuery, params), From 56ec944ce7c7280d5abe9b806d4f0ad88b777c6b Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 23 Apr 2026 12:10:43 +0300 Subject: [PATCH 2/9] PM-4877 - payment reports by category --- .../topcoder/member-payment-accrual.sql | 46 ++++++--- src/app.module.ts | 2 + .../guards/admin-payment-reports.guard.ts | 41 ++++++++ .../payment/payment-reports.controller.ts | 95 +++++++++++++++++++ src/reports/payment/payment-reports.module.ts | 13 +++ src/reports/report-directory.data.spec.ts | 22 +++-- src/reports/report-directory.data.ts | 73 ++++++++++++-- .../topcoder/topcoder-reports.controller.ts | 11 --- .../topcoder/topcoder-reports.module.ts | 1 + .../topcoder/topcoder-reports.service.ts | 7 +- 10 files changed, 267 insertions(+), 44 deletions(-) create mode 100644 src/reports/payment/guards/admin-payment-reports.guard.ts create mode 100644 src/reports/payment/payment-reports.controller.ts create mode 100644 src/reports/payment/payment-reports.module.ts diff --git a/sql/reports/topcoder/member-payment-accrual.sql b/sql/reports/topcoder/member-payment-accrual.sql index aec6c3a..8970104 100644 --- a/sql/reports/topcoder/member-payment-accrual.sql +++ b/sql/reports/topcoder/member-payment-accrual.sql @@ -26,7 +26,6 @@ recent_payments AS ( SELECT w.winning_id, w.winner_id, - w.type, w.description, w.category, w.external_id AS challenge_id, @@ -52,36 +51,55 @@ recent_payments AS ( WHERE w.type = 'PAYMENT' AND p.created_at >= pr.start_date AND p.created_at <= pr.end_date +), +categorized_payments AS ( + SELECT + rp.*, + CASE + WHEN rp.category = 'TAAS_PAYMENT' THEN 'TaaS Payment' + WHEN rp.category = 'TOPGEAR_PAYMENT' THEN 'Topgear Payment' + WHEN rp.category = 'ENGAGEMENT_PAYMENT' THEN 'Engagement Payment' + WHEN rp.category IN ( + 'TASK_PAYMENT', + 'TASK_REVIEW_PAYMENT', + 'TASK_COPILOT_PAYMENT', + 'DEPLOYMENT_TASK_PAYMENT', + 'PROJECT_DEPLOYMENT_TASK_PAYMENT' + ) THEN 'Task Payment' + ELSE 'Challenge Payment' + END AS payment_type + FROM recent_payments rp ) SELECT - rp.payment_created_at AS payment_created_at, - rp.payment_id, - rp.description AS payment_description, - rp.challenge_id, - rp.payment_status, - rp.type AS payment_type, + cp.payment_created_at AS payment_created_at, + cp.payment_id, + cp.description AS payment_description, + cp.challenge_id, + cp.payment_status, + cp.payment_type, mem.handle AS payee_handle, pm.name AS payment_method, ba."name" AS billing_account_name, cl."name" AS customer_name, ba."subcontractingEndCustomer" AS reporting_account_name, - rp.winner_id AS member_id, + cp.winner_id AS member_id, to_char(c."createdAt", 'YYYY-MM-DD') AS challenge_created_date, - rp.gross_amount AS user_payment_gross_amount -FROM recent_payments rp + cp.gross_amount AS user_payment_gross_amount +FROM categorized_payments cp LEFT JOIN challenges."Challenge" c - ON c."id" = rp.challenge_id + ON c."id" = cp.challenge_id LEFT JOIN challenges."ChallengeBilling" cb ON cb."challengeId" = c."id" LEFT JOIN "billing-accounts"."BillingAccount" ba ON ba."id" = COALESCE( - NULLIF(rp.billing_account, '')::int, + NULLIF(cp.billing_account, '')::int, NULLIF(cb."billingAccountId", '')::int ) LEFT JOIN "billing-accounts"."Client" cl ON cl."id" = ba."clientId" LEFT JOIN finance.payment_method pm - ON pm.payment_method_id = rp.payment_method_id + ON pm.payment_method_id = cp.payment_method_id LEFT JOIN members.member mem - ON mem."userId"::text = rp.winner_id + ON mem."userId"::text = cp.winner_id +WHERE ($3::text[] IS NULL OR cp.payment_type = ANY($3::text[])) ORDER BY payment_created_at DESC; diff --git a/src/app.module.ts b/src/app.module.ts index 4635441..fa7f2f9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { ChallengesReportsModule } from "./reports/challenges/challenges-reports import { IdentityReportsModule } from "./reports/identity/identity-reports.module"; import { ReportsModule } from "./reports/reports.module"; import { MemberSearchModule } from "./reports/member/member-search.module"; +import { PaymentReportsModule } from "./reports/payment/payment-reports.module"; @Module({ imports: [ @@ -25,6 +26,7 @@ import { MemberSearchModule } from "./reports/member/member-search.module"; IdentityReportsModule, ReportsModule, MemberSearchModule, + PaymentReportsModule, HealthModule, ], }) diff --git a/src/reports/payment/guards/admin-payment-reports.guard.ts b/src/reports/payment/guards/admin-payment-reports.guard.ts new file mode 100644 index 0000000..45783db --- /dev/null +++ b/src/reports/payment/guards/admin-payment-reports.guard.ts @@ -0,0 +1,41 @@ +import { + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, + UnauthorizedException, +} from "@nestjs/common"; +import { + AuthUserLike, + getNormalizedRoles, + hasAdminRole, +} from "../../../auth/permissions.util"; + +@Injectable() +export class AdminPaymentReportsGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const authUser: AuthUserLike | undefined = context + .switchToHttp() + .getRequest().authUser; + + if (!authUser) { + throw new UnauthorizedException("You are not authenticated."); + } + + if (authUser.isMachine) { + throw new ForbiddenException( + "You do not have the required permissions to access this resource.", + ); + } + + const roles = getNormalizedRoles(authUser); + + if (hasAdminRole(roles)) { + return true; + } + + throw new ForbiddenException( + "You do not have the required permissions to access this resource.", + ); + } +} diff --git a/src/reports/payment/payment-reports.controller.ts b/src/reports/payment/payment-reports.controller.ts new file mode 100644 index 0000000..890caa2 --- /dev/null +++ b/src/reports/payment/payment-reports.controller.ts @@ -0,0 +1,95 @@ +import { + Controller, + Get, + Query, + UseGuards, + UseInterceptors, +} from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { TopcoderReportsService } from "../topcoder/topcoder-reports.service"; +import { MemberPaymentAccrualQueryDto } from "../topcoder/dto/member-payment-accrual.dto"; +import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; +import { AdminPaymentReportsGuard } from "./guards/admin-payment-reports.guard"; + +@ApiTags("Payment Reports") +@ApiBearerAuth() +@UseGuards(AdminPaymentReportsGuard) +@UseInterceptors(CsvResponseInterceptor) +@Controller() +export class PaymentReportsController { + constructor(private readonly reports: TopcoderReportsService) {} + + @Get("/payment/member-payment-accrual") + @ApiOperation({ + summary: + "Member payment accruals for the provided date range (defaults to last 3 months)", + }) + getMemberPaymentAccrual(@Query() query: MemberPaymentAccrualQueryDto) { + const { startDate, endDate } = query; + return this.reports.getMemberPaymentAccrual(startDate, endDate); + } + + @Get("/payment/member-payment-accrual-taas") + @ApiOperation({ + summary: + "Member payment accruals for TaaS payments for the provided date range (defaults to last 3 months)", + }) + getMemberPaymentAccrualTaas(@Query() query: MemberPaymentAccrualQueryDto) { + const { startDate, endDate } = query; + return this.reports.getMemberPaymentAccrual(startDate, endDate, [ + "TaaS Payment", + ]); + } + + @Get("/payment/member-payment-accrual-topgear") + @ApiOperation({ + summary: + "Member payment accruals for Topgear payments for the provided date range (defaults to last 3 months)", + }) + getMemberPaymentAccrualTopgear(@Query() query: MemberPaymentAccrualQueryDto) { + const { startDate, endDate } = query; + return this.reports.getMemberPaymentAccrual(startDate, endDate, [ + "Topgear Payment", + ]); + } + + @Get("/payment/member-payment-accrual-engagement") + @ApiOperation({ + summary: + "Member payment accruals for engagement payments for the provided date range (defaults to last 3 months)", + }) + getMemberPaymentAccrualEngagement( + @Query() query: MemberPaymentAccrualQueryDto, + ) { + const { startDate, endDate } = query; + return this.reports.getMemberPaymentAccrual(startDate, endDate, [ + "Engagement Payment", + ]); + } + + @Get("/payment/member-payment-accrual-task") + @ApiOperation({ + summary: + "Member payment accruals for task payments for the provided date range (defaults to last 3 months)", + }) + getMemberPaymentAccrualTask(@Query() query: MemberPaymentAccrualQueryDto) { + const { startDate, endDate } = query; + return this.reports.getMemberPaymentAccrual(startDate, endDate, [ + "Task Payment", + ]); + } + + @Get("/payment/member-payment-accrual-challenge") + @ApiOperation({ + summary: + "Member payment accruals for challenge payments (contest, review board, copilot, checkpoint, and related challenge payouts) for the provided date range (defaults to last 3 months)", + }) + getMemberPaymentAccrualChallenge( + @Query() query: MemberPaymentAccrualQueryDto, + ) { + const { startDate, endDate } = query; + return this.reports.getMemberPaymentAccrual(startDate, endDate, [ + "Challenge Payment", + ]); + } +} diff --git a/src/reports/payment/payment-reports.module.ts b/src/reports/payment/payment-reports.module.ts new file mode 100644 index 0000000..80abf0b --- /dev/null +++ b/src/reports/payment/payment-reports.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { CsvSerializer } from "../../common/csv/csv-serializer"; +import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; +import { TopcoderReportsModule } from "../topcoder/topcoder-reports.module"; +import { PaymentReportsController } from "./payment-reports.controller"; +import { AdminPaymentReportsGuard } from "./guards/admin-payment-reports.guard"; + +@Module({ + imports: [TopcoderReportsModule], + controllers: [PaymentReportsController], + providers: [AdminPaymentReportsGuard, CsvSerializer, CsvResponseInterceptor], +}) +export class PaymentReportsModule {} diff --git a/src/reports/report-directory.data.spec.ts b/src/reports/report-directory.data.spec.ts index 8e233bf..7d255df 100644 --- a/src/reports/report-directory.data.spec.ts +++ b/src/reports/report-directory.data.spec.ts @@ -6,11 +6,19 @@ import { describe("getAccessibleReportsDirectory", () => { it("returns the full directory for administrators", () => { - expect( - getAccessibleReportsDirectory({ - roles: [AdminRoles.Admin], - }), - ).toEqual(REPORTS_DIRECTORY); + const directory = getAccessibleReportsDirectory({ + roles: [AdminRoles.Admin], + }); + + expect(directory).toEqual(REPORTS_DIRECTORY); + expect(directory.payment?.reports.map((report) => report.path)).toEqual([ + "/payment/member-payment-accrual", + "/payment/member-payment-accrual-taas", + "/payment/member-payment-accrual-topgear", + "/payment/member-payment-accrual-engagement", + "/payment/member-payment-accrual-task", + "/payment/member-payment-accrual-challenge", + ]); }); it("returns public reports plus all challenge reports for product managers", () => { @@ -85,14 +93,12 @@ describe("getAccessibleReportsDirectory", () => { expect( directory.topcoder?.reports.map((report) => report.path), ).not.toContain("/topcoder/member-payment-accrual"); + expect(directory.payment).toBeUndefined(); expect(directory.member?.reports.map((report) => report.path)).toEqual([ "/member/engagement-data", "/member/recent-member-data", "/member/search", ]); - expect(directory.admin?.reports.map((report) => report.path)).toEqual([ - "/admin/member-payment-accrual", - ]); }); it("returns an empty directory when no JWT user is present", () => { diff --git a/src/reports/report-directory.data.ts b/src/reports/report-directory.data.ts index bc12790..a5cdf0a 100644 --- a/src/reports/report-directory.data.ts +++ b/src/reports/report-directory.data.ts @@ -1,5 +1,10 @@ import { Scopes as AppScopes } from "../app-constants"; -import { AuthUserLike, hasAccessToScopes } from "../auth/permissions.util"; +import { + AuthUserLike, + getNormalizedRoles, + hasAccessToScopes, + hasAdminRole, +} from "../auth/permissions.util"; import { ChallengeStatus } from "./challenges/dtos/challenge-status.enum"; export type ReportGroupKey = @@ -8,7 +13,7 @@ export type ReportGroupKey = | "statistics" | "topcoder" | "member" - | "admin" + | "payment" | "identity"; type HttpMethod = "GET" | "POST"; @@ -52,6 +57,7 @@ export type ReportsDirectory = Partial>; type RegisteredReport = AvailableReport & { requiredScopes: readonly string[]; + adminOnly?: boolean; }; type RegisteredReportGroup = Omit & { @@ -146,6 +152,16 @@ const topcoderReport = ( parameters, ); +const adminOnlyTopcoderReport = ( + name: string, + path: string, + description: string, + parameters: ReportParameter[] = [], +): RegisteredReport => ({ + ...topcoderReport(name, path, description, parameters), + adminOnly: true, +}); + const publicReport = ( name: string, path: string, @@ -795,16 +811,46 @@ const REGISTERED_REPORTS_DIRECTORY: RegisteredReportsDirectory = { ), ], }, - admin: { - label: "Admin Reports", - basePath: "/admin", + payment: { + label: "Payment Reports", + basePath: "/payment", reports: [ - topcoderReport( + adminOnlyTopcoderReport( "Member Payment Accrual", - "/admin/member-payment-accrual", + "/payment/member-payment-accrual", "Member payment accruals for the provided date range (defaults to last 3 months)", [paymentsStartDateParam, paymentsEndDateParam], ), + adminOnlyTopcoderReport( + "Member Payment Accrual-TaaS", + "/payment/member-payment-accrual-taas", + "Member payment accruals for TaaS payments for the provided date range (defaults to last 3 months)", + [paymentsStartDateParam, paymentsEndDateParam], + ), + adminOnlyTopcoderReport( + "Member Payment Accrual-Topgear", + "/payment/member-payment-accrual-topgear", + "Member payment accruals for Topgear payments for the provided date range (defaults to last 3 months)", + [paymentsStartDateParam, paymentsEndDateParam], + ), + adminOnlyTopcoderReport( + "Member Payment Accrual-Engagement", + "/payment/member-payment-accrual-engagement", + "Member payment accruals for engagement payments for the provided date range (defaults to last 3 months)", + [paymentsStartDateParam, paymentsEndDateParam], + ), + adminOnlyTopcoderReport( + "Member Payment Accrual-Task", + "/payment/member-payment-accrual-task", + "Member payment accruals for task payments for the provided date range (defaults to last 3 months)", + [paymentsStartDateParam, paymentsEndDateParam], + ), + adminOnlyTopcoderReport( + "Member Payment Accrual-Challenge", + "/payment/member-payment-accrual-challenge", + "Member payment accruals for challenge payments (contest, review board, copilot, checkpoint, and related challenge payouts) for the provided date range (defaults to last 3 months)", + [paymentsStartDateParam, paymentsEndDateParam], + ), ], }, }; @@ -862,11 +908,18 @@ export function getAccessibleReportsDirectory( return {}; } + const normalizedRoles = getNormalizedRoles(authUser); + const isAdmin = hasAdminRole(normalizedRoles); + const accessibleGroups = Object.entries(REGISTERED_REPORTS_DIRECTORY).flatMap( ([key, group]) => { - const accessibleReports = group.reports.filter((reportDefinition) => - hasAccessToScopes(authUser, reportDefinition.requiredScopes), - ); + const accessibleReports = group.reports.filter((reportDefinition) => { + if (reportDefinition.adminOnly && !isAdmin) { + return false; + } + + return hasAccessToScopes(authUser, reportDefinition.requiredScopes); + }); if (!accessibleReports.length) { return []; diff --git a/src/reports/topcoder/topcoder-reports.controller.ts b/src/reports/topcoder/topcoder-reports.controller.ts index dff2e30..9c476d4 100644 --- a/src/reports/topcoder/topcoder-reports.controller.ts +++ b/src/reports/topcoder/topcoder-reports.controller.ts @@ -10,7 +10,6 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import { TopcoderReportsService } from "./topcoder-reports.service"; import { ChallengeSubmitterDataQueryDto } from "./dto/challenge-submitter-data.dto"; import { RegistrantCountriesQueryDto } from "./dto/registrant-countries.dto"; -import { MemberPaymentAccrualQueryDto } from "./dto/member-payment-accrual.dto"; import { RecentMemberDataQueryDto } from "./dto/recent-member-data.dto"; import { WeeklyMemberParticipationQueryDto } from "./dto/weekly-member-participation.dto"; import { CompletedProfilesQueryDto } from "./dto/completed-profiles.dto"; @@ -88,16 +87,6 @@ export class TopcoderReportsController { return this.reports.getWeeklyMemberParticipation(startDate, endDate); } - @Get("/admin/member-payment-accrual") - @ApiOperation({ - summary: - "Member payment accruals for the provided date range (defaults to last 3 months)", - }) - getMemberPaymentAccrual(@Query() query: MemberPaymentAccrualQueryDto) { - const { startDate, endDate } = query; - return this.reports.getMemberPaymentAccrual(startDate, endDate); - } - @Get("/member/recent-member-data") @RequiredScopes( AppScopes.AllReports, diff --git a/src/reports/topcoder/topcoder-reports.module.ts b/src/reports/topcoder/topcoder-reports.module.ts index e92ab4c..0c7a401 100644 --- a/src/reports/topcoder/topcoder-reports.module.ts +++ b/src/reports/topcoder/topcoder-reports.module.ts @@ -15,5 +15,6 @@ import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.i CsvSerializer, CsvResponseInterceptor, ], + exports: [TopcoderReportsService], }) export class TopcoderReportsModule {} diff --git a/src/reports/topcoder/topcoder-reports.service.ts b/src/reports/topcoder/topcoder-reports.service.ts index eb884db..0db9889 100644 --- a/src/reports/topcoder/topcoder-reports.service.ts +++ b/src/reports/topcoder/topcoder-reports.service.ts @@ -555,11 +555,16 @@ export class TopcoderReportsService implements OnModuleDestroy { })); } - async getMemberPaymentAccrual(startDate?: string, endDate?: string) { + async getMemberPaymentAccrual( + startDate?: string, + endDate?: string, + paymentTypes?: string[], + ) { const query = this.sql.load("reports/topcoder/member-payment-accrual.sql"); const rows = await this.db.query(query, [ startDate ?? null, endDate ?? null, + paymentTypes?.length ? paymentTypes : null, ]); return rows.map((row) => ({ paymentCreatedAt: this.normalizeDate(row.payment_created_at), From dd7f1954580cdaaac75d44e06567cd10fde213af Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 23 Apr 2026 12:26:18 +0300 Subject: [PATCH 3/9] Refactor payments reports to standalone module --- .../member-payment-accrual.sql | 0 .../dto/member-payment-accrual.dto.ts | 0 .../payment/payment-reports.controller.ts | 6 +- src/reports/payment/payment-reports.module.ts | 12 ++- .../payment/payment-reports.service.spec.ts | 75 +++++++++++++++++ .../payment/payment-reports.service.ts | 80 +++++++++++++++++++ .../topcoder/topcoder-reports.module.ts | 1 - .../topcoder/topcoder-reports.service.ts | 48 ----------- 8 files changed, 167 insertions(+), 55 deletions(-) rename sql/reports/{topcoder => payment}/member-payment-accrual.sql (100%) rename src/reports/{topcoder => payment}/dto/member-payment-accrual.dto.ts (100%) create mode 100644 src/reports/payment/payment-reports.service.spec.ts create mode 100644 src/reports/payment/payment-reports.service.ts diff --git a/sql/reports/topcoder/member-payment-accrual.sql b/sql/reports/payment/member-payment-accrual.sql similarity index 100% rename from sql/reports/topcoder/member-payment-accrual.sql rename to sql/reports/payment/member-payment-accrual.sql diff --git a/src/reports/topcoder/dto/member-payment-accrual.dto.ts b/src/reports/payment/dto/member-payment-accrual.dto.ts similarity index 100% rename from src/reports/topcoder/dto/member-payment-accrual.dto.ts rename to src/reports/payment/dto/member-payment-accrual.dto.ts diff --git a/src/reports/payment/payment-reports.controller.ts b/src/reports/payment/payment-reports.controller.ts index 890caa2..e3157b3 100644 --- a/src/reports/payment/payment-reports.controller.ts +++ b/src/reports/payment/payment-reports.controller.ts @@ -6,8 +6,8 @@ import { UseInterceptors, } from "@nestjs/common"; import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; -import { TopcoderReportsService } from "../topcoder/topcoder-reports.service"; -import { MemberPaymentAccrualQueryDto } from "../topcoder/dto/member-payment-accrual.dto"; +import { PaymentReportsService } from "./payment-reports.service"; +import { MemberPaymentAccrualQueryDto } from "./dto/member-payment-accrual.dto"; import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; import { AdminPaymentReportsGuard } from "./guards/admin-payment-reports.guard"; @@ -17,7 +17,7 @@ import { AdminPaymentReportsGuard } from "./guards/admin-payment-reports.guard"; @UseInterceptors(CsvResponseInterceptor) @Controller() export class PaymentReportsController { - constructor(private readonly reports: TopcoderReportsService) {} + constructor(private readonly reports: PaymentReportsService) {} @Get("/payment/member-payment-accrual") @ApiOperation({ diff --git a/src/reports/payment/payment-reports.module.ts b/src/reports/payment/payment-reports.module.ts index 80abf0b..2a39fe8 100644 --- a/src/reports/payment/payment-reports.module.ts +++ b/src/reports/payment/payment-reports.module.ts @@ -1,13 +1,19 @@ import { Module } from "@nestjs/common"; import { CsvSerializer } from "../../common/csv/csv-serializer"; import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; -import { TopcoderReportsModule } from "../topcoder/topcoder-reports.module"; +import { SqlLoaderService } from "../../common/sql-loader.service"; import { PaymentReportsController } from "./payment-reports.controller"; import { AdminPaymentReportsGuard } from "./guards/admin-payment-reports.guard"; +import { PaymentReportsService } from "./payment-reports.service"; @Module({ - imports: [TopcoderReportsModule], controllers: [PaymentReportsController], - providers: [AdminPaymentReportsGuard, CsvSerializer, CsvResponseInterceptor], + providers: [ + PaymentReportsService, + SqlLoaderService, + AdminPaymentReportsGuard, + CsvSerializer, + CsvResponseInterceptor, + ], }) export class PaymentReportsModule {} diff --git a/src/reports/payment/payment-reports.service.spec.ts b/src/reports/payment/payment-reports.service.spec.ts new file mode 100644 index 0000000..978c934 --- /dev/null +++ b/src/reports/payment/payment-reports.service.spec.ts @@ -0,0 +1,75 @@ +import { DbService } from "../../db/db.service"; +import { SqlLoaderService } from "../../common/sql-loader.service"; +import { PaymentReportsService } from "./payment-reports.service"; + +describe("PaymentReportsService", () => { + let service: PaymentReportsService; + let db: { query: jest.Mock }; + let sql: { load: jest.Mock }; + + beforeEach(() => { + db = { + query: jest.fn().mockResolvedValue([ + { + payment_created_at: new Date("2024-01-15T12:00:00.000Z"), + payment_id: "payment-123", + payment_description: "Challenge payout", + challenge_id: "3001", + payment_status: "OWED", + payment_type: "Challenge Payment", + payee_handle: "tourist", + payment_method: "PayPal", + billing_account_name: "BA One", + customer_name: "Client One", + reporting_account_name: "End Customer One", + member_id: "4001", + challenge_created_date: "2024-01-01", + user_payment_gross_amount: "150.50", + }, + ]), + }; + + sql = { + load: jest.fn().mockReturnValue("SELECT 1"), + }; + + service = new PaymentReportsService( + db as unknown as DbService, + sql as unknown as SqlLoaderService, + ); + }); + + it("loads the payment SQL and maps the accrual rows", async () => { + await expect( + service.getMemberPaymentAccrual("2024-01-01", "2024-01-31", [ + "Challenge Payment", + ]), + ).resolves.toEqual([ + { + paymentCreatedAt: "2024-01-15T12:00:00.000Z", + paymentId: "payment-123", + paymentDescription: "Challenge payout", + challengeId: "3001", + paymentStatus: "OWED", + paymentType: "Challenge Payment", + payeeHandle: "tourist", + payeePaymentMethod: "PayPal", + billingAccountName: "BA One", + customerName: "Client One", + reportingAccountName: "End Customer One", + memberId: "4001", + challengeCreatedAt: "2024-01-01", + userPaymentGrossAmount: 150.5, + }, + ]); + + expect(sql.load).toHaveBeenCalledWith( + "reports/payment/member-payment-accrual.sql", + ); + expect(db.query).toHaveBeenCalledWith("SELECT 1", [ + "2024-01-01", + "2024-01-31", + ["Challenge Payment"], + ]); + }); +}); diff --git a/src/reports/payment/payment-reports.service.ts b/src/reports/payment/payment-reports.service.ts new file mode 100644 index 0000000..8ab1d79 --- /dev/null +++ b/src/reports/payment/payment-reports.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from "@nestjs/common"; +import { DbService } from "../../db/db.service"; +import { SqlLoaderService } from "../../common/sql-loader.service"; + +type MemberPaymentAccrualRow = { + payment_created_at: Date | string | null; + payment_id: string | null; + payment_description: string | null; + challenge_id: string | null; + payment_status: string | null; + payment_type: string | null; + payee_handle: string | null; + payment_method: string | null; + billing_account_name: string | null; + customer_name: string | null; + reporting_account_name: string | null; + member_id: string | null; + challenge_created_date: string | null; + user_payment_gross_amount: string | number | null; +}; + +@Injectable() +export class PaymentReportsService { + constructor( + private readonly db: DbService, + private readonly sql: SqlLoaderService, + ) {} + + async getMemberPaymentAccrual( + startDate?: string, + endDate?: string, + paymentTypes?: string[], + ) { + const query = this.sql.load("reports/payment/member-payment-accrual.sql"); + const rows = await this.db.query(query, [ + startDate ?? null, + endDate ?? null, + paymentTypes?.length ? paymentTypes : null, + ]); + + return rows.map((row) => ({ + paymentCreatedAt: this.normalizeDate(row.payment_created_at), + paymentId: row.payment_id ?? null, + paymentDescription: row.payment_description ?? null, + challengeId: row.challenge_id ?? null, + paymentStatus: row.payment_status ?? null, + paymentType: row.payment_type ?? null, + payeeHandle: row.payee_handle ?? null, + payeePaymentMethod: row.payment_method ?? null, + billingAccountName: row.billing_account_name ?? null, + customerName: row.customer_name ?? null, + reportingAccountName: row.reporting_account_name ?? null, + memberId: row.member_id ?? null, + challengeCreatedAt: row.challenge_created_date ?? null, + userPaymentGrossAmount: this.toNullableNumber( + row.user_payment_gross_amount, + ), + })); + } + + private toNullableNumber(value: string | number | null | undefined) { + if (value === null || value === undefined) { + return null; + } + + return Number(value); + } + + private normalizeDate(value: Date | string | null | undefined) { + if (value === null || value === undefined) { + return null; + } + + if (value instanceof Date) { + return value.toISOString(); + } + + return value; + } +} diff --git a/src/reports/topcoder/topcoder-reports.module.ts b/src/reports/topcoder/topcoder-reports.module.ts index 0c7a401..e92ab4c 100644 --- a/src/reports/topcoder/topcoder-reports.module.ts +++ b/src/reports/topcoder/topcoder-reports.module.ts @@ -15,6 +15,5 @@ import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.i CsvSerializer, CsvResponseInterceptor, ], - exports: [TopcoderReportsService], }) export class TopcoderReportsModule {} diff --git a/src/reports/topcoder/topcoder-reports.service.ts b/src/reports/topcoder/topcoder-reports.service.ts index 0db9889..8fc095a 100644 --- a/src/reports/topcoder/topcoder-reports.service.ts +++ b/src/reports/topcoder/topcoder-reports.service.ts @@ -28,23 +28,6 @@ type MarathonMatchStatsRow = { marathon_submission_rate: string | number | null; }; -type MemberPaymentAccrualRow = { - payment_created_at: Date | string | null; - payment_id: string | null; - payment_description: string | null; - challenge_id: string | null; - payment_status: string | null; - payment_type: string | null; - payee_handle: string | null; - payment_method: string | null; - billing_account_name: string | null; - customer_name: string | null; - reporting_account_name: string | null; - member_id: string | null; - challenge_created_date: string | null; - user_payment_gross_amount: string | number | null; -}; - type RecentMemberDataRow = { handle: string | null; email: string | null; @@ -555,37 +538,6 @@ export class TopcoderReportsService implements OnModuleDestroy { })); } - async getMemberPaymentAccrual( - startDate?: string, - endDate?: string, - paymentTypes?: string[], - ) { - const query = this.sql.load("reports/topcoder/member-payment-accrual.sql"); - const rows = await this.db.query(query, [ - startDate ?? null, - endDate ?? null, - paymentTypes?.length ? paymentTypes : null, - ]); - return rows.map((row) => ({ - paymentCreatedAt: this.normalizeDate(row.payment_created_at), - paymentId: row.payment_id ?? null, - paymentDescription: row.payment_description ?? null, - challengeId: row.challenge_id ?? null, - paymentStatus: row.payment_status ?? null, - paymentType: row.payment_type ?? null, - payeeHandle: row.payee_handle ?? null, - payeePaymentMethod: row.payment_method ?? null, - billingAccountName: row.billing_account_name ?? null, - customerName: row.customer_name ?? null, - reportingAccountName: row.reporting_account_name ?? null, - memberId: row.member_id ?? null, - challengeCreatedAt: row.challenge_created_date ?? null, - userPaymentGrossAmount: this.toNullableNumber( - row.user_payment_gross_amount, - ), - })); - } - async getRecentMemberData(startDate?: string) { const query = this.sql.load("reports/topcoder/recent-member-data.sql"); const rows = await this.db.query(query, [ From ae4024c053df4df251f22b7ac0dab5d7de89d000 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 23 Apr 2026 12:26:52 +0300 Subject: [PATCH 4/9] deploy to dev --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a40302..6c9332b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,6 +70,7 @@ workflows: - PM-4491-fix - PM-3497_talent-search - PM-4886 + - PM-4877_payments-reports # Production builds are exectuted only on tagged commits to the # master branch. From 4252f4511f9a8dbcfa3bf53f3a6df6eefd7ce251 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 27 Apr 2026 13:25:05 +0530 Subject: [PATCH 5/9] Deploy completeness query --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a40302..1a73711 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,7 +69,7 @@ workflows: - PM-4490 - PM-4491-fix - PM-3497_talent-search - - PM-4886 + - PM-4922 # Production builds are exectuted only on tagged commits to the # master branch. From 4617424bb81eb578eaea886e8a906721f3b680a3 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 27 Apr 2026 15:48:08 +0530 Subject: [PATCH 6/9] Optimise query --- src/reports/member/member-search.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reports/member/member-search.service.ts b/src/reports/member/member-search.service.ts index 0a0f4ca..d760b7b 100644 --- a/src/reports/member/member-search.service.ts +++ b/src/reports/member/member-search.service.ts @@ -291,6 +291,7 @@ member_address AS ( OR ( mtp2.value::jsonb ? 'availability' AND mtp2.value::jsonb ? 'preferredRoles' + AND jsonb_typeof(mtp2.value::jsonb -> 'preferredRoles') = 'array' AND jsonb_array_length(mtp2.value::jsonb -> 'preferredRoles') > 0 ) ) @@ -360,8 +361,7 @@ LIMIT ${pLimit} OFFSET ${pOffset}`; const countQuery = ` WITH ${ctesBlock} SELECT COUNT(*)::integer AS total -FROM filtered_members fm -${profileComplete === true ? `INNER JOIN profile_complete_filtered pcf ON pcf.user_id = fm.user_id` : ""}`; +FROM ${profileComplete === true ? "profile_complete_filtered pcf" : "filtered_members fm"}`; const [rows, countRows] = await Promise.all([ this.db.query(dataQuery, params), From 818f31d6cdec19b7792f77cf5e76e2fa3dc2c16a Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 27 Apr 2026 16:17:03 +0530 Subject: [PATCH 7/9] Add test coverage --- .../member/member-search.service.spec.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/reports/member/member-search.service.spec.ts b/src/reports/member/member-search.service.spec.ts index d83ae52..6f662fa 100644 --- a/src/reports/member/member-search.service.spec.ts +++ b/src/reports/member/member-search.service.spec.ts @@ -145,6 +145,56 @@ describe("MemberSearchService", () => { expect(dataSql).not.toContain("COALESCE(m.verified, false) = true"); }); + it("adds profileComplete CTE/join only when enabled and keeps count params free of pagination", async () => { + mockDbService.query + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ total: 0 }]); + + await service.search({ + countries: ["us"], + profileComplete: true, + page: 3, + limit: 7, + }); + + const enabledDataSql = mockDbService.query.mock.calls[0][0] as string; + const enabledDataParams = mockDbService.query.mock.calls[0][1] as unknown[]; + const enabledCountSql = mockDbService.query.mock.calls[1][0] as string; + const enabledCountParams = mockDbService.query.mock.calls[1][1] as unknown[]; + + expect(enabledDataSql).toContain("profile_complete_filtered AS ("); + expect(enabledDataSql).toContain( + "INNER JOIN profile_complete_filtered pcf ON pcf.user_id = m.\"userId\"", + ); + expect(enabledCountSql).toContain("FROM profile_complete_filtered pcf"); + expect(enabledCountSql).not.toContain( + "INNER JOIN profile_complete_filtered pcf ON pcf.user_id = fm.user_id", + ); + expect(enabledDataParams).toEqual([["us"], 7, 14]); + expect(enabledCountParams).toEqual([["us"]]); + + mockDbService.query.mockReset(); + mockDbService.query + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{ total: 0 }]); + + await service.search({ + countries: ["us"], + page: 3, + limit: 7, + }); + + const disabledDataSql = mockDbService.query.mock.calls[0][0] as string; + const disabledCountSql = mockDbService.query.mock.calls[1][0] as string; + + expect(disabledDataSql).not.toContain("profile_complete_filtered AS ("); + expect(disabledDataSql).not.toContain( + "INNER JOIN profile_complete_filtered pcf ON pcf.user_id = m.\"userId\"", + ); + expect(disabledCountSql).toContain("FROM filtered_members fm"); + expect(disabledCountSql).not.toContain("profile_complete_filtered pcf"); + }); + it("deduplicates skills and keeps last wins value when building skill query", async () => { const skillA = "550e8400-e29b-41d4-a716-446655440000"; const skillB = "550e8400-e29b-41d4-a716-446655440001"; From 0fa936cbeb59c146c4789dd61eaf9e1507c24068 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 28 Apr 2026 13:45:12 +1000 Subject: [PATCH 8/9] PM-4934: Fix SFDC payments CSV export What was broken Reports app CSV downloads for SFDC reports, including Payments, saved JSON response bodies into .csv files. Root cause The SFDC reports controller did not use the existing CSV response interceptor, so Accept: text/csv requests still returned the controller data as JSON. What was changed Enabled application/json and text/csv production on the SFDC reports controller, attached the existing CsvResponseInterceptor, and registered the CSV serializer/interceptor providers in the SFDC reports module. Any added/updated tests Added SFDC controller and module coverage for CSV interceptor wiring. Updated the BA fees optional groupBy assertion to accept an omitted optional property while still verifying the value is undefined. --- .../sfdc/sfdc-reports.controller.spec.ts | 19 ++++++++++++++----- src/reports/sfdc/sfdc-reports.controller.ts | 12 +++++++++++- src/reports/sfdc/sfdc-reports.module.spec.ts | 14 ++++++++++++-- src/reports/sfdc/sfdc-reports.module.ts | 11 +++++++++-- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/reports/sfdc/sfdc-reports.controller.spec.ts b/src/reports/sfdc/sfdc-reports.controller.spec.ts index 2ae4c69..2de3b51 100644 --- a/src/reports/sfdc/sfdc-reports.controller.spec.ts +++ b/src/reports/sfdc/sfdc-reports.controller.spec.ts @@ -1,7 +1,10 @@ import "reflect-metadata"; import { BadRequestException } from "@nestjs/common"; +import { INTERCEPTORS_METADATA } from "@nestjs/common/constants"; import { Test, TestingModule } from "@nestjs/testing"; import { plainToInstance } from "class-transformer"; +import { CsvSerializer } from "../../common/csv/csv-serializer"; +import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; import { BaFeesReportQueryDto, ChallengesReportQueryDto, @@ -55,6 +58,8 @@ describe("SfdcReportsController", () => { const moduleRef: TestingModule = await Test.createTestingModule({ controllers: [SfdcReportsController], providers: [ + CsvSerializer, + CsvResponseInterceptor, { provide: SfdcReportsService, useValue: mockSfdcReportsService, @@ -69,6 +74,13 @@ describe("SfdcReportsController", () => { expect(controller).toBeDefined(); }); + it("enables CSV response serialization for report downloads", () => { + const interceptors = + Reflect.getMetadata(INTERCEPTORS_METADATA, SfdcReportsController) ?? []; + + expect(interceptors).toContain(CsvResponseInterceptor); + }); + it("returns the challenges report on success", async () => { mockSfdcReportsService.getChallengesReport.mockResolvedValue( normalizedChallengeData, @@ -626,11 +638,8 @@ describe("SfdcReportsController", () => { await controller.getBaFeesReport(mockBaFeesQueryDto.byBillingAccount); - expect(mockSfdcReportsService.getBaFeesReport).toHaveBeenCalledWith( - expect.objectContaining({ - groupBy: undefined, - }), - ); + const [query] = mockSfdcReportsService.getBaFeesReport.mock.calls[0]; + expect(query.groupBy).toBeUndefined(); }); }); }); diff --git a/src/reports/sfdc/sfdc-reports.controller.ts b/src/reports/sfdc/sfdc-reports.controller.ts index d922967..59db229 100644 --- a/src/reports/sfdc/sfdc-reports.controller.ts +++ b/src/reports/sfdc/sfdc-reports.controller.ts @@ -1,8 +1,15 @@ -import { Controller, Get, Query, UseGuards } from "@nestjs/common"; +import { + Controller, + Get, + Query, + UseGuards, + UseInterceptors, +} from "@nestjs/common"; import { ApiBearerAuth, ApiOperation, + ApiProduces, ApiResponse, ApiTags, } from "@nestjs/swagger"; @@ -10,6 +17,7 @@ import { import { PermissionsGuard } from "../../auth/guards/permissions.guard"; import { Scopes } from "../../auth/decorators/scopes.decorator"; import { Scopes as AppScopes } from "../../app-constants"; +import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; import { SfdcReportsService } from "./sfdc-reports.service"; import { @@ -31,6 +39,8 @@ import { import { ResponseDto } from "src/dto/api-response.dto"; @ApiTags("Sfdc Reports") +@ApiProduces("application/json", "text/csv") +@UseInterceptors(CsvResponseInterceptor) @Controller("/sfdc") export class SfdcReportsController { constructor(private readonly reportsService: SfdcReportsService) {} diff --git a/src/reports/sfdc/sfdc-reports.module.spec.ts b/src/reports/sfdc/sfdc-reports.module.spec.ts index 7bbcba4..2c7ae10 100644 --- a/src/reports/sfdc/sfdc-reports.module.spec.ts +++ b/src/reports/sfdc/sfdc-reports.module.spec.ts @@ -1,5 +1,8 @@ import { Test, TestingModule } from "@nestjs/testing"; +import { CsvResponseInterceptor } from "src/common/interceptors/csv-response.interceptor"; +import { CsvSerializer } from "src/common/csv/csv-serializer"; import { SqlLoaderService } from "src/common/sql-loader.service"; +import { DbModule } from "../../db/db.module"; import { DbService } from "../../db/db.service"; import { SfdcReportsController } from "./sfdc-reports.controller"; import { SfdcReportsModule } from "./sfdc-reports.module"; @@ -23,9 +26,10 @@ describe("SfdcReportsModule", () => { mockSqlLoaderService.load.mockReturnValue("SELECT 1"); moduleRef = await Test.createTestingModule({ - imports: [SfdcReportsModule], - providers: [{ provide: DbService, useValue: mockDbService }], + imports: [DbModule, SfdcReportsModule], }) + .overrideProvider(DbService) + .useValue(mockDbService) .overrideProvider(SqlLoaderService) .useValue(mockSqlLoaderService) .compile(); @@ -41,6 +45,12 @@ describe("SfdcReportsModule", () => { expect(moduleRef.get(SqlLoaderService)).toBe( mockSqlLoaderService, ); + expect(moduleRef.get(CsvSerializer)).toBeInstanceOf( + CsvSerializer, + ); + expect( + moduleRef.get(CsvResponseInterceptor), + ).toBeInstanceOf(CsvResponseInterceptor); }); it("injects mocked dependencies into the service", async () => { diff --git a/src/reports/sfdc/sfdc-reports.module.ts b/src/reports/sfdc/sfdc-reports.module.ts index e6d85e5..7e2f3f5 100644 --- a/src/reports/sfdc/sfdc-reports.module.ts +++ b/src/reports/sfdc/sfdc-reports.module.ts @@ -1,10 +1,17 @@ import { Module } from "@nestjs/common"; +import { CsvSerializer } from "../../common/csv/csv-serializer"; +import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; +import { SqlLoaderService } from "../../common/sql-loader.service"; import { SfdcReportsController } from "./sfdc-reports.controller"; import { SfdcReportsService } from "./sfdc-reports.service"; -import { SqlLoaderService } from "../../common/sql-loader.service"; @Module({ controllers: [SfdcReportsController], - providers: [SfdcReportsService, SqlLoaderService], + providers: [ + SfdcReportsService, + SqlLoaderService, + CsvSerializer, + CsvResponseInterceptor, + ], }) export class SfdcReportsModule {} From 8ee506cfd83c458e27417ad8c7aa5db41eeb6ced Mon Sep 17 00:00:00 2001 From: Harshit Chudasama Date: Tue, 28 Apr 2026 13:39:35 +0530 Subject: [PATCH 9/9] test --- .circleci/config.yml | 2 +- sql/reports/payment/member-payment-accrual.sql | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 95a1992..e6d288d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,7 +64,7 @@ workflows: branches: only: - develop - - PM-4922 + - PM-4949 # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/sql/reports/payment/member-payment-accrual.sql b/sql/reports/payment/member-payment-accrual.sql index 8970104..10780f4 100644 --- a/sql/reports/payment/member-payment-accrual.sql +++ b/sql/reports/payment/member-payment-accrual.sql @@ -83,8 +83,10 @@ SELECT cl."name" AS customer_name, ba."subcontractingEndCustomer" AS reporting_account_name, cp.winner_id AS member_id, - to_char(c."createdAt", 'YYYY-MM-DD') AS challenge_created_date, - cp.gross_amount AS user_payment_gross_amount +CASE + WHEN cp.payment_type = 'Engagement Payment' THEN to_char(cp.payment_created_at, 'YYYY-MM-DD') + ELSE to_char(c."createdAt", 'YYYY-MM-DD') + END AS challenge_created_date, cp.gross_amount AS user_payment_gross_amount FROM categorized_payments cp LEFT JOIN challenges."Challenge" c ON c."id" = cp.challenge_id