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
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ workflows:
- develop
- PM-4931
- improve-member-search-2
tags:
only: /^dev-.*/

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ The Reports Portal dashboard API is available under
`/v6/reports/dashboard`:

- `GET /v6/reports/dashboard` returns all dashboards, keyed as
`newSignups`, `membersPaid`, and `challengeParticipation`.
`newSignups`, `membersPaid`, `challengeParticipation`,
`memberPaymentByMonth`, and `memberPaymentByCustomer`.
- `GET /v6/reports/dashboard/:dashboard` returns one dashboard. Supported
slugs are `new-signups`, `members-paid`, and `challenge-participation`.
slugs are `new-signups`, `members-paid`, `challenge-participation`,
`member-payment-by-month`, and `member-payment-by-customer`.
- `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
Expand All @@ -24,20 +26,28 @@ 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.
includes the resolved timestamps and zero-filled calendar months. The signup,
members-paid, and challenge-participation dashboards also include 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.
- Paid-member activity uses the latest non-cancelled finance payment for a
`PAYMENT` winning. It is grouped by the payment creation month so projected
payments that are owed or on hold remain visible. Members are deduplicated
within each payment bucket and month.
- Member-payment values use the latest non-cancelled finance payment and group
`gross_amount` by payment creation month, falling back to `total_amount`.
The payment-by-customer dashboard ranks the top five billing-account clients
across the selected range and groups all unnamed or remaining clients under
`Other Customers`.
- Challenge participation uses the latest actual phase completion month for
Challenge, Marathon Match, and First2Finish cohorts. Registrants are
Submitter resources, and submitters have a non-deleted submission for the
same challenge and member. Each category is deduplicated by member and
cohort month.
- Rates are percentages from 0 through 100.

Human access is limited to Administrator and Talent Manager roles. Machine
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"packageManager": "pnpm@11.15.0",
"scripts": {
"build": "nest build",
"deploy:dev": "BRANCH=$(git rev-parse --abbrev-ref HEAD) && TAG=\"dev-${BRANCH}\" && git tag -d \"$TAG\" 2>/dev/null; git push origin \":refs/tags/$TAG\" 2>/dev/null; git tag \"$TAG\" && git push origin \"$TAG\"",
"start": "node dist/main.js",
"start:dev": "nest start --watch",
"start:prod": "node dist/main.js",
Expand Down
47 changes: 37 additions & 10 deletions sql/reports/dashboard/challenge-participation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
-- $1 timestamptz - inclusive reporting range start
-- $2 timestamptz - exclusive reporting range end
--
-- Registration and submission are independent activity events. A member is
-- counted once in each category per month, regardless of challenge count.
-- The month is the challenge cohort's latest actual phase completion month,
-- matching the Challenge Registrants report. This avoids treating legacy
-- resource import timestamps as registration dates. A member is counted once
-- in each category per month, regardless of challenge count.
WITH bounds AS (
SELECT
$1::timestamptz AT TIME ZONE 'UTC' AS start_at,
Expand All @@ -19,23 +21,48 @@ months AS (
) AS month_start
FROM bounds b
),
registration_events AS MATERIALIZED (
eligible_challenges AS MATERIALIZED (
SELECT
c.id AS challenge_id,
lp."actualEndDate" AS activity_at
FROM challenges."Challenge" c
JOIN challenges."ChallengeType" ct
ON ct.id = c."typeId"
JOIN LATERAL (
SELECT cp."actualEndDate"
FROM challenges."ChallengePhase" cp
WHERE cp."challengeId" = c.id
ORDER BY cp."scheduledEndDate" DESC
LIMIT 1
) lp
ON lp."actualEndDate" IS NOT NULL
WHERE ct.name IN ('Challenge', 'Marathon Match', 'First2Finish')
),
registration_events AS MATERIALIZED (
SELECT DISTINCT
ec.challenge_id,
NULLIF(TRIM(r."memberId"), '') AS member_id,
r."createdAt" AS activity_at
FROM resources."Resource" r
ec.activity_at
FROM eligible_challenges ec
JOIN resources."Resource" r
ON r."challengeId" = ec.challenge_id
JOIN resources."ResourceRole" rr
ON rr.id = r."roleId"
WHERE COALESCE(NULLIF(TRIM(rr."nameLower"), ''), LOWER(rr.name)) = 'submitter'
AND NULLIF(TRIM(r."memberId"), '') IS NOT NULL
),
submission_events AS MATERIALIZED (
SELECT
NULLIF(TRIM(s."memberId"), '') AS member_id,
COALESCE(s."submittedDate", s."createdAt") AS activity_at
FROM reviews.submission s
WHERE s.status <> 'DELETED'
AND NULLIF(TRIM(s."memberId"), '') IS NOT NULL
re.member_id,
re.activity_at
FROM registration_events re
WHERE EXISTS (
SELECT 1
FROM reviews.submission s
WHERE s."challengeId" = re.challenge_id
AND s."memberId" = re.member_id
AND s.status <> 'DELETED'
)
),
selected_registrations AS (
SELECT
Expand Down
149 changes: 149 additions & 0 deletions sql/reports/dashboard/member-payment-by-customer.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
-- Monthly member-payment values split by the selected range's top five clients.
--
-- Parameters:
-- $1 timestamptz - inclusive reporting range start
-- $2 timestamptz - exclusive reporting range end
--
-- The same ranked customer series is used for every month. The latest
-- non-cancelled payment record is counted in its creation month so projected
-- payments remain visible. Payments for unranked or unnamed clients are
-- grouped under Other Customers.
-- Billing-account ids are normalized and compared as text so the historical
-- zero sentinel falls back to challenge billing without unsafe integer casts.
WITH bounds AS (
SELECT
$1::timestamptz AT TIME ZONE 'UTC' AS start_at,
$2::timestamptz AT TIME ZONE 'UTC' AS end_at
),
months AS (
SELECT GENERATE_SERIES(
DATE_TRUNC('month', b.start_at),
DATE_TRUNC('month', b.end_at - INTERVAL '1 microsecond'),
INTERVAL '1 month'
) AS month_start
FROM bounds b
),
latest_payment_versions AS MATERIALIZED (
SELECT
p.winnings_id,
MAX(p.version) AS max_version
FROM finance.payment p
GROUP BY p.winnings_id
),
payment_events AS MATERIALIZED (
SELECT
p.created_at AS activity_at,
COALESCE(p.gross_amount, p.total_amount, 0) AS amount,
NULLIF(TRIM(cl.id), '') AS customer_id,
NULLIF(TRIM(cl.name), '') AS customer_label
FROM finance.payment p
JOIN latest_payment_versions lpv
ON lpv.winnings_id = p.winnings_id
AND lpv.max_version = p.version
JOIN finance.winnings w
ON w.winning_id = p.winnings_id
LEFT JOIN challenges."Challenge" c
ON c.id = w.external_id
LEFT JOIN challenges."ChallengeBilling" cb
ON cb."challengeId" = c.id
LEFT JOIN "billing-accounts"."BillingAccount" payment_ba
ON payment_ba.id::text = NULLIF(
TRIM(LEADING '0' FROM TRIM(p.billing_account)),
''
)
LEFT JOIN "billing-accounts"."BillingAccount" challenge_ba
ON challenge_ba.id::text = NULLIF(
TRIM(LEADING '0' FROM TRIM(cb."billingAccountId")),
''
)
LEFT JOIN "billing-accounts"."Client" cl
ON cl.id = COALESCE(payment_ba."clientId", challenge_ba."clientId")
WHERE p.payment_status IS DISTINCT FROM 'CANCELLED'
AND w.type = 'PAYMENT'
AND p.created_at IS NOT NULL
AND NULLIF(TRIM(w.winner_id), '') IS NOT NULL
AND w.category::text IS DISTINCT FROM 'TOPGEAR_PAYMENT'
),
selected_events AS (
SELECT pe.*
FROM payment_events pe
CROSS JOIN bounds b
WHERE pe.activity_at >= b.start_at
AND pe.activity_at < b.end_at
),
customer_totals AS (
SELECT
se.customer_id,
se.customer_label,
SUM(se.amount) AS total_amount
FROM selected_events se
WHERE se.customer_id IS NOT NULL
AND se.customer_label IS NOT NULL
GROUP BY se.customer_id, se.customer_label
),
ranked_customers AS (
SELECT
ct.customer_id,
ct.customer_label,
ROW_NUMBER() OVER (
ORDER BY
ct.total_amount DESC,
LOWER(ct.customer_label),
ct.customer_label,
ct.customer_id
) AS series_order
FROM customer_totals ct
),
top_customers AS (
SELECT
rc.customer_id,
rc.customer_label,
rc.series_order
FROM ranked_customers rc
WHERE rc.series_order <= 5
),
series AS (
SELECT
'customer-' || tc.customer_id AS series_key,
tc.customer_id,
tc.customer_label,
tc.series_order
FROM top_customers tc

UNION ALL

SELECT
'other-customers' AS series_key,
NULL::text AS customer_id,
'Other Customers' AS customer_label,
6 AS series_order
),
monthly_amounts AS (
SELECT
DATE_TRUNC('month', se.activity_at) AS month_start,
COALESCE(
'customer-' || tc.customer_id,
'other-customers'
) AS series_key,
SUM(se.amount) AS amount
FROM selected_events se
LEFT JOIN top_customers tc
ON tc.customer_id = se.customer_id
AND tc.customer_label = se.customer_label
GROUP BY
DATE_TRUNC('month', se.activity_at),
COALESCE('customer-' || tc.customer_id, 'other-customers')
)
SELECT
TO_CHAR(m.month_start, 'YYYY-MM-01') AS month,
s.series_key,
s.customer_id,
s.customer_label,
s.series_order,
COALESCE(ma.amount, 0) AS amount
FROM months m
CROSS JOIN series s
LEFT JOIN monthly_amounts ma
ON ma.month_start = m.month_start
AND ma.series_key = s.series_key
ORDER BY m.month_start, s.series_order;
88 changes: 88 additions & 0 deletions sql/reports/dashboard/member-payment-by-month.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
-- Monthly member-payment values split by canonical payment bucket.
--
-- Parameters:
-- $1 timestamptz - inclusive reporting range start
-- $2 timestamptz - exclusive reporting range end
--
-- The latest non-cancelled payment record is counted in its creation month so
-- projected payments remain visible. Gross amount is the preferred
-- member-payment value, with total amount used as a fallback.
WITH bounds AS (
SELECT
$1::timestamptz AT TIME ZONE 'UTC' AS start_at,
$2::timestamptz AT TIME ZONE 'UTC' AS end_at
),
months AS (
SELECT GENERATE_SERIES(
DATE_TRUNC('month', b.start_at),
DATE_TRUNC('month', b.end_at - INTERVAL '1 microsecond'),
INTERVAL '1 month'
) AS month_start
FROM bounds b
),
latest_payment_versions AS MATERIALIZED (
SELECT
p.winnings_id,
MAX(p.version) AS max_version
FROM finance.payment p
GROUP BY p.winnings_id
),
payment_events AS MATERIALIZED (
SELECT
p.created_at AS activity_at,
COALESCE(p.gross_amount, p.total_amount, 0) AS amount,
CASE
WHEN w.category::text = 'TAAS_PAYMENT' THEN 'taas'
WHEN w.category::text = 'ENGAGEMENT_PAYMENT' THEN 'engagement'
WHEN w.category::text IN (
'TASK_PAYMENT',
'TASK_REVIEW_PAYMENT',
'TASK_COPILOT_PAYMENT',
'DEPLOYMENT_TASK_PAYMENT',
'PROJECT_DEPLOYMENT_TASK_PAYMENT'
) THEN 'task'
ELSE 'challenge'
END AS payment_type
FROM finance.payment p
JOIN latest_payment_versions lpv
ON lpv.winnings_id = p.winnings_id
AND lpv.max_version = p.version
JOIN finance.winnings w
ON w.winning_id = p.winnings_id
WHERE p.payment_status IS DISTINCT FROM 'CANCELLED'
AND w.type = 'PAYMENT'
AND p.created_at IS NOT NULL
AND NULLIF(TRIM(w.winner_id), '') IS NOT NULL
AND w.category::text IS DISTINCT FROM 'TOPGEAR_PAYMENT'
),
selected_months AS (
SELECT
DATE_TRUNC('month', pe.activity_at) AS month_start,
COALESCE(SUM(pe.amount) FILTER (
WHERE pe.payment_type = 'taas'
), 0) AS taas,
COALESCE(SUM(pe.amount) FILTER (
WHERE pe.payment_type = 'task'
), 0) AS task,
COALESCE(SUM(pe.amount) FILTER (
WHERE pe.payment_type = 'challenge'
), 0) AS challenge,
COALESCE(SUM(pe.amount) FILTER (
WHERE pe.payment_type = 'engagement'
), 0) AS engagement
FROM payment_events pe
CROSS JOIN bounds b
WHERE pe.activity_at >= b.start_at
AND pe.activity_at < b.end_at
GROUP BY DATE_TRUNC('month', pe.activity_at)
)
SELECT
TO_CHAR(m.month_start, 'YYYY-MM-01') AS month,
COALESCE(sm.taas, 0) AS taas,
COALESCE(sm.task, 0) AS task,
COALESCE(sm.challenge, 0) AS challenge,
COALESCE(sm.engagement, 0) AS engagement
FROM months m
LEFT JOIN selected_months sm
ON sm.month_start = m.month_start
ORDER BY m.month_start;
Loading
Loading