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: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ Dashboard figures use these shared definitions:
`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 payment version and sum `gross_amount`,
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`.
- 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
Expand Down
26 changes: 14 additions & 12 deletions sql/reports/dashboard/member-payment-by-customer.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
-- Monthly paid-member values split by the selected range's top five clients.
-- 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. Payments for
-- unranked or unnamed clients are grouped under Other Customers.
-- 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 (
Expand All @@ -28,9 +30,9 @@ latest_payment_versions AS MATERIALIZED (
FROM finance.payment p
GROUP BY p.winnings_id
),
paid_events AS MATERIALIZED (
payment_events AS MATERIALIZED (
SELECT
COALESCE(p.date_paid, p.created_at) AS paid_at,
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
Expand All @@ -56,18 +58,18 @@ paid_events AS MATERIALIZED (
)
LEFT JOIN "billing-accounts"."Client" cl
ON cl.id = COALESCE(payment_ba."clientId", challenge_ba."clientId")
WHERE p.payment_status = 'PAID'
WHERE p.payment_status IS DISTINCT FROM 'CANCELLED'
AND w.type = 'PAYMENT'
AND COALESCE(p.date_paid, p.created_at) IS NOT NULL
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 paid_events pe
FROM payment_events pe
CROSS JOIN bounds b
WHERE pe.paid_at >= b.start_at
AND pe.paid_at < b.end_at
WHERE pe.activity_at >= b.start_at
AND pe.activity_at < b.end_at
),
customer_totals AS (
SELECT
Expand Down Expand Up @@ -118,7 +120,7 @@ series AS (
),
monthly_amounts AS (
SELECT
DATE_TRUNC('month', se.paid_at) AS month_start,
DATE_TRUNC('month', se.activity_at) AS month_start,
COALESCE(
'customer-' || tc.customer_id,
'other-customers'
Expand All @@ -129,7 +131,7 @@ monthly_amounts AS (
ON tc.customer_id = se.customer_id
AND tc.customer_label = se.customer_label
GROUP BY
DATE_TRUNC('month', se.paid_at),
DATE_TRUNC('month', se.activity_at),
COALESCE('customer-' || tc.customer_id, 'other-customers')
)
SELECT
Expand Down
25 changes: 13 additions & 12 deletions sql/reports/dashboard/member-payment-by-month.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
-- Monthly paid-member values split by canonical payment bucket.
-- Monthly member-payment values split by canonical payment bucket.
--
-- Parameters:
-- $1 timestamptz - inclusive reporting range start
-- $2 timestamptz - exclusive reporting range end
--
-- Only the latest version of each payment is considered. Gross amount is the
-- preferred member-payment value, with total amount used as a fallback.
-- 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,
Expand All @@ -26,9 +27,9 @@ latest_payment_versions AS MATERIALIZED (
FROM finance.payment p
GROUP BY p.winnings_id
),
paid_events AS MATERIALIZED (
payment_events AS MATERIALIZED (
SELECT
COALESCE(p.date_paid, p.created_at) AS paid_at,
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'
Expand All @@ -48,15 +49,15 @@ paid_events AS MATERIALIZED (
AND lpv.max_version = p.version
JOIN finance.winnings w
ON w.winning_id = p.winnings_id
WHERE p.payment_status = 'PAID'
WHERE p.payment_status IS DISTINCT FROM 'CANCELLED'
AND w.type = 'PAYMENT'
AND COALESCE(p.date_paid, p.created_at) IS NOT NULL
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.paid_at) AS month_start,
DATE_TRUNC('month', pe.activity_at) AS month_start,
COALESCE(SUM(pe.amount) FILTER (
WHERE pe.payment_type = 'taas'
), 0) AS taas,
Expand All @@ -69,11 +70,11 @@ selected_months AS (
COALESCE(SUM(pe.amount) FILTER (
WHERE pe.payment_type = 'engagement'
), 0) AS engagement
FROM paid_events pe
FROM payment_events pe
CROSS JOIN bounds b
WHERE pe.paid_at >= b.start_at
AND pe.paid_at < b.end_at
GROUP BY DATE_TRUNC('month', pe.paid_at)
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,
Expand Down
13 changes: 9 additions & 4 deletions src/reports/dashboard/dashboard-reports.sql.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ describe("Dashboard report SQL", () => {
expect(sql).toMatch(/COUNT\(DISTINCT pe\.member_id\) FILTER/g);
});

it("sums latest paid-member values by canonical payment bucket", () => {
it("sums projected member-payment values by canonical payment bucket", () => {
const sql = sqlLoader.load("reports/dashboard/member-payment-by-month.sql");

expect(sql).toContain("MAX(p.version) AS max_version");
expect(sql).toContain("lpv.max_version = p.version");
expect(sql).toContain("p.payment_status = 'PAID'");
expect(sql).toContain("p.payment_status IS DISTINCT FROM 'CANCELLED'");
expect(sql).toContain("w.type = 'PAYMENT'");
expect(sql).toContain("COALESCE(p.date_paid, p.created_at)");
expect(sql).toContain("p.created_at AS activity_at");
expect(sql).not.toContain("p.payment_status = 'PAID'");
expect(sql).not.toContain("p.date_paid");
expect(sql).toContain(
"COALESCE(p.gross_amount, p.total_amount, 0) AS amount",
);
Expand All @@ -80,7 +82,10 @@ describe("Dashboard report SQL", () => {
);

expect(sql).toContain("MAX(p.version) AS max_version");
expect(sql).toContain("p.payment_status = 'PAID'");
expect(sql).toContain("p.payment_status IS DISTINCT FROM 'CANCELLED'");
expect(sql).toContain("p.created_at AS activity_at");
expect(sql).not.toContain("p.payment_status = 'PAID'");
expect(sql).not.toContain("p.date_paid");
expect(sql).toContain(
"COALESCE(p.gross_amount, p.total_amount, 0) AS amount",
);
Expand Down
Loading