Skip to content

Conversation

@lissavxo
Copy link
Collaborator

@lissavxo lissavxo commented Oct 2, 2025

Related to #818

Description

Added date range filter to payments page
the filter also applies to csv

Test plan

Go to payments page and make sure payments filter works as expected also test csv file

Summary by CodeRabbit

  • New Features
    • Added date range filtering on the Payments page with start and end date inputs.
    • Payments list, pagination, and counts now respect the selected date range.
    • CSV export includes the chosen date range to download only matching transactions.
    • Clear Filters now resets date range along with existing filters.
    • Year and date range filters are mutually exclusive; selecting one clears the other.
    • Preserves previous behavior when no date range is selected.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Walkthrough

Adds optional startDate/endDate filtering across payments listing, count, and download. API endpoints parse and forward date range with timezone. Frontend introduces date-range UI, integrates into fetch, count, and export URLs, and coordinates with year filters. Service layer adds date-range utilities and extends query methods to apply date or year filters.

Changes

Cohort / File(s) Summary
API endpoints: date-range support
pages/api/payments/index.ts, pages/api/payments/count/index.ts, pages/api/payments/download/index.ts
Parse optional startDate/endDate from req.query; pass to service calls alongside timezone; preserve existing flows; extend count, list (with pagination), and download to support date filtering.
Frontend payments page
pages/payments/index.tsx
Add startDate/endDate state and inputs; include date params in data/count/export requests; clear conflicting year filters when date range is set and vice versa; update dependencies and clear-filters logic.
Service layer: date-range querying
services/transactionService.ts
Extend functions to accept startDate?: string, endDate?: string; implement buildDateRange and getDateRangeFilter; apply date-range when provided, else year-based (getYearFilters updated to use range builder); propagate through pagination, list, and count methods.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant P as Payments Page (UI)
  participant API as /api/payments (GET)
  participant S as transactionService
  participant DB as Database

  U->>P: Set filters (years or start/end date)
  P->>API: GET with userId, paging, years, startDate, endDate, timezone
  API->>S: fetchAllPaymentsByUserIdWithPagination(..., years, tz, startDate, endDate)
  alt startDate & endDate provided
    S->>S: buildDateRange(startDate, endDate, tz)
    S->>DB: Query with date-range filter
  else years provided
    S->>S: getYearFilters(years, tz)
    S->>DB: Query with year-based filter
  end
  DB-->>S: Results
  S-->>API: Payments page data
  API-->>P: JSON response
  P-->>U: Render payments
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant P as Payments Page (UI)
  participant C as /api/payments/count
  participant D as /api/payments/download
  participant S as transactionService
  participant DB as Database

  P->>C: GET count with years/startDate/endDate/tz
  C->>S: getFilteredTransactionCount(..., years, tz, startDate, endDate)
  S->>DB: Count with appropriate filter
  DB-->>S: Count
  S-->>C: Count
  C-->>P: Count JSON

  U->>P: Export CSV
  P->>D: GET download with years/startDate/endDate/tz
  D->>S: fetchAllPaymentsByUserId(..., tz, startDate, endDate)
  S->>DB: Query with filter
  DB-->>S: Rows
  S-->>D: Data
  D-->>P: CSV stream
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble dates on data vines,
From start to end, the range aligns.
Years hop back when filters play,
Timezones guide my twilit way.
Counts and CSVs now flow—
A rabbit’s ledger, good to go! 🐇📅

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run `@coderabbitai generate docstrings` to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely communicates the core feature addition—a date range filter—directly aligning with the primary changes introduced in the payments API and UI.
Description Check ✅ Passed The PR description adheres to the repository template by including a Related to section with issue #818, a clear Description section outlining the date range filter addition and its CSV application, and a Test plan section specifying verification steps for both the UI filter and the CSV export.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/date-range-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lissavxo lissavxo requested a review from chedieck October 2, 2025 20:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
pages/payments/index.tsx (1)

140-142: Consider adding startDate to useEffect dependencies.

The useEffect only includes endDate in its dependencies, which means changing startDate alone won't trigger a data refresh. While users typically set startDate first and then endDate (which would trigger the fetch), a user who modifies only startDate after setting both dates would see stale data.

Apply this diff to include startDate in the dependencies:

   useEffect(() => {
     setRefreshCount(prev => prev + 1)
-  }, [selectedButtonIds, selectedTransactionYears, endDate])
+  }, [selectedButtonIds, selectedTransactionYears, startDate, endDate])
services/transactionService.ts (1)

752-763: Consider consistent parameter ordering across functions.

The parameter order differs between functions:

  • fetchAllPaymentsByUserIdWithPagination (lines 752-763): ..., timezone?, startDate?, endDate?
  • fetchAllPaymentsByUserId (lines 889-897): ..., startDate?, endDate?, timezone?
  • getFilteredTransactionCount (lines 949-956): ..., timezone?, startDate?, endDate?

