diff --git a/sql/reports/topcoder/weekly-member-participation.sql b/sql/reports/topcoder/weekly-member-participation.sql index 6a22b56..27c108b 100644 --- a/sql/reports/topcoder/weekly-member-participation.sql +++ b/sql/reports/topcoder/weekly-member-participation.sql @@ -1,7 +1,22 @@ -WITH window_bounds AS ( +WITH provided_dates AS ( SELECT - (DATE_TRUNC('week', CURRENT_DATE) - INTERVAL '4 weeks') AS window_start, - (DATE_TRUNC('week', CURRENT_DATE) + INTERVAL '1 week') AS window_end + NULLIF($1, '')::timestamptz AS start_date, + NULLIF($2, '')::timestamptz AS end_date +), +window_bounds AS ( + SELECT + COALESCE( + pd.start_date, + CASE + WHEN pd.end_date IS NOT NULL THEN pd.end_date - INTERVAL '5 weeks' + ELSE DATE_TRUNC('week', CURRENT_DATE) - INTERVAL '4 weeks' + END + ) AS window_start, + COALESCE( + pd.end_date, + DATE_TRUNC('week', CURRENT_DATE) + INTERVAL '1 week' + ) AS window_end + FROM provided_dates pd ), billing AS ( SELECT @@ -18,7 +33,7 @@ billing AS ( LEFT JOIN projects.projects proj ON proj.id::text = NULLIF(TRIM(c."projectId"::text), '') LEFT JOIN "billing-accounts"."BillingAccount" project_ba - ON project_ba.id = proj."billingAccountId" + ON project_ba.id::text = NULLIF(TRIM(proj."billingAccountId"::text), '') GROUP BY c.id ), project_clients AS ( diff --git a/src/auth/auth.middleware.ts b/src/auth/auth.middleware.ts index 39945f5..a95d13d 100644 --- a/src/auth/auth.middleware.ts +++ b/src/auth/auth.middleware.ts @@ -44,8 +44,7 @@ function decodeTokenPayload(token: string): Record | null { return null; } const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/"); - const padded = - payload + "=".repeat((4 - (payload.length % 4)) % 4); + const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4); const decoded = Buffer.from(padded, "base64").toString("utf8"); return JSON.parse(decoded); } catch { diff --git a/src/common/validation.util.ts b/src/common/validation.util.ts index e3ed5e7..f7362d4 100644 --- a/src/common/validation.util.ts +++ b/src/common/validation.util.ts @@ -25,5 +25,7 @@ export function transformArray({ value }: { value: unknown }) { return [v]; }; - return Array.isArray(value) ? value.flatMap(splitIfString) : splitIfString(value); + return Array.isArray(value) + ? value.flatMap(splitIfString) + : splitIfString(value); } diff --git a/src/reports/report-directory.data.ts b/src/reports/report-directory.data.ts index b643ed9..03c8681 100644 --- a/src/reports/report-directory.data.ts +++ b/src/reports/report-directory.data.ts @@ -446,7 +446,8 @@ export const REPORTS_DIRECTORY: ReportsDirectory = { report( "Weekly Member Participation", "/topcoder/weekly-member-participation", - "Weekly distinct registrants and submitters for the last five weeks", + "Weekly distinct registrants and submitters for the provided date range (defaults to last five weeks)", + [paymentsStartDateParam, paymentsEndDateParam], ), report( "Member Payment Accrual", diff --git a/src/reports/sfdc/sfdc-reports.dto.spec.ts b/src/reports/sfdc/sfdc-reports.dto.spec.ts index 0a528bb..43be0da 100644 --- a/src/reports/sfdc/sfdc-reports.dto.spec.ts +++ b/src/reports/sfdc/sfdc-reports.dto.spec.ts @@ -732,11 +732,7 @@ describe("TaasResourceBookingsReportQueryDto validation", () => { billingAccountIds: "80001012,80002012 , 80003012", }); expect(errors).toHaveLength(0); - expect(dto.billingAccountIds).toEqual([ - "80001012", - "80002012", - "80003012", - ]); + expect(dto.billingAccountIds).toEqual(["80001012", "80002012", "80003012"]); }); it("accepts ISO date strings for startDate and endDate", async () => { diff --git a/src/reports/topcoder/dto/weekly-member-participation.dto.ts b/src/reports/topcoder/dto/weekly-member-participation.dto.ts new file mode 100644 index 0000000..65d82dc --- /dev/null +++ b/src/reports/topcoder/dto/weekly-member-participation.dto.ts @@ -0,0 +1,22 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsDateString, IsOptional } from "class-validator"; + +export class WeeklyMemberParticipationQueryDto { + @ApiPropertyOptional({ + description: + "Start date (inclusive) for filtering challenge posting date in ISO 8601 format", + example: "2024-01-01T00:00:00.000Z", + }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ + description: + "End date (exclusive) for filtering challenge posting date in ISO 8601 format", + example: "2024-02-01T00:00:00.000Z", + }) + @IsOptional() + @IsDateString() + endDate?: string; +} diff --git a/src/reports/topcoder/topcoder-reports.controller.ts b/src/reports/topcoder/topcoder-reports.controller.ts index 7f177b5..63aec82 100644 --- a/src/reports/topcoder/topcoder-reports.controller.ts +++ b/src/reports/topcoder/topcoder-reports.controller.ts @@ -11,6 +11,7 @@ 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 { WeeklyMemberParticipationQueryDto } from "./dto/weekly-member-participation.dto"; import { TopcoderReportsGuard } from "../../auth/guards/topcoder-reports.guard"; import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor"; @@ -63,10 +64,13 @@ export class TopcoderReportsController { @Get("/weekly-member-participation") @ApiOperation({ summary: - "Weekly distinct registrants and submitters for the last five weeks", + "Weekly distinct registrants and submitters for the provided date range (defaults to last five weeks)", }) - getWeeklyMemberParticipation() { - return this.reports.getWeeklyMemberParticipation(); + getWeeklyMemberParticipation( + @Query() query: WeeklyMemberParticipationQueryDto, + ) { + const { startDate, endDate } = query; + return this.reports.getWeeklyMemberParticipation(startDate, endDate); } @Get("/member-payment-accrual") diff --git a/src/reports/topcoder/topcoder-reports.service.ts b/src/reports/topcoder/topcoder-reports.service.ts index dcd0040..a298a3a 100644 --- a/src/reports/topcoder/topcoder-reports.service.ts +++ b/src/reports/topcoder/topcoder-reports.service.ts @@ -448,7 +448,7 @@ export class TopcoderReportsService { })); } - async getWeeklyMemberParticipation() { + async getWeeklyMemberParticipation(startDate?: string, endDate?: string) { const query = this.sql.load( "reports/topcoder/weekly-member-participation.sql", ); @@ -456,7 +456,7 @@ export class TopcoderReportsService { "challenge_stats.posting_date_week": string; "challenge_stats.count_distinct_registrant": string | number; "challenge_stats.count_distinct_submitter": string | number; - }>(query); + }>(query, [startDate ?? null, endDate ?? null]); return rows.map((row) => ({ "challenge_stats.posting_date_week": @@ -470,10 +470,7 @@ export class TopcoderReportsService { })); } - async getMemberPaymentAccrual( - startDate?: string, - endDate?: string, - ) { + async getMemberPaymentAccrual(startDate?: string, endDate?: string) { const query = this.sql.load("reports/topcoder/member-payment-accrual.sql"); const rows = await this.db.query(query, [ startDate ?? null, diff --git a/src/reports/topgear/topgear-reports.service.ts b/src/reports/topgear/topgear-reports.service.ts index 8763473..d8832d7 100644 --- a/src/reports/topgear/topgear-reports.service.ts +++ b/src/reports/topgear/topgear-reports.service.ts @@ -22,7 +22,8 @@ export class TopgearReportsService { } async getTopgearHandles(opts: { startDate?: string }) { - const startDate = parseOptionalDate(opts.startDate) ?? subDays(new Date(), 90); + const startDate = + parseOptionalDate(opts.startDate) ?? subDays(new Date(), 90); const query = this.sql.load("reports/topgear/topgear-handles.sql"); return this.db.query(query, [startDate.toISOString()]); }