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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
.circleci
.env
.env.*
node_modules
dist
coverage
*.log
18 changes: 10 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
# ---- Base Stage ----
FROM node:20-alpine AS base
FROM node:22-alpine AS base
WORKDIR /usr/src/app

# ---- Package Manager Stage ----
FROM base AS package-manager
# Pin pnpm so clean builds do not change behaviour when a new major is released.
RUN npm install --global pnpm@11.15.0

# ---- Dependencies Stage ----
FROM base AS deps
# Install pnpm
RUN npm install -g pnpm
FROM package-manager AS deps
# Copy dependency-defining files
COPY package.json pnpm-lock.yaml ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile --prod

# ---- Build Stage ----
FROM base AS build
RUN npm install -g pnpm
FROM package-manager AS build
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY . .
# Build the application
RUN pnpm build

# ---- Production Stage ----
FROM base AS production
ENV NODE_ENV production
ENV NODE_ENV=production
# Copy built application from the build stage
COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /usr/src/app/sql ./sql
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,43 @@ Reports return JSON data by default. Endpoints that support CSV can also return
CSV when the request sets `Accept: text/csv` (including the Challenges,
Topcoder, Member, and Admin report groups).

## Reports Portal Dashboards

The Reports Portal dashboard API is available under
`/v6/reports/dashboard`:

- `GET /v6/reports/dashboard` returns all dashboards, keyed as
`newSignups`, `membersPaid`, and `challengeParticipation`.
- `GET /v6/reports/dashboard/:dashboard` returns one dashboard. Supported
slugs are `new-signups`, `members-paid`, and `challenge-participation`.
- `GET /v6/reports/dashboard/export` downloads all monthly dashboard rows as
a flat CSV.
- `GET /v6/reports/dashboard/:dashboard/export` downloads one dashboard as a
flat CSV.

All endpoints accept optional ISO-8601 `startDate` (inclusive) and `endDate`
(exclusive) query parameters. With neither bound, the monthly series covers
the latest six UTC calendar months, including the current month. With one
bound, the other is derived six calendar months away. The response always
includes the resolved timestamps, zero-filled calendar months, and an
all-time summary.

Dashboard figures use these shared definitions:

- Signups come from `identity.user.create_date`. `status = 'A'` is activated;
every other current status is not activated.
- Paid-member activity requires a `PAID` finance payment for a `PAYMENT`
winning. Its event timestamp is `date_paid`, falling back to `created_at`.
Members are deduplicated within each payment bucket and month.
- Registrations are Submitter resource creation events. Submissions are
non-deleted review submission events, using `submittedDate` and falling
back to `createdAt`. Each category is deduplicated independently by member
and month.
- Rates are percentages from 0 through 100.

Human access is limited to Administrator and Talent Manager roles. Machine
tokens require the `reports:all` scope.

## Security

