Skip to content
Merged
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, timezone)

await downloadTxsFile(res, quoteSlug, timezone, transactions, userId)
} catch (error: any) {
Expand Down
22 changes: 19 additions & 3 deletions pages/api/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default async (req: any, res: any): Promise<void> => {
if (req.method === 'GET') {
await setSession(req, res)
const userId = req.session.userId
const user = await fetchUserProfileFromId(userId)
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 | 🟠 Major

Timezone fallback bug and unhandled missing-user case

Two related concerns here:

  1. userPreferredTimezone fallback is wrong for undefined/null:

    • When user?.preferredTimezone is undefined or null, the check userPreferredTimezone !== '' is still true, so timezone becomes undefined/null instead of falling back to userReqTimezone.
    • This can lead to timezone being invalid while downstream code expects a usable string (or at least a well-defined default) and uses it for date-range filtering.

    Consider tightening the condition and adding a final default (e.g. 'UTC'):

  • const userPreferredTimezone = user?.preferredTimezone
  • const timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone
  • const userPreferredTimezone = user?.preferredTimezone
  • const timezone =
  •  (userPreferredTimezone && userPreferredTimezone.trim() !== '')
    
  •    ? userPreferredTimezone
    
  •    : (userReqTimezone || 'UTC')
    
    
    
  1. fetchUserProfileFromId(userId) throws when the profile is missing. If there are edge cases where a session exists without a user profile, this will 500 the request. If that’s possible, consider handling the error (e.g. 404 or 401) or relaxing the dependency on the profile when you only need a timezone.

Also applies to: 31-33

🤖 Prompt for AI Agents
In pages/api/payments/index.ts around line 9 (and similarly lines 31-33),
fetchUserProfileFromId(userId) can throw when no profile exists and
user?.preferredTimezone can be null/undefined but the current check allows those
through; update to catch errors from fetchUserProfileFromId and handle
missing-profile cases (return 401/404 or proceed without profile depending on
intended flow), and change the timezone fallback logic to use a strict presence
check (e.g. user?.preferredTimezone && user.preferredTimezone.trim() !== ''),
then compute timezone = userPreferredTimezone ?? userReqTimezone ?? 'UTC' so
null/undefined/empty strings fall back to the request timezone and finally to
'UTC'.

const page = req.query.page as number
const pageSize = req.query.pageSize as number
const orderDesc: boolean = !!(req.query.orderDesc === '' || req.query.orderDesc === undefined || req.query.orderDesc === 'true')
Expand All @@ -19,19 +20,34 @@ 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
const userPreferredTimezone = user?.preferredTimezone
let timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone
if (timezone === '' || timezone === undefined || timezone === null) {
const timezoneValue = timezone === '' ? 'an empty string' : (timezone === undefined ? 'undefined' : 'null')
console.warn(`WARN: Payments API got timezone as ${timezoneValue}, defaulting to UTC`)
timezone = 'UTC'
}

const resJSON = await fetchAllPaymentsByUserIdWithPagination(
userId,
page,
pageSize,
timezone,
orderBy,
orderDesc,
buttonIds,
years,
userPreferredTimezone ?? userReqTimezone
startDate,
endDate
)
res.status(200).json(resJSON)
}
Expand Down
106 changes: 88 additions & 18 deletions pages/payments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import XECIcon from 'assets/xec-logo.png'
import BCHIcon from 'assets/bch-logo.png'
import EyeIcon from 'assets/eye-icon.png'
import { formatQuoteValue, compareNumericString, removeUnserializableFields, removeDateFields } from 'utils/index'
import { XEC_NETWORK_ID, BCH_TX_EXPLORER_URL, XEC_TX_EXPLORER_URL, NETWORK_TICKERS_FROM_ID, DECIMALS } from 'constants/index'
import { XEC_NETWORK_ID, BCH_TX_EXPLORER_URL, XEC_TX_EXPLORER_URL, NETWORK_TICKERS_FROM_ID, DECIMALS, HUMAN_READABLE_DATE_FORMAT } from 'constants/index'
import moment from 'moment-timezone'
import TopBar from 'components/TopBar'
import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService'
Expand Down Expand Up @@ -86,10 +86,13 @@ 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: {
Timezone: moment.tz.guess()
Timezone: timezone
}
})
const result = await response?.json()
Expand All @@ -100,8 +103,12 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
setInvoiceData(null)
setRefreshCount(prev => prev + 1)
}

const todayDateString = moment().format(HUMAN_READABLE_DATE_FORMAT)

const onCreateInvoice = async (transaction: TransactionWithAddressAndPricesAndInvoices): Promise<void> => {
const nextInvoiceNumber = await fetchNextInvoiceNumberByUserId()
const now = new Date()
const invoiceData = {
invoiceNumber: nextInvoiceNumber ?? '',
amount: transaction.amount,
Expand All @@ -113,8 +120,8 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
userId: '',
transaction,
transactionId: transaction.id,
createdAt: new Date(),
updatedAt: new Date(),
createdAt: now,
updatedAt: now,
id: ''
}
setInvoiceDataTransaction(transaction)
Expand All @@ -136,7 +143,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 +200,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: timezone
}
})

const paymentsCountResponse = await fetch(
paymentsCountUrl,
{ headers: { Timezone: timezone } }
Expand Down Expand Up @@ -390,15 +411,21 @@ 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}`
}

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

Expand Down Expand Up @@ -443,10 +470,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 +485,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 +554,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 +573,40 @@ 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}
max={todayDateString}
/>
<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}
max={todayDateString}
className={style.filter_input}
/>
</div>
</div>
</div>
)}
<TableContainerGetter
Expand Down
Loading