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
19 changes: 10 additions & 9 deletions src/services/admin-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ const adminService = {
e.id AS event_id, e.name AS event_name,
COUNT(t.id)::int AS tickets_generated,
COUNT(t.check_in_at)::int AS tickets_checked_in,
p.transaction_id, p.gateway, p.status AS gateway_status, p.paid_at
p.transaction_id, p.juspay_order_id, p.gateway, p.status AS gateway_status, p.paid_at
FROM bookings b
Comment on lines +548 to 549

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== getPaymentsWithTickets row shape ==\n'
sed -n '539,586p' src/services/admin-service.js

printf '\n== PaymentAnalysisView row key ==\n'
sed -n '505,518p' src/views/admin/payments/PaymentAnalysisView.js

printf '\n== payments table schema ==\n'
sed -n '133,160p' schema.sql

Repository: NexEvent/Citronics_2k26

Length of output: 3748


Add a payment-scoped row ID to prevent DataGrid collisions.

The query groups by p.juspay_order_id and other payment fields without grouping by p.id. This means a single booking with multiple payments will produce multiple rows in the result—all with the same b.id. The frontend grid at src/views/admin/payments/PaymentAnalysisView.js, Line 516 uses getRowId={row => row.id} (booking ID), causing duplicate row IDs in the DataGrid whenever a booking has more than one payment.

Suggested change
-             COUNT(t.check_in_at)::int AS tickets_checked_in,
-             p.transaction_id, p.juspay_order_id, p.gateway, p.status AS gateway_status, p.paid_at
+             COUNT(t.check_in_at)::int AS tickets_checked_in,
+             p.id AS payment_id, p.transaction_id, p.juspay_order_id, p.gateway, p.status AS gateway_status, p.paid_at
...
-               p.transaction_id, p.juspay_order_id, p.gateway, p.status, p.paid_at`
+               p.id, p.transaction_id, p.juspay_order_id, p.gateway, p.status, p.paid_at`

Update the grid row key:

-          getRowId={row => row.id}
+          getRowId={row => row.payment_id ?? row.id}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/admin-service.js` around lines 548 - 549, The SQL SELECT is
missing the payment row identifier (p.id) which causes multiple payments for the
same booking to produce duplicate b.id rows and collide with the frontend
DataGrid that uses getRowId={row => row.id}; update the query that builds the
payment rows to include p.id (or alias it as payment_id) in the SELECT and GROUP
BY so each payment row has a unique id, and ensure the frontend
PaymentAnalysisView's grid uses that payment_id (or the new unique alias) as the
row key instead of booking id.

JOIN users u ON u.id = b.user_id
JOIN events e ON e.id = b.event_id
Expand Down Expand Up @@ -578,7 +578,7 @@ const adminService = {

if (conditions.length > 0) query += ` WHERE ${conditions.join(' AND ')}`
query += ` GROUP BY b.id, u.id, u.name, u.email, u.phone, e.id, e.name,
p.transaction_id, p.gateway, p.status, p.paid_at`
p.transaction_id, p.juspay_order_id, p.gateway, p.status, p.paid_at`
query += ` ORDER BY b.booked_at DESC LIMIT $${p++} OFFSET $${p++}`
params.push(limit, offset)

Expand Down Expand Up @@ -613,22 +613,23 @@ const adminService = {

const whereClause = conditions.join(' AND ')

// For tickets subquery, we need to rebuild the conditions
const ticketConditions = managerId
// For tickets/payment subqueries, we need to rebuild the conditions
const subqueryManagerCond = managerId
? `bk.event_id IN (SELECT id FROM events WHERE manager_id = $${managerParamPosition})`
: '1=1'
const ticketDateCond = [`bk.booked_at >= $${fromParamPosition}`]
if (toParamPosition) ticketDateCond.push(`bk.booked_at <= $${toParamPosition}`)
const ticketWhere = [ticketConditions, ...ticketDateCond, 'bk.status = \'confirmed\''].filter(Boolean).join(' AND ')
const subqueryDateCond = [`bk.booked_at >= $${fromParamPosition}`]
if (toParamPosition) subqueryDateCond.push(`bk.booked_at <= $${toParamPosition}`)
const ticketWhere = [subqueryManagerCond, ...subqueryDateCond, 'bk.status = \'confirmed\''].filter(Boolean).join(' AND ')
const paymentWhere = [subqueryManagerCond, ...subqueryDateCond, "p.status IN ('success', 'refunded')"].filter(Boolean).join(' AND ')
Comment on lines +620 to +623

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't window refunded revenue by bk.booked_at.

The new net-revenue formula still uses the booking timestamp for date filtering. That makes historical ranges unstable: a refund posted later changes revenue for the original booking window because refund_amount is subtracted regardless of refund_at. If this report is meant to reconcile with payment data, the windowing needs to use payment/refund timestamps, not the booking date.

Also applies to: 631-632


const row = await dbOneOrNone(`
SELECT
COUNT(*)::int AS total_payments,
COUNT(*) FILTER (WHERE b.status = 'confirmed')::int AS successful_payments,
COUNT(*) FILTER (WHERE b.status = 'pending')::int AS pending_payments,
COUNT(*) FILTER (WHERE b.status = 'cancelled')::int AS failed_payments,
COALESCE(SUM(b.total_amount) FILTER (WHERE b.status = 'confirmed'), 0)::numeric AS total_revenue,
COALESCE(AVG(b.total_amount) FILTER (WHERE b.status = 'confirmed'), 0)::numeric AS avg_order_value,
COALESCE((SELECT SUM(p.amount - COALESCE(p.refund_amount, 0)) FROM payments p JOIN bookings bk ON bk.id = p.booking_id WHERE ${paymentWhere}), 0)::numeric AS total_revenue,
COALESCE((SELECT AVG(p.amount - COALESCE(p.refund_amount, 0)) FROM payments p JOIN bookings bk ON bk.id = p.booking_id WHERE ${paymentWhere} AND (p.amount - COALESCE(p.refund_amount, 0)) > 0), 0)::numeric AS avg_order_value,
Comment on lines +631 to +632

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

getPaymentStats now computes revenue/AOV from payments joined via p.booking_id. But payments are created as a single record linked to the first booking in a multi-event order (see payment-service comment), so manager-scoped stats can incorrectly include/exclude revenue depending on which booking was first, and AOV becomes per-payment rather than per-booking. Consider deriving manager revenue from bookings (as before) or expanding the payments subquery to associate a payment to all bookingIds stored in payments.raw_payload so manager/date filters apply correctly.

Suggested change
COALESCE((SELECT SUM(p.amount - COALESCE(p.refund_amount, 0)) FROM payments p JOIN bookings bk ON bk.id = p.booking_id WHERE ${paymentWhere}), 0)::numeric AS total_revenue,
COALESCE((SELECT AVG(p.amount - COALESCE(p.refund_amount, 0)) FROM payments p JOIN bookings bk ON bk.id = p.booking_id WHERE ${paymentWhere} AND (p.amount - COALESCE(p.refund_amount, 0)) > 0), 0)::numeric AS avg_order_value,
COALESCE((
SELECT SUM(p.amount - COALESCE(p.refund_amount, 0))
FROM payments p
JOIN LATERAL jsonb_array_elements_text(
COALESCE(
(p.raw_payload->'bookingIds')::jsonb,
to_jsonb(ARRAY[p.booking_id]::int[])
)
) AS bid(booking_id_text) ON TRUE
JOIN bookings bk ON bk.id = bid.booking_id_text::int
WHERE ${paymentWhere}
), 0)::numeric AS total_revenue,
COALESCE((
SELECT AVG(p.amount - COALESCE(p.refund_amount, 0))
FROM payments p
JOIN LATERAL jsonb_array_elements_text(
COALESCE(
(p.raw_payload->'bookingIds')::jsonb,
to_jsonb(ARRAY[p.booking_id]::int[])
)
) AS bid(booking_id_text) ON TRUE
JOIN bookings bk ON bk.id = bid.booking_id_text::int
WHERE ${paymentWhere} AND (p.amount - COALESCE(p.refund_amount, 0)) > 0
), 0)::numeric AS avg_order_value,

Copilot uses AI. Check for mistakes.
(SELECT COUNT(*)::int FROM tickets t JOIN bookings bk ON bk.id = t.booking_id WHERE ${ticketWhere}) AS total_tickets,
(SELECT COUNT(*)::int FROM tickets t JOIN bookings bk ON bk.id = t.booking_id WHERE t.check_in_at IS NOT NULL AND ${ticketWhere}) AS checked_in_tickets
FROM bookings b
Expand Down
28 changes: 28 additions & 0 deletions src/views/admin/payments/PaymentAnalysisView.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ const PaymentAnalysisView = () => {
const statusData = Object.entries(statusCounts).filter(([, v]) => v > 0)

const txnCols = [
{
field: 'id', headerName: 'Order ID', width: 100,
renderCell: ({ row }) => (
<Typography variant='body2' fontWeight={600} sx={{ fontFamily: 'monospace' }}>
#{row.id}
</Typography>
Comment on lines +131 to +136

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

This column labels row.id as "Order ID", but in this grid id is the booking id (while the actual payment/order identifier is shown as juspay_order_id). Renaming the header to something like "Booking ID" (or "Booking #") would reduce confusion in the UI and CSV export.

Copilot uses AI. Check for mistakes.
)
},
{
field: 'juspay_order_id', headerName: 'Juspay ID', width: 180,
renderCell: ({ row }) => (
<Tooltip title={row.juspay_order_id || '—'} arrow>
<Typography variant='body2' color='text.secondary' noWrap sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }}>
{row.juspay_order_id || '—'}
</Typography>
</Tooltip>
)
},
{
field: 'transaction_id', headerName: 'Transaction ID', width: 180,
renderCell: ({ row }) => (
<Tooltip title={row.transaction_id || '—'} arrow>
<Typography variant='body2' color='text.secondary' noWrap sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }}>
{row.transaction_id || '—'}
</Typography>
</Tooltip>
)
},
{
field: 'user_name', headerName: 'User', flex: 1, minWidth: 220,
renderCell: ({ row }) => (
Expand Down
Loading