Skip to content
Open
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
13 changes: 11 additions & 2 deletions pages/api/payments/count/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,18 @@ export default async (req: any, res: any): Promise<void> => {
if (typeof req.query.years === 'string' && req.query.years !== '') {
years = (req.query.years as string).split(',')
}
let startDate: string | undefined
if (typeof req.query.startDate === 'string' && req.query.startDate !== '') {
startDate = req.query.startDate as string
}
let endDate: string | undefined
if (typeof req.query.endDate === 'string' && req.query.endDate !== '') {
endDate = req.query.endDate as string
}
if (((buttonIds !== undefined) && buttonIds.length > 0) ||
((years !== undefined) && years.length > 0)) {
const totalCount = await getFilteredTransactionCount(userId, buttonIds, years)
((years !== undefined) && years.length > 0) ||
(startDate !== undefined && endDate !== undefined && startDate !== '' && endDate !== '')) {
const totalCount = await getFilteredTransactionCount(userId, buttonIds, years, timezone, startDate, endDate)
res.status(200).json(totalCount)
} else {
const totalCount = await CacheGet.paymentsCount(userId, timezone)
Expand Down
10 changes: 9 additions & 1 deletion pages/api/payments/download/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,16 @@ export default async (req: any, res: any): Promise<void> => {
if (typeof req.query.years === 'string' && req.query.years !== '') {
years = (req.query.years as string).split(',')
}
let startDate: string | undefined
if (typeof req.query.startDate === 'string' && req.query.startDate !== '') {
startDate = req.query.startDate as string
}
let endDate: string | undefined
if (typeof req.query.endDate === 'string' && req.query.endDate !== '') {
endDate = req.query.endDate as string
}

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.


await downloadTxsFile(res, quoteSlug, timezone, transactions, userId)
} catch (error: any) {
Expand Down
12 changes: 11 additions & 1 deletion pages/api/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export default async (req: any, res: any): Promise<void> => {
if (typeof req.query.years === 'string' && req.query.years !== '') {
years = (req.query.years as string).split(',')
}
let startDate: string | undefined
if (typeof req.query.startDate === 'string' && req.query.startDate !== '') {
startDate = req.query.startDate as string
}
let endDate: string | undefined
if (typeof req.query.endDate === 'string' && req.query.endDate !== '') {
endDate = req.query.endDate as string
}
const userReqTimezone = req.headers.timezone as string
const userProfile = await fetchUserProfileFromId(userId)
const userPreferredTimezone = userProfile?.preferredTimezone
Expand All @@ -31,7 +39,9 @@ export default async (req: any, res: any): Promise<void> => {
orderDesc,
buttonIds,
years,
userPreferredTimezone ?? userReqTimezone
userPreferredTimezone ?? userReqTimezone,
startDate,
endDate
)
res.status(200).json(resJSON)
}
Expand Down
90 changes: 77 additions & 13 deletions pages/payments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
const [invoiceMode, setInvoiceMode] = useState<'create' | 'edit' | 'view'>('create')
const [isModalOpen, setIsModalOpen] = useState(false)

const [startDate, setStartDate] = useState<string>('')
const [endDate, setEndDate] = useState<string>('')

const fetchNextInvoiceNumberByUserId = async (): Promise<string> => {
const response = await fetch('/api/invoices/invoiceNumber/', {
headers: {
Expand Down Expand Up @@ -136,7 +139,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
}
useEffect(() => {
setRefreshCount(prev => prev + 1)
}, [selectedButtonIds, selectedTransactionYears])
}, [selectedButtonIds, selectedTransactionYears, endDate])

const fetchPaybuttons = async (): Promise<any> => {
const res = await fetch(`/api/paybuttons?userId=${user?.userProfile.id}`, {
Expand Down Expand Up @@ -193,19 +196,33 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
if (selectedTransactionYears.length > 0) {
url += `&years=${selectedTransactionYears.join(',')}`
}
if (startDate !== '') {
url += `&startDate=${startDate}`
}
if (endDate !== '') {
url += `&endDate=${endDate}`
}

const paymentsResponse = await fetch(url, {
headers: {
Timezone: moment.tz.guess()
}
})
let paymentsCountUrl = '/api/payments/count'
if (selectedButtonIds.length > 0) {
paymentsCountUrl += `?buttonIds=${selectedButtonIds.join(',')}`
}
if (selectedTransactionYears.length > 0) {
paymentsCountUrl += `${selectedButtonIds.length > 0 ? '&' : '?'}years=${selectedTransactionYears.join(',')}`
paymentsCountUrl += `${paymentsCountUrl.includes('?') ? '&' : '?'}years=${selectedTransactionYears.join(',')}`
}
if (startDate !== '') {
paymentsCountUrl += `${paymentsCountUrl.includes('?') ? '&' : '?'}startDate=${startDate}`
}
if (endDate !== '') {
paymentsCountUrl += `${paymentsCountUrl.includes('?') ? '&' : '?'}endDate=${endDate}`
}

const paymentsResponse = await fetch(url, {
headers: {
Timezone: moment.tz.guess()
}
})

const paymentsCountResponse = await fetch(
paymentsCountUrl,
{ headers: { Timezone: timezone } }
Expand Down Expand Up @@ -390,8 +407,14 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
if (selectedTransactionYears.length > 0) {
url += `&years=${selectedTransactionYears.join(',')}`
}
const isCurrencyEmptyOrUndefined = (value: string): boolean => (value === '' || value === undefined)
if (startDate !== '') {
url += `&startDate=${startDate}`
}
if (endDate !== '') {
url += `&endDate=${endDate}`
}

const isCurrencyEmptyOrUndefined = (value: string): boolean => (value === '' || value === undefined)
if (!isCurrencyEmptyOrUndefined(currency)) {
url += `&network=${currency}`
}
Expand Down Expand Up @@ -443,10 +466,13 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
const handleClearFilters = (): void => {
setSelectedButtonIds([])
setSelectedTransactionYears([])
setStartDate('')
setEndDate('')
}

return (
<>
<TopBar title="Payments" user={user?.stUser?.email} />
<TopBar title="Payments" user={user?.stUser?.email} />
<div className={style.filters_export_ctn}>
<div className={style.filter_btns}>
<div
Expand All @@ -455,7 +481,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
>
<Image src={SettingsIcon} alt="filters" width={15} />Filters
</div>
{(selectedButtonIds.length > 0 || selectedTransactionYears.length > 0) &&
{(selectedButtonIds.length > 0 || selectedTransactionYears.length > 0 || startDate !== '' || endDate !== '') &&
<div
onClick={() => handleClearFilters()}
className={style.show_filters_button}
Expand Down Expand Up @@ -524,11 +550,17 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
<div
key={y}
onClick={() => {
setSelectedTransactionYears(prev =>
prev.includes(y)
setSelectedTransactionYears(prev => {
const newYears = prev.includes(y)
? prev.filter(year => year !== y)
: [...prev, y]
)

if (newYears.length > 0) {
setStartDate('')
setEndDate('')
}
return newYears
})
}}
className={`${style.filter_button} ${selectedTransactionYears.includes(y) ? style.active : ''}`}
>
Expand All @@ -537,6 +569,38 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
))}
</div>
</div>
<div className={style.showfilters_ctn}>
<span>Filter by date range</span>
<div className={style.filters_ctn} style={{ alignItems: 'center' }}>
<input
type="date"
value={startDate}
onChange={(e) => {
const newStartDate = e.target.value
setStartDate(newStartDate)
if (newStartDate !== '') {
setSelectedTransactionYears([])
}
setEndDate('')
}}
className={style.filter_input}
/>
<span style={{ margin: '0 8px' }}>to</span>
<input
type="date"
value={endDate}
onChange={(e) => {
const newEndDate = e.target.value
setEndDate(newEndDate)
if (newEndDate !== '') {
setSelectedTransactionYears([])
}
}}
min={startDate !== '' ? startDate : undefined}
className={style.filter_input}
/>
</div>
</div>
</div>
)}
<TableContainerGetter
Expand Down
108 changes: 69 additions & 39 deletions services/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,9 @@ export async function fetchAllPaymentsByUserIdWithPagination (
orderDesc = true,
buttonIds?: string[],
years?: string[],
timezone?: string
timezone?: string,
startDate?: string,
endDate?: string
): Promise<Payment[]> {
const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'

Expand Down Expand Up @@ -792,18 +794,15 @@ export async function fetchAllPaymentsByUserIdWithPagination (

const where: Prisma.TransactionWhereInput = {
address: {
userProfiles: {
some: { userId }
}
userProfiles: { some: { userId } }
},
amount: {
gt: 0
}
amount: { gt: 0 }
}
if (years !== undefined && years.length > 0) {
const yearFilters = getYearFilters(years, timezone)

where.OR = yearFilters
if (startDate !== undefined && endDate !== undefined && startDate !== '' && endDate !== '') {
Object.assign(where, getDateRangeFilter(startDate, endDate, timezone))
} else if (years !== undefined && years.length > 0) {
where.OR = getYearFilters(years, timezone)
}

if ((buttonIds !== undefined) && buttonIds.length > 0) {
Expand Down Expand Up @@ -834,29 +833,55 @@ export async function fetchAllPaymentsByUserIdWithPagination (
}
return transformedData
}
const getYearFilters = (years: string[], timezone?: string): Prisma.TransactionWhereInput[] => {
return years.map((year) => {
let start: number
let end: number
if (timezone !== undefined && timezone !== null && timezone !== '') {
const startDate = new Date(`${year}-01-01T00:00:00`)
const endDate = new Date(`${Number(year) + 1}-01-01T00:00:00`)
const startInTimezone = new Date(startDate.toLocaleString('en-US', { timeZone: timezone }))
const endInTimezone = new Date(endDate.toLocaleString('en-US', { timeZone: timezone }))
const startOffset = startDate.getTime() - startInTimezone.getTime()
const endOffset = endDate.getTime() - endInTimezone.getTime()
start = (startDate.getTime() + startOffset) / 1000
end = (endDate.getTime() + endOffset) / 1000
} else {
start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000
end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000
}

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)
}
}
Comment on lines +837 to +866
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.


const getDateRangeFilter = (
startDate: string,
endDate: string,
timezone?: string
): Prisma.TransactionWhereInput => ({
timestamp: buildDateRange(startDate, endDate, timezone)
})

const getYearFilters = (
years: string[],
timezone?: string
): Prisma.TransactionWhereInput[] => {
return years.map((year) => {
const startDate = `${year}-01-01T00:00:00`
const endDate = `${Number(year) + 1}-01-01T00:00:00`
return {
timestamp: {
gte: Math.floor(start),
lt: Math.floor(end)
}
timestamp: buildDateRange(startDate, endDate, timezone)
}
})
}
Expand All @@ -866,6 +891,8 @@ export async function fetchAllPaymentsByUserId (
networkIds?: number[],
buttonIds?: string[],
years?: string[],
startDate?: string,
endDate?: string,
timezone?: string
): Promise<TransactionsWithPaybuttonsAndPrices[]> {
const where: Prisma.TransactionWhereInput = {
Expand All @@ -892,10 +919,10 @@ export async function fetchAllPaymentsByUserId (
}
}

if (years !== undefined && years.length > 0) {
const yearFilters = getYearFilters(years, timezone)

where.OR = yearFilters
if (startDate !== undefined && endDate !== undefined && startDate !== '' && endDate !== '') {
Object.assign(where, getDateRangeFilter(startDate, endDate, timezone))
} else if (years !== undefined && years.length > 0) {
where.OR = getYearFilters(years, timezone)
}

return await prisma.transaction.findMany({
Expand Down Expand Up @@ -923,7 +950,9 @@ export const getFilteredTransactionCount = async (
userId: string,
buttonIds?: string[],
years?: string[],
timezone?: string
timezone?: string,
startDate?: string,
endDate?: string
): Promise<number> => {
const where: Prisma.TransactionWhereInput = {
address: {
Expand All @@ -942,10 +971,11 @@ export const getFilteredTransactionCount = async (
}
}
}
if (years !== undefined && years.length > 0) {
const yearFilters = getYearFilters(years, timezone)

where.OR = yearFilters
if (startDate !== undefined && endDate !== undefined && startDate !== '' && endDate !== '') {
Object.assign(where, getDateRangeFilter(startDate, endDate, timezone))
} else if (years !== undefined && years.length > 0) {
where.OR = getYearFilters(years, timezone)
}

return await prisma.transaction.count({ where })
Expand Down