Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

This repository houses the reports API for all Topcoder and Topgear reports on the Topcoder platform. The reports are pulled directly from live data, not a data warehouse, so they should be up-to-date when they are generated and the response is returned.

All reports will return JSON data with the expected fields for the individual report
Reports return JSON data by default. Endpoints that support CSV can also return
CSV when the request sets `Accept: text/csv` (including the Challenges and
Topcoder report groups).

## Security

Expand Down
46 changes: 44 additions & 2 deletions scripts/export-member-tax.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const path = require("path");
const { Pool } = require("pg");

const CSV_COLUMNS = [
"payment.payment_id",
"payee.handle",
"payee.email",
"payee.first_name",
Expand Down Expand Up @@ -336,6 +335,11 @@ function writeCsv(outputPath, rows) {
fs.writeFileSync(outputPath, `${lines.join("\n")}\n`, "utf8");
}

function toNumberOrZero(value) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The toNumberOrZero function is introduced to handle conversion of values to numbers with a fallback to zero. Ensure that this function is used consistently throughout the codebase wherever similar conversions are needed to maintain uniformity and prevent potential errors from inconsistent handling.

const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}

async function run() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
Expand Down Expand Up @@ -436,7 +440,45 @@ async function run() {
};
});

writeCsv(outputPath, mergedRows);
const aggregatedByUser = new Map();
for (const row of mergedRows) {
const userId = String(row.__user_id);
const existing = aggregatedByUser.get(userId);

if (!existing) {
aggregatedByUser.set(userId, {
...row,
"user_payment.gross_amount": toNumberOrZero(
row["user_payment.gross_amount"],
),
"user_payment.net_amount": toNumberOrZero(row["user_payment.net_amount"]),
});
continue;
}

existing["user_payment.gross_amount"] =
toNumberOrZero(existing["user_payment.gross_amount"]) +
toNumberOrZero(row["user_payment.gross_amount"]);
existing["user_payment.net_amount"] =
toNumberOrZero(existing["user_payment.net_amount"]) +
toNumberOrZero(row["user_payment.net_amount"]);
}

const aggregatedRows = Array.from(aggregatedByUser.values())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The use of Array.from(aggregatedByUser.values()) followed by map and sort is correct but could be optimized. Consider chaining these operations directly on the Map to avoid creating an intermediate array, which could improve performance slightly, especially with large datasets.

.map((row) => {
const normalized = { ...row };
delete normalized.__user_id;
delete normalized.__payment_id;
delete normalized["payment.payment_id"];
return normalized;
})
.sort((a, b) =>
String(a["payee.handle"] ?? "").localeCompare(
String(b["payee.handle"] ?? ""),
),
);

writeCsv(outputPath, aggregatedRows);
console.log(`[member-tax-export] Wrote CSV: ${outputPath}`);
} finally {
await Promise.allSettled([mainPool.end(), oldPaymentsPool.end()]);
Expand Down
12 changes: 11 additions & 1 deletion src/reports/challenges/challenges-reports.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
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";
Expand All @@ -11,6 +18,7 @@ import { PaymentsReportResponse } from "../sfdc/sfdc-reports.dto";
import { Scopes } from "../../auth/decorators/scopes.decorator";
import { Scopes as AppScopes } from "../../app-constants";
import { ChallengesReportsService } from "./challenges-reports.service";
import { CsvResponseInterceptor } from "../../common/interceptors/csv-response.interceptor";
import { ChallengeRegistrantsQueryDto } from "./dtos/registrants.dto";
import { ChallengesReportQueryDto } from "./dtos/challenge.dto";
import {
Expand All @@ -19,6 +27,8 @@ import {
} from "./dtos/submission-links.dto";

@ApiTags("Challenges Reports")
@ApiProduces("application/json", "text/csv")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The @ApiProduces decorator specifies both application/json and text/csv. Ensure that the CsvResponseInterceptor correctly handles the content negotiation between these types, as it might affect the response format expected by clients.

@UseInterceptors(CsvResponseInterceptor)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
The CsvResponseInterceptor is applied globally to the controller. Verify that all endpoints within this controller are intended to support CSV responses, as this might introduce unexpected behavior for endpoints not designed to return CSV.

@Controller("/challenges")
export class ChallengesReportsController {
constructor(private readonly reportsService: ChallengesReportsService) {}
Expand Down
9 changes: 8 additions & 1 deletion src/reports/challenges/challenges-reports.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Module } from "@nestjs/common";
import { CsvSerializer } from "src/common/csv/csv-serializer";
import { SqlLoaderService } from "src/common/sql-loader.service";
import { CsvResponseInterceptor } from "src/common/interceptors/csv-response.interceptor";
import { ChallengesReportsController } from "./challenges-reports.controller";
import { ChallengesReportsService } from "./challenges-reports.service";

@Module({
controllers: [ChallengesReportsController],
providers: [ChallengesReportsService, SqlLoaderService],
providers: [
ChallengesReportsService,
SqlLoaderService,
CsvSerializer,
CsvResponseInterceptor,
],
})
export class ChallengesReportsModule {}
Loading