Currently, an M2M token is required to pull any report, and each report has its own scope associated with it that must be applied to the M2M token client ID
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "reports-api-v6",
"version": "0.0.1",
"private": true,
"packageManager": "pnpm@11.15.0",
"scripts": {
"build": "nest build",
"start": "node dist/main.js",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
packages:
- .

allowBuilds:
'@nestjs/core@11.1.9': false
'@prisma/engines@7.0.1': true
'@scarf/scarf@1.4.0': false
dtrace-provider@0.8.8: false
prisma@7.0.1: true
63 changes: 51 additions & 12 deletions sql/reports/challenges/submitters.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
WITH challenge_context AS (
SELECT
c.id,
(ct.name = 'Marathon Match') AS is_marathon_match
(ct.name = 'Marathon Match') AS is_marathon_match,
(c.status = 'COMPLETED') AS is_completed
FROM challenges."Challenge" AS c
JOIN challenges."ChallengeType" AS ct
ON ct.id = c."typeId"
Expand All @@ -17,17 +18,46 @@ submission_metrics AS (
s."finalScore"::double precision,
s."initialScore"::double precision
) AS standard_score,
provisional_review.provisional_score,
COALESCE(
final_review."aggregateScore",
s."finalScore"::double precision
) AS final_score_raw
CASE
WHEN s.status IN (
'FAILED_SCREENING',
'FAILED_REVIEW',
'FAILED_CHECKPOINT_SCREENING',
'FAILED_CHECKPOINT_REVIEW',
'DELETED'
) THEN NULL
WHEN provisional_review.has_provisional_review THEN CASE
WHEN provisional_review.provisional_score >= 0 THEN provisional_review.provisional_score
ELSE NULL
END
WHEN s."initialScore"::double precision >= 0 THEN s."initialScore"::double precision
ELSE NULL
END AS provisional_score,
CASE
WHEN NOT cc.is_completed THEN NULL
WHEN s.status IN (
'FAILED_SCREENING',
'FAILED_REVIEW',
'FAILED_CHECKPOINT_SCREENING',
'FAILED_CHECKPOINT_REVIEW',
'DELETED'
) THEN NULL
WHEN final_review.has_final_review THEN CASE
WHEN final_review."aggregateScore" >= 0 THEN final_review."aggregateScore"
ELSE NULL
END
WHEN s."finalScore"::double precision >= 0 THEN s."finalScore"::double precision
ELSE NULL
END AS final_score_raw,
cc.is_completed
FROM challenge_context AS cc
JOIN reviews."submission" AS s
ON s."challengeId" = cc.id
AND s."memberId" IS NOT NULL
LEFT JOIN LATERAL (
SELECT rs."aggregateScore"
SELECT
TRUE AS has_final_review,
rs."aggregateScore"
FROM reviews."reviewSummation" AS rs
WHERE rs."submissionId" = s.id
AND COALESCE(rs."isFinal", TRUE) = TRUE
Expand All @@ -36,7 +66,9 @@ submission_metrics AS (
LIMIT 1
) AS final_review ON TRUE
LEFT JOIN LATERAL (
SELECT rs."aggregateScore" AS provisional_score
SELECT
TRUE AS has_provisional_review,
rs."aggregateScore" AS provisional_score
FROM reviews."reviewSummation" AS rs
WHERE rs."submissionId" = s.id
AND rs."isProvisional" IS TRUE
Expand Down Expand Up @@ -73,9 +105,12 @@ mm_latest_submission_scores AS (
sm."memberId",
sm.provisional_score AS provisional_score_raw,
sm.final_score_raw,
COALESCE(sm.final_score_raw, sm.provisional_score) AS effective_score_raw,
sm.is_completed,
sm.submission_timestamp
FROM submission_metrics AS sm
WHERE
sm.provisional_score IS NOT NULL
OR sm.final_score_raw IS NOT NULL
ORDER BY
sm."memberId",
sm.submission_timestamp DESC NULLS LAST,
Expand All @@ -93,13 +128,13 @@ mm_ranked_scores AS (
ELSE ROUND(mlss.final_score_raw::numeric, 2)
END AS "finalScore",
CASE
WHEN mlss.effective_score_raw IS NULL THEN NULL
ELSE ROW_NUMBER() OVER (
WHEN mlss.is_completed AND mlss.final_score_raw IS NOT NULL THEN ROW_NUMBER() OVER (
ORDER BY
mlss.effective_score_raw DESC NULLS LAST,
mlss.final_score_raw DESC NULLS LAST,
mlss.submission_timestamp ASC NULLS LAST,
mlss."memberId" ASC
)
ELSE NULL
END AS "finalRank"
FROM mm_latest_submission_scores AS mlss
)
Expand Down Expand Up @@ -177,6 +212,10 @@ ORDER BY
WHEN sm.is_marathon_match THEN mrs."finalRank"
ELSE NULL
END ASC NULLS LAST,
CASE
WHEN sm.is_marathon_match AND mrs."finalRank" IS NULL THEN mrs."provisionalScore"
ELSE NULL
END DESC NULLS LAST,
CASE
WHEN sm.is_marathon_match THEN NULL
ELSE sms."submissionScore"
Expand Down
63 changes: 51 additions & 12 deletions sql/reports/challenges/valid-submitters.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
WITH challenge_context AS (
SELECT
c.id,
(ct.name = 'Marathon Match') AS is_marathon_match
(ct.name = 'Marathon Match') AS is_marathon_match,
(c.status = 'COMPLETED') AS is_completed
FROM challenges."Challenge" AS c
JOIN challenges."ChallengeType" AS ct
ON ct.id = c."typeId"
Expand All @@ -17,11 +18,38 @@ submission_metrics AS (
s."finalScore"::double precision,
s."initialScore"::double precision
) AS standard_score,
provisional_review.provisional_score,
COALESCE(
final_review."aggregateScore",
s."finalScore"::double precision
) AS final_score_raw,
CASE
WHEN s.status IN (
'FAILED_SCREENING',
'FAILED_REVIEW',
'FAILED_CHECKPOINT_SCREENING',
'FAILED_CHECKPOINT_REVIEW',
'DELETED'
) THEN NULL
WHEN provisional_review.has_provisional_review THEN CASE
WHEN provisional_review.provisional_score >= 0 THEN provisional_review.provisional_score
ELSE NULL
END
WHEN s."initialScore"::double precision >= 0 THEN s."initialScore"::double precision
ELSE NULL
END AS provisional_score,
CASE
WHEN NOT cc.is_completed THEN NULL
WHEN s.status IN (
'FAILED_SCREENING',
'FAILED_REVIEW',
'FAILED_CHECKPOINT_SCREENING',
'FAILED_CHECKPOINT_REVIEW',
'DELETED'
) THEN NULL
WHEN final_review.has_final_review THEN CASE
WHEN final_review."aggregateScore" >= 0 THEN final_review."aggregateScore"
ELSE NULL
END
WHEN s."finalScore"::double precision >= 0 THEN s."finalScore"::double precision
ELSE NULL
END AS final_score_raw,
cc.is_completed,
(
passing_review.is_passing IS TRUE
OR COALESCE(s."finalScore"::double precision, 0) > 98
Expand All @@ -31,7 +59,9 @@ submission_metrics AS (
ON s."challengeId" = cc.id
AND s."memberId" IS NOT NULL
LEFT JOIN LATERAL (
SELECT rs."aggregateScore"
SELECT
TRUE AS has_final_review,
rs."aggregateScore"
FROM reviews."reviewSummation" AS rs
WHERE rs."submissionId" = s.id
AND COALESCE(rs."isFinal", TRUE) = TRUE
Expand All @@ -40,7 +70,9 @@ submission_metrics AS (
LIMIT 1
) AS final_review ON TRUE
LEFT JOIN LATERAL (
SELECT rs."aggregateScore" AS provisional_score
SELECT
TRUE AS has_provisional_review,
rs."aggregateScore" AS provisional_score
FROM reviews."reviewSummation" AS rs
WHERE rs."submissionId" = s.id
AND rs."isProvisional" IS TRUE
Expand Down Expand Up @@ -90,9 +122,12 @@ mm_latest_submission_scores AS (
vsm."memberId",
vsm.provisional_score AS provisional_score_raw,
vsm.final_score_raw,
COALESCE(vsm.final_score_raw, vsm.provisional_score) AS effective_score_raw,
vsm.is_completed,
vsm.submission_timestamp
FROM valid_submission_metrics AS vsm
WHERE
vsm.provisional_score IS NOT NULL
OR vsm.final_score_raw IS NOT NULL
ORDER BY
vsm."memberId",
vsm.submission_timestamp DESC NULLS LAST,
Expand All @@ -110,13 +145,13 @@ mm_ranked_scores AS (
ELSE ROUND(mlss.final_score_raw::numeric, 2)
END AS "finalScore",
CASE
WHEN mlss.effective_score_raw IS NULL THEN NULL
ELSE ROW_NUMBER() OVER (
WHEN mlss.is_completed AND mlss.final_score_raw IS NOT NULL THEN ROW_NUMBER() OVER (
ORDER BY
mlss.effective_score_raw DESC NULLS LAST,
mlss.final_score_raw DESC NULLS LAST,
mlss.submission_timestamp ASC NULLS LAST,
mlss."memberId" ASC
)
ELSE NULL
END AS "finalRank"
FROM mm_latest_submission_scores AS mlss
)
Expand Down Expand Up @@ -194,6 +229,10 @@ ORDER BY
WHEN vsm.is_marathon_match THEN mrs."finalRank"
ELSE NULL
END ASC NULLS LAST,
CASE
WHEN vsm.is_marathon_match AND mrs."finalRank" IS NULL THEN mrs."provisionalScore"
ELSE NULL
END DESC NULLS LAST,
CASE
WHEN vsm.is_marathon_match THEN NULL
ELSE sms."submissionScore"
Expand Down
Loading
Loading