Skip to content

Commit d3e7ee8

Browse files
authored
Merge pull request #116 from topcoder-platform/PM-5703
PM-5703: correct dashboard payment and participation data
2 parents 7193d00 + c3b3248 commit d3e7ee8

4 files changed

Lines changed: 94 additions & 41 deletions

File tree

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,19 @@ Dashboard figures use these shared definitions:
3434

3535
- Signups come from `identity.user.create_date`. `status = 'A'` is activated;
3636
every other current status is not activated.
37-
- Paid-member activity requires a `PAID` finance payment for a `PAYMENT`
38-
winning. Its event timestamp is `date_paid`, falling back to `created_at`.
39-
Members are deduplicated within each payment bucket and month.
37+
- Paid-member activity uses the latest non-cancelled finance payment for a
38+
`PAYMENT` winning. It is grouped by the payment creation month so projected
39+
payments that are owed or on hold remain visible. Members are deduplicated
40+
within each payment bucket and month.
4041
- Member-payment values use the latest payment version and sum `gross_amount`,
4142
falling back to `total_amount`. The payment-by-customer dashboard ranks the
4243
top five billing-account clients across the selected range and groups all
4344
unnamed or remaining clients under `Other Customers`.
44-
- Registrations are Submitter resource creation events. Submissions are
45-
non-deleted review submission events, using `submittedDate` and falling
46-
back to `createdAt`. Each category is deduplicated independently by member
47-
and month.
45+
- Challenge participation uses the latest actual phase completion month for
46+
Challenge, Marathon Match, and First2Finish cohorts. Registrants are
47+
Submitter resources, and submitters have a non-deleted submission for the
48+
same challenge and member. Each category is deduplicated by member and
49+
cohort month.
4850
- Rates are percentages from 0 through 100.
4951

5052
Human access is limited to Administrator and Talent Manager roles. Machine

