Skip to content

Commit ef675b8

Browse files
committed
PM-5839: Correct payment report calendar dates
What was broken Date filters could omit the selected end date or, after making it inclusive, show late July 31 SFDC payments as August 1. Monthly BA fees could also place those payments in the next month. Root cause Finance payment timestamps are stored as UTC values without timezone metadata. Date bounds were interpreted as instants or through the database session timezone, while SFDC output applied the America/New_York conversion in the wrong direction. What was changed Interpret report filters as calendar dates with half-open next-day bounds. Convert SFDC New York calendar bounds to UTC for payment storage, return payment dates with the correct New York offset, and group BA fees by the corrected New York date. Any added/updated tests Updated SQL regression coverage for payment accruals, SFDC payments, both BA fee variants, monthly buckets, and report-directory metadata. PostgreSQL 16 checks cover the July 31 boundary and daylight-saving transitions.
1 parent 4c8c6c9 commit ef675b8

10 files changed

Lines changed: 157 additions & 42 deletions

sql/reports/payment/member-payment-accrual.sql

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
WITH provided_dates AS (
22
SELECT
3-
NULLIF($1, '')::timestamptz AS start_date,
4-
NULLIF($2, '')::timestamptz AS end_date
3+
NULLIF($1, '')::date AS start_date,
4+
NULLIF($2, '')::date AS end_date
55
),
66
params AS (
77
SELECT
@@ -50,9 +50,7 @@ recent_payments AS (
5050
JOIN params pr ON TRUE
5151
WHERE w.type = 'PAYMENT'
5252
AND p.created_at >= pr.start_date
53-
AND p.created_at < (
54-
DATE_TRUNC('day', pr.end_date) + INTERVAL '1 day'
55-
)
53+
AND p.created_at < (pr.end_date + INTERVAL '1 day')
5654
),
5755
categorized_payments AS (
5856
SELECT

sql/reports/sfdc/ba-fees-monthly.sql

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,18 @@ WITH filtered_payments AS (
2020
LEFT JOIN finance.winnings w
2121
ON w.winning_id = p.winnings_id
2222
WHERE
23-
($1::timestamptz IS NULL OR p.created_at >= $1::timestamptz)
24-
AND ($2::timestamptz IS NULL OR p.created_at < (DATE_TRUNC('day', $2::timestamptz) + INTERVAL '1 day'))
23+
(
24+
$1::date IS NULL
25+
OR p.created_at >= (
26+
($1::date::timestamp AT TIME ZONE 'America/New_York') AT TIME ZONE 'UTC'
27+
)
28+
)
29+
AND (
30+
$2::date IS NULL
31+
OR p.created_at < (
32+
(($2::date + 1)::timestamp AT TIME ZONE 'America/New_York') AT TIME ZONE 'UTC'
33+
)
34+
)
2535
AND ($3::text[] IS NULL OR p.billing_account = ANY($3::text[]))
2636
AND ($4::text[] IS NULL OR p.billing_account != ALL($4::text[]))
2737
),
@@ -34,17 +44,17 @@ latest_status AS (
3444
)
3545
SELECT
3646
fp.billing_account AS "billingAccountId",
37-
TO_CHAR(DATE_TRUNC('month', fp.created_at AT TIME ZONE 'America/New_York'), 'YYYY-MM') AS "month",
47+
TO_CHAR(DATE_TRUNC('month', fp.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York'), 'YYYY-MM') AS "month",
3848
COALESCE(SUM(fp.challenge_fee), 0) AS "totalFees",
3949
COALESCE(SUM(fp.total_amount), 0) AS "totalMemberPayments",
4050
COUNT(fp.payment_id) AS "paymentCount",
41-
MIN(fp.created_at AT TIME ZONE 'America/New_York')::date AS "earliestPaymentDate",
42-
MAX(fp.created_at AT TIME ZONE 'America/New_York')::date AS "latestPaymentDate",
51+
MIN(fp.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York')::date AS "earliestPaymentDate",
52+
MAX(fp.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York')::date AS "latestPaymentDate",
4353
ls.payment_status_desc AS "currentPaymentStatus"
4454
FROM filtered_payments fp
4555
LEFT JOIN latest_status ls ON ls.billing_account = fp.billing_account
4656
GROUP BY
4757
fp.billing_account,
48-
DATE_TRUNC('month', fp.created_at AT TIME ZONE 'America/New_York'),
58+
DATE_TRUNC('month', fp.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York'),
4959
ls.payment_status_desc
5060
ORDER BY fp.billing_account, "month" DESC;

sql/reports/sfdc/ba-fees.sql

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,18 @@ WITH filtered_payments AS (
1919
LEFT JOIN finance.winnings w
2020
ON w.winning_id = p.winnings_id
2121
WHERE
22-
($1::timestamptz IS NULL OR p.created_at >= $1::timestamptz)
23-
AND ($2::timestamptz IS NULL OR p.created_at < (DATE_TRUNC('day', $2::timestamptz) + INTERVAL '1 day'))
22+
(
23+
$1::date IS NULL
24+
OR p.created_at >= (
25+
($1::date::timestamp AT TIME ZONE 'America/New_York') AT TIME ZONE 'UTC'
26+
)
27+
)
28+
AND (
29+
$2::date IS NULL
30+
OR p.created_at < (
31+
(($2::date + 1)::timestamp AT TIME ZONE 'America/New_York') AT TIME ZONE 'UTC'
32+
)
33+
)
2434
AND ($3::text[] IS NULL OR p.billing_account = ANY($3::text[]))
2535
AND ($4::text[] IS NULL OR p.billing_account != ALL($4::text[]))
2636
),

sql/reports/sfdc/payments.sql

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ WITH resolved_payment_references AS (
4444
)
4545
SELECT
4646
payment_id as "paymentId",
47-
created_at AT TIME ZONE 'America/New_York' as "paymentDate",
47+
TO_CHAR(
48+
created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York',
49+
'YYYY-MM-DD"T"HH24:MI:SS.MS'
50+
) || TO_CHAR(
51+
(created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/New_York') - created_at,
52+
'HH24:MI'
53+
) as "paymentDate",
4854
billing_account as "billingAccountId",
4955
payment_status as "paymentStatus",
5056
challenge_fee as "challengeFee",
@@ -73,8 +79,16 @@ WHERE
7379
WHERE m2.handle = ANY($5::text[])
7480
))
7581
AND ($6::text IS NULL OR challenge_name ILIKE '%' || $6 || '%')
76-
AND created_at >= COALESCE($7::timestamptz, (NOW() AT TIME ZONE 'UTC') - INTERVAL '45 days')
77-
AND ($8::timestamptz IS NULL OR created_at < (DATE_TRUNC('day', $8::timestamptz) + INTERVAL '1 day'))
82+
AND created_at >= COALESCE(
83+
($7::date::timestamp AT TIME ZONE 'America/New_York') AT TIME ZONE 'UTC',
84+
(NOW() AT TIME ZONE 'UTC') - INTERVAL '45 days'
85+
)
86+
AND (
87+
$8::date IS NULL
88+
OR created_at < (
89+
(($8::date + 1)::timestamp AT TIME ZONE 'America/New_York') AT TIME ZONE 'UTC'
90+
)
91+
)
7892
AND ($9::numeric IS NULL OR total_amount >= $9::numeric)
7993
AND ($10::numeric IS NULL OR total_amount <= $10::numeric)
8094
AND ($11::text[] IS NULL OR reported_challenge_status::text = ANY($11::text[]))

src/reports/payment/dto/member-payment-accrual.dto.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import { IsDateString, IsOptional } from "class-validator";
44
export class MemberPaymentAccrualQueryDto {
55
@ApiPropertyOptional({
66
description:
7-
"Start date (inclusive) for filtering payment creation date in ISO 8601 format",
8-
example: "2024-01-01T00:00:00.000Z",
7+
"Start date (inclusive) for filtering payment creation date in YYYY-MM-DD format. For accepted ISO timestamps, only the written calendar-date portion is used",
8+
example: "2024-01-01",
99
})
1010
@IsOptional()
1111
@IsDateString()
1212
startDate?: string;
1313

1414
@ApiPropertyOptional({
1515
description:
16-
"End date (inclusive through the full calendar day) for filtering payment creation date in ISO 8601 format",
16+
"End date (inclusive through the full calendar day) for filtering payment creation date in YYYY-MM-DD format. For accepted ISO timestamps, only the written calendar-date portion is used",
1717
example: "2024-01-31",
1818
})
1919
@IsOptional()

src/reports/payment/payment-reports.service.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,11 @@ describe("PaymentReportsService", () => {
7878
"reports/payment/member-payment-accrual.sql",
7979
);
8080

81+
expect(paymentSql).toContain("NULLIF($1, '')::date AS start_date");
82+
expect(paymentSql).toContain("NULLIF($2, '')::date AS end_date");
8183
expect(paymentSql).toContain("p.created_at >= pr.start_date");
82-
expect(paymentSql).toContain(
83-
"DATE_TRUNC('day', pr.end_date) + INTERVAL '1 day'",
84-
);
84+
expect(paymentSql).toContain("pr.end_date + INTERVAL '1 day'");
8585
expect(paymentSql).not.toContain("p.created_at <= pr.end_date");
86+
expect(paymentSql).not.toContain("DATE_TRUNC('day', pr.end_date)");
8687
});
8788
});

src/reports/report-directory.data.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ describe("getAccessibleReportsDirectory", () => {
1919
"/payment/member-payment-accrual-task",
2020
"/payment/member-payment-accrual-challenge",
2121
]);
22+
expect(
23+
directory.sfdc?.reports
24+
.find((report) => report.path === "/sfdc/payments")
25+
?.parameters?.find((parameter) => parameter.name === "endDate")
26+
?.description,
27+
).toContain("America/New_York");
28+
expect(
29+
directory.payment?.reports[0].parameters?.find(
30+
(parameter) => parameter.name === "endDate",
31+
)?.description,
32+
).toContain("Inclusive full calendar end date");
2233
expect(directory.statistics?.reports.map((report) => report.path)).toEqual(
2334
expect.arrayContaining([
2435
"/statistics/general/country-member-details",

src/reports/report-directory.data.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,28 @@ const paymentsEndDateParam: ReportParameter = {
231231
location: "query",
232232
};
233233

234+
const paymentAccrualStartDateParam: ReportParameter = {
235+
...paymentsStartDateParam,
236+
description: "Inclusive calendar start date in YYYY-MM-DD format",
237+
};
238+
239+
const paymentAccrualEndDateParam: ReportParameter = {
240+
...paymentsEndDateParam,
241+
description: "Inclusive full calendar end date in YYYY-MM-DD format",
242+
};
243+
244+
const sfdcPaymentsStartDateParam: ReportParameter = {
245+
...paymentsStartDateParam,
246+
description:
247+
"Inclusive America/New_York calendar start date in YYYY-MM-DD format",
248+
};
249+
250+
const sfdcPaymentsEndDateParam: ReportParameter = {
251+
...paymentsEndDateParam,
252+
description:
253+
"Inclusive full America/New_York calendar end date in YYYY-MM-DD format",
254+
};
255+
234256
const challengeNameParam: ReportParameter = {
235257
name: "challengeName",
236258
type: "string",
@@ -294,8 +316,8 @@ const paymentsFilters = [
294316
challengeNameParam,
295317
challengeIdsParam,
296318
engagementIdsParam,
297-
paymentsStartDateParam,
298-
paymentsEndDateParam,
319+
sfdcPaymentsStartDateParam,
320+
sfdcPaymentsEndDateParam,
299321
handlesParam,
300322
minPaymentParam,
301323
maxPaymentParam,
@@ -306,14 +328,16 @@ const baFeesDateParams: ReportParameter[] = [
306328
{
307329
name: "startDate",
308330
type: "date",
309-
description: "Start date for the report query in ISO format",
331+
description:
332+
"Inclusive America/New_York calendar start date in YYYY-MM-DD format",
310333
location: "query",
311334
required: true,
312335
},
313336
{
314337
name: "endDate",
315338
type: "date",
316-
description: "End date for the report query in ISO format",
339+
description:
340+
"Inclusive full America/New_York calendar end date in YYYY-MM-DD format",
317341
location: "query",
318342
},
319343
];
@@ -837,37 +861,37 @@ const REGISTERED_REPORTS_DIRECTORY: RegisteredReportsDirectory = {
837861
"Member Payment Accrual",
838862
"/payment/member-payment-accrual",
839863
"Member payment accruals for the provided date range (defaults to last 3 months)",
840-
[paymentsStartDateParam, paymentsEndDateParam],
864+
[paymentAccrualStartDateParam, paymentAccrualEndDateParam],
841865
),
842866
adminOnlyTopcoderReport(
843867
"Member Payment Accrual-TaaS",
844868
"/payment/member-payment-accrual-taas",
845869
"Member payment accruals for TaaS payments for the provided date range (defaults to last 3 months)",
846-
[paymentsStartDateParam, paymentsEndDateParam],
870+
[paymentAccrualStartDateParam, paymentAccrualEndDateParam],
847871
),
848872
adminOnlyTopcoderReport(
849873
"Member Payment Accrual-Topgear",
850874
"/payment/member-payment-accrual-topgear",
851875
"Member payment accruals for Topgear payments for the provided date range (defaults to last 3 months)",
852-
[paymentsStartDateParam, paymentsEndDateParam],
876+
[paymentAccrualStartDateParam, paymentAccrualEndDateParam],
853877
),
854878
adminOnlyTopcoderReport(
855879
"Member Payment Accrual-Engagement",
856880
"/payment/member-payment-accrual-engagement",
857881
"Member payment accruals for engagement payments for the provided date range (defaults to last 3 months)",
858-
[paymentsStartDateParam, paymentsEndDateParam],
882+
[paymentAccrualStartDateParam, paymentAccrualEndDateParam],
859883
),
860884
adminOnlyTopcoderReport(
861885
"Member Payment Accrual-Task",
862886
"/payment/member-payment-accrual-task",
863887
"Member payment accruals for task payments for the provided date range (defaults to last 3 months)",
864-
[paymentsStartDateParam, paymentsEndDateParam],
888+
[paymentAccrualStartDateParam, paymentAccrualEndDateParam],
865889
),
866890
adminOnlyTopcoderReport(
867891
"Member Payment Accrual-Challenge",
868892
"/payment/member-payment-accrual-challenge",
869893
"Member payment accruals for challenge payments (contest, review board, copilot, checkpoint, and related challenge payouts) for the provided date range (defaults to last 3 months)",
870-
[paymentsStartDateParam, paymentsEndDateParam],
894+
[paymentAccrualStartDateParam, paymentAccrualEndDateParam],
871895
),
872896
],
873897
},

src/reports/sfdc/sfdc-reports.dto.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,9 @@ export class PaymentsReportQueryDto {
241241

242242
@ApiProperty({
243243
required: false,
244-
description: "Start date for the report query in ISO 8601 format",
245-
example: "2023-01-01T00:00:00.000Z",
244+
description:
245+
"Start date (inclusive from the start of the America/New_York calendar day) for the report query in YYYY-MM-DD format. For accepted ISO timestamps, only the written calendar-date portion is used",
246+
example: "2023-01-01",
246247
})
247248
@IsOptional()
248249
@IsDateString()
@@ -251,7 +252,7 @@ export class PaymentsReportQueryDto {
251252
@ApiProperty({
252253
required: false,
253254
description:
254-
"End date (inclusive through the full calendar day) for the report query in ISO 8601 format",
255+
"End date (inclusive through the full America/New_York calendar day) for the report query in YYYY-MM-DD format. For accepted ISO timestamps, only the written calendar-date portion is used",
255256
example: "2023-01-31",
256257
})
257258
@IsOptional()
@@ -332,6 +333,11 @@ export class PaymentsReportResponse {
332333
description: "Winnings category from finance.winnings.category",
333334
})
334335
category: string;
336+
@ApiProperty({
337+
description:
338+
"Payment creation timestamp in America/New_York with its UTC offset",
339+
example: "2026-07-31T18:53:33.383-04:00",
340+
})
335341
paymentDate: string;
336342
paymentId: string;
337343
paymentStatus: string;
@@ -854,8 +860,8 @@ export class BaFeesReportQueryDto {
854860
@ApiProperty({
855861
required: false,
856862
description:
857-
"Start date for the report query in ISO 8601 format (inclusive). If omitted the report uses an open-ended lower bound.",
858-
example: "2023-01-01T00:00:00.000Z",
863+
"Start date (inclusive from the start of the America/New_York calendar day) for the report query in YYYY-MM-DD format. For accepted ISO timestamps, only the written calendar-date portion is used. If omitted the report uses an open-ended lower bound.",
864+
example: "2023-01-01",
859865
})
860866
@IsOptional()
861867
@IsDateString()
@@ -864,7 +870,7 @@ export class BaFeesReportQueryDto {
864870
@ApiProperty({
865871
required: false,
866872
description:
867-
"End date (inclusive through the full calendar day) for the report query in ISO 8601 format",
873+
"End date (inclusive through the full America/New_York calendar day) for the report query in YYYY-MM-DD format. For accepted ISO timestamps, only the written calendar-date portion is used",
868874
example: "2023-01-31",
869875
})
870876
@IsOptional()

0 commit comments

Comments
 (0)