While this doesn't cause bugs (calls pass parameters correctly), consistent ordering improves maintainability.

Consider standardizing the order (e.g., always place timezone before startDate and endDate) in a follow-up refactor:

 export async function fetchAllPaymentsByUserId (
   userId: string,
   networkIds?: number[],
   buttonIds?: string[],
   years?: string[],
+  timezone?: string,
   startDate?: string,
-  endDate?: string,
-  timezone?: string
+  endDate?: string
 ): Promise<TransactionsWithPaybuttonsAndPrices[]> {

Also applies to: 889-897, 949-956

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87b146b and e5515fb.

📒 Files selected for processing (5)
  • pages/api/payments/count/index.ts (1 hunks)
  • pages/api/payments/download/index.ts (1 hunks)
  • pages/api/payments/index.ts (2 hunks)
  • pages/payments/index.tsx (8 hunks)
  • services/transactionService.ts (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
pages/api/payments/count/index.ts (3)
pages/api/payments/download/index.ts (1)
  • req (13-80)
pages/api/payments/index.ts (1)
  • req (5-48)
services/transactionService.ts (1)
  • getFilteredTransactionCount (949-982)
pages/api/payments/index.ts (2)
pages/api/payments/count/index.ts (1)
  • req (6-43)
pages/api/payments/download/index.ts (1)
  • req (13-80)
pages/api/payments/download/index.ts (3)
pages/api/payments/count/index.ts (1)
  • req (6-43)
pages/api/payments/index.ts (1)
  • req (5-48)
services/transactionService.ts (1)
  • fetchAllPaymentsByUserId (889-935)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (3)
pages/api/payments/count/index.ts (1)

23-35: LGTM!

The startDate and endDate parameter parsing follows the established pattern for buttonIds and years. The filter condition correctly requires both dates to be present and non-empty before enabling date-range filtering, and the function call properly passes timezone along with the date range parameters.

pages/payments/index.tsx (1)

553-603: Date range and year filter coordination looks good.

The mutual exclusion between date range and year filters is correctly implemented:

  • Selecting years clears the date range (lines 558-561)
  • Setting startDate clears years and resets endDate (lines 582-584)
  • Setting endDate clears years (lines 595-597)

The min attribute on the endDate input (line 599) properly prevents selecting an end date before the start date.

pages/api/payments/index.ts (1)

22-45: LGTM!

The startDate and endDate parameters are correctly parsed and passed to fetchAllPaymentsByUserIdWithPagination with proper timezone handling.

}

const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years)
const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years, startDate, endDate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing timezone parameter in fetchAllPaymentsByUserId call.

The timezone variable is calculated on line 33 but not passed to fetchAllPaymentsByUserId. This causes date range filtering (and existing year filtering) to ignore the user's timezone preference, potentially returning incorrect data.

Apply this diff to pass the timezone parameter:

-    const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years, startDate, endDate)
+    const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years, startDate, endDate, timezone)

Note: This bug also affects the existing years filtering functionality, not just the new date range feature.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years, startDate, endDate)
const transactions = await fetchAllPaymentsByUserId(
userId,
networkIdArray,
buttonIds,
years,
startDate,
endDate,
timezone
)
🤖 Prompt for AI Agents
In pages/api/payments/download/index.ts around line 63, the call to
fetchAllPaymentsByUserId omits the timezone parameter (computed on line 33), so
date range and year filtering ignore user timezone; update the function
invocation to include the timezone argument (passing the timezone variable
computed earlier in the same scope) in the correct position according to the
function signature so that fetchAllPaymentsByUserId receives timezone and
performs timezone-aware filtering.

