diff --git a/README.md b/README.md index 003fba8..d59cf23 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/scripts/export-member-tax.js b/scripts/export-member-tax.js index 7b715cf..ad4b8ff 100644 --- a/scripts/export-member-tax.js +++ b/scripts/export-member-tax.js @@ -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", @@ -336,6 +335,11 @@ function writeCsv(outputPath, rows) { fs.writeFileSync(outputPath, `${lines.join("\n")}\n`, "utf8"); } +function toNumberOrZero(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + async function run() { const options = parseArgs(process.argv.slice(2)); if (options.help) { @@ -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()) + .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()]); diff --git a/src/reports/challenges/challenges-reports.controller.ts b/src/reports/challenges/challenges-reports.controller.ts index f6f78fc..4f2cad7 100644 --- a/src/reports/challenges/challenges-reports.controller.ts +++ b/src/reports/challenges/challenges-reports.controller.ts @@ -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"; @@ -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 { @@ -19,6 +27,8 @@ import { } from "./dtos/submission-links.dto"; @ApiTags("Challenges Reports") +@ApiProduces("application/json", "text/csv") +@UseInterceptors(CsvResponseInterceptor) @Controller("/challenges") export class ChallengesReportsController { constructor(private readonly reportsService: ChallengesReportsService) {} diff --git a/src/reports/challenges/challenges-reports.module.ts b/src/reports/challenges/challenges-reports.module.ts index 396c13f..e43662b 100644 --- a/src/reports/challenges/challenges-reports.module.ts +++ b/src/reports/challenges/challenges-reports.module.ts @@ -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 {}