sql/reports/dashboard/challenge-participation.sql

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
-- $1 timestamptz - inclusive reporting range start
55
-- $2 timestamptz - exclusive reporting range end
66
--
7-
-- Registration and submission are independent activity events. A member is
8-
-- counted once in each category per month, regardless of challenge count.
7+
-- The month is the challenge cohort's latest actual phase completion month,
8+
-- matching the Challenge Registrants report. This avoids treating legacy
9+
-- resource import timestamps as registration dates. A member is counted once
10+
-- in each category per month, regardless of challenge count.
911
WITH bounds AS (
1012
SELECT
1113
$1::timestamptz AT TIME ZONE 'UTC' AS start_at,
@@ -19,23 +21,48 @@ months AS (
1921
) AS month_start
2022
FROM bounds b
2123
),
22-
registration_events AS MATERIALIZED (
24+
eligible_challenges AS MATERIALIZED (
2325
SELECT
26+
c.id AS challenge_id,
27+
lp."actualEndDate" AS activity_at
28+
FROM challenges."Challenge" c
29+
JOIN challenges."ChallengeType" ct
30+
ON ct.id = c."typeId"
31+
JOIN LATERAL (
32+
SELECT cp."actualEndDate"
33+
FROM challenges."ChallengePhase" cp
34+
WHERE cp."challengeId" = c.id
35+
ORDER BY cp."scheduledEndDate" DESC
36+
LIMIT 1
37+
) lp
38+
ON lp."actualEndDate" IS NOT NULL
39+
WHERE ct.name IN ('Challenge', 'Marathon Match', 'First2Finish')
40+
),
41+
registration_events AS MATERIALIZED (
42+
SELECT DISTINCT
43+
ec.challenge_id,
2444
NULLIF(TRIM(r."memberId"), '') AS member_id,
25-
r."createdAt" AS activity_at
26-
FROM resources."Resource" r
45+
ec.activity_at
46+
FROM eligible_challenges ec
47+
JOIN resources."Resource" r
48+
ON r."challengeId" = ec.challenge_id
2749
JOIN resources."ResourceRole" rr
2850
ON rr.id = r."roleId"
2951
WHERE COALESCE(NULLIF(TRIM(rr."nameLower"), ''), LOWER(rr.name)) = 'submitter'
3052
AND NULLIF(TRIM(r."memberId"), '') IS NOT NULL
3153
),
3254
submission_events AS MATERIALIZED (
3355
SELECT
34-
NULLIF(TRIM(s."memberId"), '') AS member_id,
35-
COALESCE(s."submittedDate", s."createdAt") AS activity_at
36-
FROM reviews.submission s
37-
WHERE s.status <> 'DELETED'
38-
AND NULLIF(TRIM(s."memberId"), '') IS NOT NULL
56+
re.member_id,
57+
re.activity_at
58+
FROM registration_events re
59+
WHERE EXISTS (
60+
SELECT 1
61+
FROM reviews.submission s
62+
WHERE s."challengeId" = re.challenge_id
63+
AND s."memberId" = re.member_id
64+
AND s.status <> 'DELETED'
65+
)
3966
),
4067
selected_registrations AS (
4168
SELECT

sql/reports/dashboard/members-paid.sql

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
-- $1 timestamptz - inclusive reporting range start
55
-- $2 timestamptz - exclusive reporting range end
66
--
7-
-- A paid event is a PAYMENT winning whose current payment status is PAID.
8-
-- date_paid is authoritative when present; created_at supports migrated paid rows
9-
-- that do not have a paid timestamp. TOPGEAR_PAYMENT is intentionally excluded
10-
-- because it is a separate canonical accrual bucket and is not one of the four
11-
-- dashboard categories.
7+
-- The report is a financial projection, so the latest non-cancelled payment
8+
-- record is counted in its creation month even when payout is still on hold or
9+
-- owed. TOPGEAR_PAYMENT is intentionally excluded because it is a separate
10+
-- canonical accrual bucket and is not one of the four dashboard categories.
1211
WITH bounds AS (
1312
SELECT
1413
$1::timestamptz AT TIME ZONE 'UTC' AS start_at,
@@ -22,10 +21,17 @@ months AS (
2221
) AS month_start
2322
FROM bounds b
2423
),
25-
paid_events AS MATERIALIZED (
24+
latest_payment_versions AS MATERIALIZED (
25+
SELECT
26+
p.winnings_id,
27+
MAX(p.version) AS max_version
28+
FROM finance.payment p
29+
GROUP BY p.winnings_id
30+
),
31+
payment_events AS MATERIALIZED (
2632
SELECT
2733
NULLIF(TRIM(w.winner_id), '') AS member_id,
28-
COALESCE(p.date_paid, p.created_at) AS paid_at,
34+
p.created_at AS activity_at,
2935
CASE
3036
WHEN w.category::text = 'TAAS_PAYMENT' THEN 'taas'
3137
WHEN w.category::text = 'ENGAGEMENT_PAYMENT' THEN 'engagement'
@@ -39,17 +45,20 @@ paid_events AS MATERIALIZED (
3945
ELSE 'challenge'
4046
END AS payment_type
4147
FROM finance.payment p
48+
JOIN latest_payment_versions lpv
49+
ON lpv.winnings_id = p.winnings_id
50+
AND lpv.max_version = p.version
4251
JOIN finance.winnings w
4352
ON w.winning_id = p.winnings_id
44-
WHERE p.payment_status = 'PAID'
53+
WHERE p.payment_status IS DISTINCT FROM 'CANCELLED'
4554
AND w.type = 'PAYMENT'
46-
AND COALESCE(p.date_paid, p.created_at) IS NOT NULL
55+
AND p.created_at IS NOT NULL
4756
AND NULLIF(TRIM(w.winner_id), '') IS NOT NULL
4857
AND w.category::text IS DISTINCT FROM 'TOPGEAR_PAYMENT'
4958
),
5059
selected_months AS (
5160
SELECT
52-
DATE_TRUNC('month', pe.paid_at) AS month_start,
61+
DATE_TRUNC('month', pe.activity_at) AS month_start,
5362
COUNT(DISTINCT pe.member_id) FILTER (
5463
WHERE pe.payment_type = 'taas'
5564
) AS taas,
@@ -62,18 +71,18 @@ selected_months AS (
6271
COUNT(DISTINCT pe.member_id) FILTER (
6372
WHERE pe.payment_type = 'engagement'
6473
) AS engagement
65-
FROM paid_events pe
74+
FROM payment_events pe
6675
CROSS JOIN bounds b
67-
WHERE pe.paid_at >= b.start_at
68-
AND pe.paid_at < b.end_at
69-
GROUP BY DATE_TRUNC('month', pe.paid_at)
76+
WHERE pe.activity_at >= b.start_at
77+
AND pe.activity_at < b.end_at
78+
GROUP BY DATE_TRUNC('month', pe.activity_at)
7079
),
7180
all_time_months AS (
7281
SELECT
73-
DATE_TRUNC('month', pe.paid_at) AS month_start,
82+
DATE_TRUNC('month', pe.activity_at) AS month_start,
7483
COUNT(DISTINCT pe.member_id) AS unique_members
75-
FROM paid_events pe
76-
GROUP BY DATE_TRUNC('month', pe.paid_at)
84+
FROM payment_events pe
85+
GROUP BY DATE_TRUNC('month', pe.activity_at)
7786
),
7887
peak_month AS (
7988
SELECT
@@ -98,7 +107,7 @@ all_time_summary AS (
98107
COUNT(DISTINCT pe.member_id) FILTER (
99108
WHERE pe.payment_type = 'engagement'
100109
) AS engagement_unique_members
101-
FROM paid_events pe
110+
FROM payment_events pe
102111
)
103112
SELECT
104113
TO_CHAR(m.month_start, 'YYYY-MM-01') AS month,

src/reports/dashboard/dashboard-reports.sql.spec.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@ describe("Dashboard report SQL", () => {
3434
expect(sql).toContain("AS peak_month_signups");
3535
});
3636

37-
it("counts paid members once per canonical payment bucket and month", () => {
37+
it("counts projected members once per canonical payment bucket and month", () => {
3838
const sql = sqlLoader.load("reports/dashboard/members-paid.sql");
3939

40-
expect(sql).toContain("p.payment_status = 'PAID'");
40+
expect(sql).toContain("MAX(p.version) AS max_version");
41+
expect(sql).toContain("lpv.max_version = p.version");
42+
expect(sql).toContain("p.payment_status IS DISTINCT FROM 'CANCELLED'");
4143
expect(sql).toContain("w.type = 'PAYMENT'");
42-
expect(sql).toContain("COALESCE(p.date_paid, p.created_at)");
44+
expect(sql).toContain("p.created_at AS activity_at");
45+
expect(sql).not.toContain("p.payment_status = 'PAID'");
46+
expect(sql).not.toContain("p.date_paid");
4347
expect(sql).toContain("w.category::text = 'TAAS_PAYMENT'");
4448
expect(sql).toContain("w.category::text = 'ENGAGEMENT_PAYMENT'");
4549
expect(sql).toContain("'TASK_REVIEW_PAYMENT'");
@@ -104,15 +108,26 @@ describe("Dashboard report SQL", () => {
104108
expect(sql).toContain("COALESCE(ma.amount, 0) AS amount");
105109
});
106110

107-
it("counts registration and submission activity independently", () => {
111+
it("counts registrants and linked submitters by challenge completion cohort", () => {
108112
const sql = sqlLoader.load("reports/dashboard/challenge-participation.sql");
109113

110-
expect(sql).toContain('FROM resources."Resource" r');
114+
expect(sql).toContain('FROM challenges."Challenge" c');
115+
expect(sql).toContain('JOIN challenges."ChallengeType" ct');
116+
expect(sql).toContain(
117+
"ct.name IN ('Challenge', 'Marathon Match', 'First2Finish')",
118+
);
119+
expect(sql).toContain('FROM challenges."ChallengePhase" cp');
120+
expect(sql).toContain('lp."actualEndDate" AS activity_at');
121+
expect(sql).toContain('ORDER BY cp."scheduledEndDate" DESC');
122+
expect(sql).toContain('JOIN resources."Resource" r');
111123
expect(sql).toContain('JOIN resources."ResourceRole" rr');
112124
expect(sql).toContain("= 'submitter'");
113125
expect(sql).toContain("FROM reviews.submission s");
126+
expect(sql).toContain('s."challengeId" = re.challenge_id');
127+
expect(sql).toContain('s."memberId" = re.member_id');
114128
expect(sql).toContain("s.status <> 'DELETED'");
115-
expect(sql).toContain('COALESCE(s."submittedDate", s."createdAt")');
129+
expect(sql).not.toContain('r."createdAt" AS activity_at');
130+
expect(sql).not.toContain('COALESCE(s."submittedDate", s."createdAt")');
116131
expect(sql).toContain("COUNT(DISTINCT re.member_id)");
117132
expect(sql).toContain("COUNT(DISTINCT se.member_id)");
118133
expect(sql).toContain("LEAST(");

0 commit comments

Comments
 (0)