Comment on lines +837 to +866
const buildDateRange = (
startDate: string | Date,
endDate: string | Date,
timezone?: string
): { gte: number, lt: number } => {
let start: number
let end: number

const startObj = new Date(startDate)
const endObj = new Date(endDate)

if (timezone !== undefined && timezone !== null && timezone !== '') {
const startInTimezone = new Date(startObj.toLocaleString('en-US', { timeZone: timezone }))
const endInTimezone = new Date(endObj.toLocaleString('en-US', { timeZone: timezone }))

const startOffset = startObj.getTime() - startInTimezone.getTime()
const endOffset = endObj.getTime() - endInTimezone.getTime()

start = (startObj.getTime() + startOffset) / 1000
end = (endObj.getTime() + endOffset) / 1000
} else {
start = startObj.getTime() / 1000
end = endObj.getTime() / 1000
}

return {
gte: Math.floor(start),
lt: Math.floor(end)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Timezone handling in buildDateRange is incorrect.

The timezone adjustment logic has a fundamental flaw:

  1. new Date(startDate) interprets "2024-01-15" as midnight UTC
  2. startObj.toLocaleString('en-US', { timeZone: timezone }) converts that UTC time to a string in the target timezone (e.g., "1/14/2024, 7:00:00 PM" for NYC)
  3. new Date(startInTimezone) parses that string in the server's system timezone (which is UTC), not the target timezone
  4. The resulting offset calculation is meaningless

Example:

  • User in NYC (UTC-5) selects "2024-01-15"
  • startObj = 2024-01-15T00:00:00Z (midnight UTC)
  • toLocaleString = "1/14/2024, 7:00:00 PM" (7 PM previous day in NYC)
  • new Date("1/14/2024, 7:00:00 PM") on server (UTC) produces wrong result
  • The offset and final timestamp are incorrect

Impact:
Date range filters will return incorrect results for users in non-UTC timezones. Transactions may be included or excluded incorrectly based on timezone.

The file already uses Prisma, which likely has moment or date-fns available. Use a proper timezone library:

+import moment from 'moment-timezone'
+
 const buildDateRange = (
   startDate: string | Date,
   endDate: string | Date,
   timezone?: string
 ): { gte: number, lt: number } => {
-  let start: number
-  let end: number
-
-  const startObj = new Date(startDate)
-  const endObj = new Date(endDate)
-
-  if (timezone !== undefined && timezone !== null && timezone !== '') {
-    const startInTimezone = new Date(startObj.toLocaleString('en-US', { timeZone: timezone }))
-    const endInTimezone = new Date(endObj.toLocaleString('en-US', { timeZone: timezone }))
-
-    const startOffset = startObj.getTime() - startInTimezone.getTime()
-    const endOffset = endObj.getTime() - endInTimezone.getTime()
-
-    start = (startObj.getTime() + startOffset) / 1000
-    end = (endObj.getTime() + endOffset) / 1000
-  } else {
-    start = startObj.getTime() / 1000
-    end = endObj.getTime() / 1000
-  }
-
+  let start: number
+  let end: number
+  
+  if (timezone !== undefined && timezone !== null && timezone !== '') {
+    // Parse date in the given timezone
+    start = moment.tz(startDate, timezone).unix()
+    end = moment.tz(endDate, timezone).unix()
+  } else {
+    // No timezone: interpret as UTC
+    start = moment.utc(startDate).unix()
+    end = moment.utc(endDate).unix()
+  }
+  
   return {
     gte: Math.floor(start),
     lt: Math.floor(end)
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const buildDateRange = (
startDate: string | Date,
endDate: string | Date,
timezone?: string
): { gte: number, lt: number } => {
let start: number
let end: number
const startObj = new Date(startDate)
const endObj = new Date(endDate)
if (timezone !== undefined && timezone !== null && timezone !== '') {
const startInTimezone = new Date(startObj.toLocaleString('en-US', { timeZone: timezone }))
const endInTimezone = new Date(endObj.toLocaleString('en-US', { timeZone: timezone }))
const startOffset = startObj.getTime() - startInTimezone.getTime()
const endOffset = endObj.getTime() - endInTimezone.getTime()
start = (startObj.getTime() + startOffset) / 1000
end = (endObj.getTime() + endOffset) / 1000
} else {
start = startObj.getTime() / 1000
end = endObj.getTime() / 1000
}
return {
gte: Math.floor(start),
lt: Math.floor(end)
}
}
import moment from 'moment-timezone'
const buildDateRange = (
startDate: string | Date,
endDate: string | Date,
timezone?: string
): { gte: number, lt: number } => {
let start: number
let end: number
if (timezone !== undefined && timezone !== null && timezone !== '') {
// Parse date in the given timezone
start = moment.tz(startDate, timezone).unix()
end = moment.tz(endDate, timezone).unix()
} else {
// No timezone: interpret as UTC
start = moment.utc(startDate).unix()
end = moment.utc(endDate).unix()
}
return {
gte: Math.floor(start),
lt: Math.floor(end)
}
}
🤖 Prompt for AI Agents
In services/transactionService.ts around lines 837–866, the current timezone
adjustment uses toLocaleString and re-parses strings which yields incorrect
offsets; replace this logic with a timezone-aware library (e.g., date-fns-tz or
moment-timezone) to: 1) interpret the input date as a date in the target
timezone (get the timezone's startOfDay for startDate and endOfDay for endDate
or compute startOfNextDay for exclusive upper bound), 2) convert those zoned
datetimes to UTC instants (zonedTimeToUtc or moment.tz(...).utc()), and 3)
return gte as Math.floor(utcStart/1000) and lt as
Math.floor(utcEndExclusive/1000); ensure imports are added and that the upper
bound is exclusive (use start of next day or endOfDay+1ms converted to UTC) so
date filters behave correctly across timezones.

Copy link
Collaborator

@chedieck chedieck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect the end date to be included, if nothing is said. Right now the initial date is included and the end date is not.

One option is to add a toggle in the rightside of the selectors saying "Include end date", but if you want just to make it include it and that's it, would be fine too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants