feat: extract clean admin/B2B/referral pieces from wave-7-admin-analytics - #29
feat: extract clean admin/B2B/referral pieces from wave-7-admin-analytics#29TechHypeXP wants to merge 7 commits into
Conversation
Extracted from PR #25 (wave-7-admin-analytics). Centralizes B2B/B2C tier detection, discount calculation, and upgrade-path metadata. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracted from PR #25 (wave-7-admin-analytics). KPI cards, revenue and product performance charts (recharts), referral leaderboard, and paginated orders/products tables. Admin auth already goes through the existing verifyAdminAccess (email-whitelist) pattern used elsewhere in this repo. UI adapted from shadcn/ui primitives (not present in this codebase) to this repo's actual @astryxdesign/core Button/Card components. Dropped the QuickActions "Commission Payouts" shortcut since the payouts admin surface is intentionally excluded from this extraction (see PR description). Adds recharts as a new dependency for the two chart components. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracted from PR #25 (wave-7-admin-analytics). Server action creates a b2b_upgrade_requests row and enforces the one-pending-request-per-user invariant; page collects business name, tax ID, address, and credit-check consent. Adapted the auth check from a `getSupabase(cookieStore)` call (invalid — getSupabase's argument is passed straight to supabase-js's createClient, not a cookie jar) to the same sb-access-token/sb-refresh-token cookie restoration pattern already used by verifyAdminAccess() in src/lib/admin/auth.ts, since this repo doesn't use @supabase/ssr. UI swapped from shadcn/ui Input/Button to this repo's actual @astryxdesign/core TextInput/Button. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracted from PR #25 (wave-7-admin-analytics). - api/referrals/track: replaced main's manual referral_stats UPDATE with the atomic update_referral_stats_atomic RPC (Law #1: atomic ops only). The RPC already exists on main (migrations 009/012), main's handler just wasn't calling it. - api/referrals/track-click (new): fire-and-forget click tracking for ?ref=CODE links. Fixed a code-review finding from the source PR: the endpoint always returned success even when the referral_clicks insert failed, making broken tracking invisible. It still always returns { success: true } so the redirect/pixel flow is never blocked, but insert failures are now sent to Sentry and logged as errors instead of being silently swallowed. - components/referrals/ReferralCodeDisplay: copy-to-clipboard code/link card, adapted to this repo's @astryxdesign/core Button. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports mig 015/016 from the closed wave-7-admin-analytics branch, renumbered to 019/020 (next free slots after 018). Admin policies use the existing tier='admin' EXISTS convention (migs 006/010) instead of the bare using(true)/with check(true) the B2B migration originally had. Documents the tax_id-as-plain-text tradeoff since this repo has no column encryption/redaction precedent yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
❌ Deploy Preview for hex-diva failed.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🧙 Sourcery is reviewing your pull request! Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
WalkthroughAdds admin RLS policies, dashboard APIs and pages, a B2B upgrade-request workflow, tier utilities, referral tracking, referral sharing UI, and Recharts visualizations. ChangesB2B upgrade workflow
Admin dashboard
Referral tracking
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
PR Summary by QodoExtract admin analytics, B2B upgrade flow, referrals tracking, and RLS policies
AI Description
Diagram
High-Level Assessment
Files changed (23)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
12 rules 1. Admin APIs lose auth
|
| const { data: existingRequest } = await supabase | ||
| .from('b2b_upgrade_requests' as any) | ||
| .select('id, status') | ||
| .eq('user_id', user.id) | ||
| .single() as any |
There was a problem hiding this comment.
1. as any in supabase calls 📘 Rule violation ⚙ Maintainability
The new B2B upgrade server action uses explicit any casts in Supabase queries, disabling type safety and allowing incorrect DB shapes to propagate at runtime. This violates the rule forbidding direct any usage in changed TS/TSX code.
Agent Prompt
## Issue description
`submitB2BUpgradeRequest()` uses `as any` and table-name casts like `.from('b2b_upgrade_requests' as any)`, which violates the no-`any` rule and removes compile-time guarantees.
## Issue Context
This repo has strict TS settings enabled, so `any` should be replaced with typed query results (e.g., `Database['public']['Tables']['b2b_upgrade_requests']['Row']`) or `unknown` plus narrowing.
## Fix Focus Areas
- src/app/(dashboard)/upgrade-to-b2b/actions.ts[66-100]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const totalRevenue = orders.reduce((sum: number, order: any) => sum + (order.total || 0), 0); | ||
| const thisMonth = new Date(); | ||
| thisMonth.setMonth(thisMonth.getMonth() - 1); | ||
| const ordersThisMonth = orders.filter( | ||
| (o: any) => new Date(o.created_at) > thisMonth | ||
| ).length; | ||
| const avgOrderValue = ordersThisMonth > 0 ? totalRevenue / ordersThisMonth : 0; | ||
|
|
||
| // Calculate YoY growth (simplified: compare this month vs last month) | ||
| const lastYear = new Date(); | ||
| lastYear.setFullYear(lastYear.getFullYear() - 1); | ||
| const ordersLastYear = orders.filter((o: any) => new Date(o.created_at) > lastYear).length; | ||
| const growthYoY = ordersLastYear > 0 ? ((ordersThisMonth - ordersLastYear) / ordersLastYear) * 100 : 0; | ||
|
|
||
| // User metrics | ||
| const totalUsers = users.length; | ||
| const b2cUsers = usersWithTier.filter((u: any) => u.tier === 'b2c').length; | ||
| const b2bUsers = usersWithTier.filter((u: any) => u.tier === 'b2b').length; | ||
| const signupsThisMonth = users.filter( | ||
| (u: any) => new Date(u.created_at) > thisMonth | ||
| ).length; | ||
|
|
||
| // Revenue over time (last 30 days, grouped by day) | ||
| const revenueChartMap = new Map<string, number>(); | ||
| orders.forEach((order: any) => { | ||
| if (!order.created_at) return; |
There was a problem hiding this comment.
2. any in dashboard calculations 📘 Rule violation ⚙ Maintainability
The admin dashboard API uses any for orders/users/items during KPI and chart aggregation, which disables type safety in a security-sensitive admin surface. This violates the no-any TypeScript compliance rule.
Agent Prompt
## Issue description
The dashboard route relies on `any` (e.g., `(order: any)`, `(o: any)`, `(u: any)`, `(item: any)`) for data returned from Supabase queries.
## Issue Context
Use generated DB types (`Database` types) or explicit interfaces for the selected columns and relationships (`orders`, `users`, `order_items`, nested `products(name)`) to keep strict typing and prevent shape drift.
## Fix Focus Areas
- src/app/api/admin/dashboard/route.ts[69-131]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const transformedOrders = (orders || []).map((order: any) => ({ | ||
| id: order.id, | ||
| user_id: order.user_id, | ||
| email: order.users?.email || 'Unknown', | ||
| status: order.status, | ||
| total: order.total, | ||
| created_at: order.created_at, | ||
| item_count: order.order_items?.length || 0, | ||
| })); |
There was a problem hiding this comment.
3. any in orders mapping 📘 Rule violation ⚙ Maintainability
The admin orders API maps results using (order: any), disabling type checking for the response payload. This violates the compliance rule disallowing explicit any in modified TypeScript.
Agent Prompt
## Issue description
The code uses `(order: any)` when transforming Supabase `orders` results, violating the no-`any` rule.
## Issue Context
Define a type for the selected shape (`id`, `user_id`, `status`, `total`, `created_at`, `users(email)`, `order_items(id)`) and use it in the map callback.
## Fix Focus Areas
- src/app/api/admin/orders/route.ts[45-53]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const referrerIds = (referralStats || []).map((s: any) => s.referrer_id); | ||
| const { data: users, error: usersError } = await supabase | ||
| .from('users') | ||
| .select('id, email, display_name') | ||
| .in('id', referrerIds); | ||
|
|
||
| if (usersError) throw usersError; | ||
|
|
||
| // Create a map of user data for quick lookup | ||
| const userMap = new Map( | ||
| (users || []).map((u: any) => [u.id, { email: u.email, name: u.display_name }]) | ||
| ); |
There was a problem hiding this comment.
4. any in referrals mapping 📘 Rule violation ⚙ Maintainability
The admin referrals API uses any for referral stat rows, user rows, and payout rows, reducing compile-time safety for the admin leaderboard response. This violates the compliance rule disallowing explicit any in TypeScript source.
Agent Prompt
## Issue description
The route uses `any` in multiple places (e.g., `(s: any)`, `(u: any)`, `(payout: any)`, `(stat: any)`), violating the no-`any` rule.
## Issue Context
Introduce types for `referral_stats` rows, `users` rows, and `commission_payouts` rows matching the `.select(...)` shapes used in this handler.
## Fix Focus Areas
- src/app/api/admin/referrals/route.ts[29-67]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| interface Order { | ||
| id: string; | ||
| user_id: string; | ||
| email: string; | ||
| status: string; | ||
| total: number; | ||
| created_at: string; | ||
| item_count: number; | ||
| } |
There was a problem hiding this comment.
7. user_id breaks camelcase 📘 Rule violation ⚙ Maintainability
New TS interfaces and API payload shapes use snake_case identifiers like user_id, created_at, and item_count instead of camelCase. This violates the camelCase identifier naming requirement for non-component JS/TS identifiers.
Agent Prompt
## Issue description
Snake_case identifiers are introduced in TypeScript interfaces and UI state (`user_id`, `created_at`, `item_count`, `in_stock`).
## Issue Context
If these shapes mirror DB columns, keep DB access in snake_case but map API responses to camelCase DTOs at the route boundary (e.g., `userId`, `createdAt`, `itemCount`, `inStock`) so application code stays consistent.
## Fix Focus Areas
- src/app/(admin)/orders/page.tsx[9-17]
- src/app/(admin)/products/page.tsx[8-17]
- src/app/api/admin/orders/route.ts[45-53]
- src/app/api/admin/products/route.ts[25-39]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const [ordersData, usersData, usersWithTierData, topReferrersData, productsData] = await Promise.all([ | ||
| // Get orders metrics | ||
| supabase | ||
| .from('orders') | ||
| .select('id, total, created_at, status') | ||
| .gte('created_at', new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString()), | ||
|
|
||
| // Get user metrics | ||
| supabase | ||
| .from('users') | ||
| .select('id, created_at') | ||
| .gte('created_at', new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString()), | ||
|
|
||
| // Get user tier distribution | ||
| supabase | ||
| .from('users') | ||
| .select('id, tier'), | ||
|
|
||
| // Get top referrers | ||
| supabase | ||
| .from('referral_stats') | ||
| .select('referrer_id, total_commission_earned') | ||
| .order('total_commission_earned', { ascending: false }) | ||
| .limit(10), | ||
|
|
||
| // Get top products by revenue | ||
| supabase | ||
| .from('order_items') | ||
| .select('product_id, quantity, total, products(name)') | ||
| .limit(1000), | ||
| ]); | ||
|
|
||
| if ( | ||
| ordersData.error || | ||
| usersData.error || | ||
| usersWithTierData.error || | ||
| topReferrersData.error || | ||
| productsData.error | ||
| ) { | ||
| throw new Error('Failed to fetch data from database'); | ||
| } | ||
|
|
||
| // Calculate KPIs | ||
| const orders = ordersData.data || []; | ||
| const users = usersData.data || []; | ||
| const usersWithTier = usersWithTierData.data || []; | ||
| const topReferrers = topReferrersData.data || []; | ||
| const items = productsData.data || []; | ||
|
|
||
| const totalRevenue = orders.reduce((sum: number, order: any) => sum + (order.total || 0), 0); | ||
| const thisMonth = new Date(); | ||
| thisMonth.setMonth(thisMonth.getMonth() - 1); | ||
| const ordersThisMonth = orders.filter( | ||
| (o: any) => new Date(o.created_at) > thisMonth | ||
| ).length; | ||
| const avgOrderValue = ordersThisMonth > 0 ? totalRevenue / ordersThisMonth : 0; | ||
|
|
||
| // Calculate YoY growth (simplified: compare this month vs last month) | ||
| const lastYear = new Date(); | ||
| lastYear.setFullYear(lastYear.getFullYear() - 1); | ||
| const ordersLastYear = orders.filter((o: any) => new Date(o.created_at) > lastYear).length; | ||
| const growthYoY = ordersLastYear > 0 ? ((ordersThisMonth - ordersLastYear) / ordersLastYear) * 100 : 0; | ||
|
|
||
| // User metrics | ||
| const totalUsers = users.length; | ||
| const b2cUsers = usersWithTier.filter((u: any) => u.tier === 'b2c').length; | ||
| const b2bUsers = usersWithTier.filter((u: any) => u.tier === 'b2b').length; | ||
| const signupsThisMonth = users.filter( | ||
| (u: any) => new Date(u.created_at) > thisMonth | ||
| ).length; |
There was a problem hiding this comment.
9. Dashboard kpis miscomputed 🐞 Bug ≡ Correctness
GET /api/admin/dashboard computes KPIs with inconsistent windows/denominators (e.g., “All time” revenue is derived from a 90-day query and avgOrderValue divides 90-day revenue by 1-month order count; growthYoY compares last-month orders to ‘last year’ computed from the same 90-day set).
Agent Prompt
## Issue description
The dashboard endpoint mixes time windows and uses incorrect denominators, producing misleading KPI values (total revenue labeled “All time”, incorrect AOV, and nonsensical YoY growth).
## Issue Context
The UI explicitly labels total revenue as “All time”, and the endpoint currently only fetches orders for the last 90 days and users for the last 365 days.
## Fix Focus Areas
- src/app/api/admin/dashboard/route.ts[20-89]
- src/app/(admin)/dashboard/page.tsx[115-123]
## Suggested fix
- Decide the intended KPI semantics (true all-time vs last-90-days vs current calendar month).
- Adjust queries accordingly:
- For all-time counts/sums, prefer `select(..., { count: 'exact', head: true })` for counts and/or a dedicated RPC for sums.
- For AOV, compute `revenueThisMonth / ordersThisMonth` using the same time window.
- For YoY, compare the same period last year (e.g., last 30 days vs same 30 days shifted by 1 year) using separate filtered datasets.
- Update UI labels if you intentionally keep “last 90 days/last year” semantics.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Fetch corresponding user emails | ||
| const referrerIds = (referralStats || []).map((s: any) => s.referrer_id); | ||
| const { data: users, error: usersError } = await supabase | ||
| .from('users') | ||
| .select('id, email, display_name') | ||
| .in('id', referrerIds); | ||
|
|
||
| if (usersError) throw usersError; | ||
|
|
||
| // Create a map of user data for quick lookup | ||
| const userMap = new Map( | ||
| (users || []).map((u: any) => [u.id, { email: u.email, name: u.display_name }]) | ||
| ); | ||
|
|
||
| // Fetch pending commissions (not yet paid) | ||
| const { data: pendingCommissions, error: pendingError } = await supabase | ||
| .from('commission_payouts') | ||
| .select('referrer_id, amount, status') | ||
| .in('referrer_id', referrerIds) | ||
| .eq('status', 'pending'); | ||
|
|
||
| if (pendingError) throw pendingError; |
There was a problem hiding this comment.
10. Empty referrals query 500 🐞 Bug ☼ Reliability
GET /api/admin/referrals runs .in() queries with an empty referrerIds array when there are no referral stats, which can turn a valid empty leaderboard into a 500.
Agent Prompt
## Issue description
When `referral_stats` returns no rows, `referrerIds` becomes an empty array but the handler still executes `.in('id', referrerIds)` and `.in('referrer_id', referrerIds)`. This can error at the PostgREST layer and currently results in a 500.
## Issue Context
The endpoint should return an empty list cleanly when there are no referrals.
## Fix Focus Areas
- src/app/api/admin/referrals/route.ts[19-49]
## Suggested fix
- After building `referrerIds`, add:
- `if (referrerIds.length === 0) return NextResponse.json({ success: true, data: [] });`
- Alternatively, skip the follow-up queries and keep `userMap/pendingMap` empty when there are no ids.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const { data: existingRequest } = await supabase | ||
| .from('b2b_upgrade_requests' as any) | ||
| .select('id, status') | ||
| .eq('user_id', user.id) | ||
| .single() as any | ||
|
|
||
| if (existingRequest?.status === 'pending') { | ||
| return { | ||
| success: false, | ||
| error: 'You already have a pending B2B upgrade request. Please wait for our review.', | ||
| } | ||
| } | ||
|
|
||
| if (existingRequest?.status === 'approved') { | ||
| return { | ||
| success: false, | ||
| error: 'Your account is already B2B. Please check your tier settings.', | ||
| } | ||
| } | ||
|
|
||
| const { data: createdRequest, error: insertError } = await supabase | ||
| .from('b2b_upgrade_requests' as any) | ||
| .insert({ | ||
| user_id: user.id, | ||
| business_name: data.businessName, | ||
| tax_id: data.taxId, | ||
| business_address: data.businessAddress || null, | ||
| credit_check_consented: data.creditCheckConsented, | ||
| status: 'pending', | ||
| created_at: new Date().toISOString(), | ||
| updated_at: new Date().toISOString(), | ||
| }) | ||
| .select('id') | ||
| .single() as any | ||
|
|
There was a problem hiding this comment.
11. Rejected b2b requests stuck 🐞 Bug ≡ Correctness
Users cannot resubmit after rejection: the table enforces a UNIQUE user_id and user updates are only allowed while status='pending', but the server action falls through on 'rejected' and attempts a new insert that will violate the unique constraint.
Agent Prompt
## Issue description
A rejected B2B upgrade request cannot be resubmitted. The schema allows only one row per user (`user_id unique`), RLS only allows user updates when `status='pending'`, and the action inserts a new row for any non-pending/non-approved status (including rejected).
## Issue Context
The UX implies users can correct info and try again, but the current constraints/policies prevent it.
## Fix Focus Areas
- migrations/020_b2b_upgrade_requests.sql[5-8]
- migrations/020_b2b_upgrade_requests.sql[57-63]
- src/app/(dashboard)/upgrade-to-b2b/actions.ts[66-107]
## Suggested fix
Pick one consistent model:
1) **Single-row-per-user (keep UNIQUE):**
- Update the action: if existing status is `rejected`, perform an `update` of the existing row to set new fields and set `status='pending'`.
- Update RLS to allow that transition (e.g., allow updates when status in ('pending','rejected') and with-check allows status='pending').
2) **Request history (remove UNIQUE):**
- Remove/alter the unique constraint, insert new rows on resubmission, and update reads to select the latest request (`order by created_at desc limit 1`) instead of `.single()`.
Also update the user-facing error message for the rejected case to avoid a generic “Failed to create upgrade request”.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| -- Create indexes for performance | ||
| create index idx_b2b_upgrade_requests_user on public.b2b_upgrade_requests(user_id); | ||
| create index idx_b2b_upgrade_requests_status on public.b2b_upgrade_requests(status); | ||
| create index idx_b2b_upgrade_requests_created_at on public.b2b_upgrade_requests(created_at desc); | ||
|
|
||
| -- Enable RLS | ||
| alter table public.b2b_upgrade_requests enable row level security; | ||
|
|
||
| -- RLS Policies | ||
| -- Users can read their own upgrade requests | ||
| create policy "Users can read own upgrade requests" | ||
| on public.b2b_upgrade_requests | ||
| for select | ||
| using (auth.uid() = user_id); | ||
|
|
||
| -- Admin can read all upgrade requests | ||
| -- Matches the tier='admin' EXISTS convention used in migs 006/010/019, rather than | ||
| -- a bare `using (true)` that would let any authenticated row-owner-unrelated caller through. | ||
| create policy "Admin can read all upgrade requests" | ||
| on public.b2b_upgrade_requests | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Users can insert their own requests | ||
| create policy "Users can insert own upgrade requests" | ||
| on public.b2b_upgrade_requests | ||
| for insert | ||
| with check (auth.uid() = user_id); | ||
|
|
||
| -- Users can update their own pending requests | ||
| create policy "Users can update own pending requests" | ||
| on public.b2b_upgrade_requests | ||
| for update | ||
| using (auth.uid() = user_id and status = 'pending') | ||
| with check (auth.uid() = user_id and status = 'pending'); | ||
|
|
||
| -- Admin can update any request (reviewed_by, status, reviewed_at, rejection_reason) | ||
| create policy "Admin can update all requests" | ||
| on public.b2b_upgrade_requests | ||
| for update | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')) | ||
| with check (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); |
There was a problem hiding this comment.
12. Migration rerun can fail 🐞 Bug ☼ Reliability
migrations/020_b2b_upgrade_requests.sql uses CREATE TABLE IF NOT EXISTS but then creates indexes and policies without IF NOT EXISTS/drop guards, so a partial apply or manual rerun can fail with duplicate-object errors.
Agent Prompt
## Issue description
Migration 020 is internally inconsistent about idempotency: it guards table creation but not indexes/policies. If the migration partially applied (table created, then failed later) or is re-executed manually, it can fail on already-existing indexes/policies.
## Issue Context
This repo already uses drop+create for policies in other migrations, and uses `create index if not exists` elsewhere.
## Fix Focus Areas
- migrations/020_b2b_upgrade_requests.sql[28-69]
## Suggested fix
- Change indexes to `create index if not exists ...`.
- Prepend each policy with `drop policy if exists ... on public.b2b_upgrade_requests;` before the `create policy ...`.
- Keep `CREATE TABLE IF NOT EXISTS` only if you’re aiming for replay safety; otherwise remove it for consistency (but prefer replay safety if your deployment process benefits from it).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Track click (fire-and-forget, don't block response) | ||
| // Intentionally not awaiting this to avoid blocking the response. | ||
| // NOTE: this endpoint always returns { success: true } to the caller | ||
| // regardless of whether the insert below succeeds, so the redirect/pixel | ||
| // flow is never broken by a tracking failure. But a silently swallowed | ||
| // insert failure makes broken click-tracking invisible in practice, so | ||
| // failures here are surfaced to Sentry (with the referral code as a tag) | ||
| // and logged as an error rather than a generic console.error, making | ||
| // them alertable instead of only discoverable by manually tailing logs. | ||
| void (async () => { | ||
| try { | ||
| const { error: insertError } = await supabaseAdmin | ||
| .from('referral_clicks') | ||
| .insert({ | ||
| referral_id: referral.id, | ||
| clicked_at: new Date().toISOString(), | ||
| }) | ||
|
|
||
| if (insertError) { | ||
| Sentry.captureException(insertError, { | ||
| tags: { endpoint: 'referrals/track-click', referralCode }, | ||
| }) | ||
| console.error( | ||
| `Failed to record referral click for code ${referralCode}:`, | ||
| insertError | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| console.log(`Referral click tracked: ${referralCode}`) | ||
| } catch (error) { | ||
| Sentry.captureException(error, { | ||
| tags: { endpoint: 'referrals/track-click', referralCode }, | ||
| }) | ||
| console.error('Failed to track referral click:', error) | ||
| } | ||
| })() | ||
|
|
||
| // Always return success immediately | ||
| return NextResponse.json({ success: true }, { status: 200 }) | ||
| } catch (error) { |
There was a problem hiding this comment.
13. Fire-and-forget click inserts 🐞 Bug ☼ Reliability
POST /api/referrals/track-click returns success before awaiting the referral_clicks insert; if the runtime stops executing after the response, click records can be silently dropped even though the caller sees success.
Agent Prompt
## Issue description
The referral click tracking handler spawns an async IIFE and returns immediately. This makes click recording best-effort and can drop events if execution doesn’t continue reliably after the response.
## Issue Context
The handler already treats tracking failures as non-fatal (always returns `{ success: true }`), so awaiting the insert doesn’t need to change the API contract.
## Fix Focus Areas
- src/app/api/referrals/track-click/route.ts[36-76]
## Suggested fix
- Prefer awaiting the insert and still returning `{ success: true }` regardless of insert success.
- If you truly need to avoid adding DB latency to the request, route events into a durable async mechanism (queue/cron/worker) that guarantees execution beyond the request lifecycle.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 29
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/api/referrals/track/route.ts (1)
101-115: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winNo idempotency guard against duplicate commission creation.
If this endpoint is invoked twice for the same
orderId(client retry, duplicate webhook delivery, etc.), nothing prevents inserting a secondcommissionsrow and re-runningupdate_referral_stats_atomic, double-crediting the referrer's commission stats. Consider checking for an existing commission byorder_idbefore insert, and/or adding a unique constraint oncommissions.order_idat the DB level as a hard guarantee.🛡️ Suggested guard
+ // Guard against duplicate processing (retries, redelivery) + const { data: existingCommission } = await supabaseAdmin + .from('commissions') + .select('id') + .eq('order_id', orderId) + .maybeSingle(); + + if (existingCommission) { + return NextResponse.json( + { success: false, message: 'Commission already recorded for this order' }, + { status: 200 } + ); + } + // Create commission record const { data: commission, error: commissionError } = await supabaseAdmin🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/api/referrals/track/route.ts` around lines 101 - 115, Make commission creation idempotent in the referral tracking flow: before inserting in the commissions insert block, detect an existing commission for the same orderId and return or reuse it without calling update_referral_stats_atomic. Add a database-level unique constraint on commissions.order_id if supported, and handle duplicate-key insert errors so concurrent retries cannot create duplicate rows or double-credit statistics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@migrations/019_admin_rls_policies.sql`:
- Around line 26-30: The admin UPDATE policies for orders, products,
commissions, and the additional referenced table do not enforce their documented
column scopes. Implement database-level column restriction for each policy,
using explicit column GRANTs or BEFORE UPDATE validation comparing OLD and NEW,
so only the documented fields can change while preserving the existing admin
checks.
- Around line 20-100: Add an admin read-all RLS policy for public.users
alongside the other admin read policies, using the existing admins-can-read
pattern based on auth.uid() and users.tier = 'admin'. Ensure the policy is
safely recreated with a drop-if-exists step and does not broaden admin update
permissions.
In `@migrations/020_b2b_upgrade_requests.sql`:
- Around line 5-26: Implement self-service resubmission for rejected B2B
requests: in migrations/020_b2b_upgrade_requests.sql lines 5-26, replace the
permanent user_id uniqueness with a model permitting a new pending request after
rejection; in migrations/020_b2b_upgrade_requests.sql lines 58-62, extend the
user UPDATE policy to allow rejected rows to transition back to pending; and in
submitB2BUpgradeRequest within src/app/(dashboard)/upgrade-to-b2b/actions.ts
lines 72-99, detect rejected requests and update them with the new submission
data instead of attempting a failing INSERT.
In `@src/app/`(admin)/dashboard/page.tsx:
- Around line 49-84: Prevent stale asynchronous responses from overwriting newer
state: in src/app/(admin)/dashboard/page.tsx lines 49-84, guard
fetchDashboardData and its Refresh-triggered updates with an AbortController or
ignore flag; apply the same protection to enrichData in
src/components/admin/dashboard/ReferralLeaderboard.tsx lines 14-36 and
fetchOrders in src/app/(admin)/orders/page.tsx lines 40-80, ensuring only the
latest request updates state and stale requests do not update success or error
state.
In `@src/app/`(admin)/orders/page.tsx:
- Around line 105-119: Associate the “Status:” label with the status filter
select by assigning the select a unique id and matching it with the label’s
htmlFor attribute. Update the label and select in the status filter controls
without changing the existing filter behavior.
In `@src/app/`(admin)/products/page.tsx:
- Around line 1-42: The ProductsPage component is fully client-rendered for an
initial data fetch that can be performed server-side. Convert ProductsPage to a
server component that loads the initial page before rendering, and move only
interactive pagination state and controls into a small client component using
search params or an equivalent server-driven page selection. Preserve the
existing Product data and pagination behavior while eliminating the initial
fetchProducts useEffect and loading spinner for the first render.
- Around line 38-76: Update the useEffect and fetchProducts flow to prevent
stale overlapping pagination requests from updating state: add request
cancellation or a request-id sequencing guard tied to state.currentPage, and
ignore aborted or superseded responses and errors. Ensure only the latest
request can update products, loading, totalCount, or error state.
In `@src/app/`(dashboard)/upgrade-to-b2b/actions.ts:
- Around line 114-124: The getB2BUpgradeStatus function should derive the acting
user ID from the authenticated session via getSessionScopedSupabase and
supabase.auth.getUser(), rather than using its caller-supplied userId parameter
in the query. Update the function signature and user filter accordingly,
preserving the existing status response behavior.
- Around line 66-70: Regenerate the Supabase Database types to include
b2b_upgrade_requests, then update all queries in the upgrade request
flow—including the existingRequest query and the interactions around lines 87-99
and 120-124—to remove both the .from('b2b_upgrade_requests' as any) and
.single() as any casts. Preserve the current query behavior while relying on
generated table, column, and result types for compile-time validation.
- Around line 101-104: Sanitize the error before passing it to Sentry in the
upgrade-request insertion failure branch around insertError and createdRequest.
Replace raw insertError capture with a redacted exception that excludes
Postgrest details, hints, and Supabase request metadata such as bodies and
headers, while preserving the existing failure response.
In `@src/app/`(dashboard)/upgrade-to-b2b/page.tsx:
- Line 38: Update the success message associated with the redirect in the
upgrade flow to accurately identify the referrals destination instead of
generically saying “dashboard.” Apply the same copy correction to the
corresponding message at the other success-message location, while preserving
the existing router.push('/dashboard/referrals') behavior.
- Around line 36-38: Update the success-navigation flow in the upgrade page so
the delayed router push is managed by a useEffect keyed on success, rather than
created directly in the result handler. Return a clearTimeout cleanup from the
effect and preserve the 2-second redirect to /dashboard/referrals only when
success is true.
- Around line 10-49: Refactor UpgradeToB2BPage’s handleSubmit flow to use React
19’s useActionState for the async submitB2BUpgradeRequest action and
useFormStatus for pending state instead of manual isLoading and error state.
Preserve the existing success redirect, error messaging, and
Sentry.captureException behavior while wiring the form controls to the action
state and pending status.
- Around line 87-91: Add role="alert" or aria-live="polite" to the conditional
error banner in the upgrade page so assistive technologies announce submission
failures when the error content appears.
In `@src/app/api/admin/dashboard/route.ts`:
- Around line 69-75: Update the calculation around totalRevenue,
ordersThisMonth, and avgOrderValue so the numerator and denominator cover the
same time window. Compute revenue from the orders included in the recent-month
count, or otherwise use a matching 90-day order count, ensuring avgOrderValue is
not based on mismatched periods.
- Around line 69-131: Replace the repeated any annotations in the dashboard
metrics logic with strict interfaces or generated Supabase row types for orders,
users, items, and referrers. Apply these types to the callbacks in the
totalRevenue, ordersThisMonth, ordersLastYear, signupsThisMonth,
revenueChartMap, productMap, and referralLeaderboard calculations while
preserving the existing behavior and nullable-field handling.
- Around line 45-49: Update the top-products query in the dashboard route,
including the aggregation that produces topProducts, so it considers the
complete relevant order_items dataset rather than an unordered hard-capped
1,000-row subset. Add the intended date filter and deterministic revenue-based
ordering before applying any limit, or otherwise aggregate in the database so
ranking reflects all matching order items. Preserve the existing product revenue
ranking and response shape.
- Around line 69-81: Update the growthYoY calculation near ordersLastYear to
compare orders from the current 30-day window with orders from the equivalent
30-day period one year earlier. Since orders only contains the 90-day fetch
window, ensure the prior-year count is obtained from an appropriate data source
or query rather than filtering the existing orders array, and preserve the
zero-denominator fallback.
In `@src/app/api/admin/orders/route.ts`:
- Around line 45-53: Replace the `(order: any)` annotation in the
`transformedOrders` map with a defined interface or inferred strict type
describing the joined order row, including `id`, `user_id`, `status`, `total`,
`created_at`, optional `users.email`, and optional `order_items`. Preserve the
existing fallback behavior for missing users and order items without introducing
any `any` types.
- Around line 18-19: Validate the offset and limit values parsed in the orders
route before passing them to the range query: reject non-numeric, negative, and
out-of-bounds inputs, including excessively large limits, and retain safe
defaults for missing or invalid values. Ensure the validated values are the ones
used by .range(offset, offset + limit - 1).
In `@src/app/api/admin/products/route.ts`:
- Around line 18-19: Validate the offset and limit query parameters in the
products route before using them for pagination, handling NaN, negative offsets,
and invalid or non-positive limits with safe defaults or the established
orders-route behavior. Update the parsing logic around offset and limit while
preserving the existing pagination flow.
In `@src/app/api/admin/referrals/route.ts`:
- Around line 29-47: The referrals handler should short-circuit when
referralStats produces no referrerIds, returning the endpoint’s empty-result
response before querying users or pending commissions. Update the flow around
referralStats and referrerIds while preserving the existing response shape for
non-empty stats.
- Line 29: Replace the any annotations in the referral statistics mapping and
related callbacks with explicit row types. Define or reuse typed shapes for
referralStats, user records, payout records, and stat records, then apply them
at the callbacks around referrerIds and the additional affected mappings while
preserving the existing behavior.
In `@src/components/admin/dashboard/QuickActions.tsx`:
- Around line 19-25: Resolve the broken Audit Logs navigation in QuickActions by
either adding the missing /admin/audit route/page or changing the Audit Logs
link to the existing /admin/settings destination. Keep the Settings action
targeting /admin/settings and ensure every rendered link resolves to a valid
admin page.
In `@src/components/admin/dashboard/RevenueChart.tsx`:
- Around line 19-23: Make date formatting deterministic at both affected sites:
in RevenueChart’s formattedData mapping, add timeZone: 'UTC' to the existing
en-US date options; in src/app/(admin)/orders/page.tsx lines 167-169, update the
order.created_at toLocaleDateString call to use an explicit en-US locale and
timeZone: 'UTC'.
In `@src/components/referrals/ReferralCodeDisplay.tsx`:
- Line 21: Replace the shared copied state in ReferralCodeDisplay with separate
state variables for the referral-code and link copy actions. Update each
corresponding handler and button label to read and update only its own state, so
copying one value changes only that button’s “Copied” status.
- Around line 35-37: Guard URL construction in ReferralCodeDisplay by handling
invalid or empty baseUrl values before or around new URL(baseUrl), preventing
synchronous render failure and degrading gracefully with the component’s
existing fallback behavior. Keep valid referral URL generation and referralCode
query parameter handling unchanged.
In `@src/lib/tier-helpers.ts`:
- Around line 20-26: The user lookup in the Supabase query must retain and
handle the query error instead of silently falling back through user?.tier.
Update the flow around the users query to inspect the returned error, propagate
or log genuine database/network failures, and preserve the 'b2c' fallback only
for an absent user or tier.
- Around line 16-19: Update the supabase parameter types in getUserTier and
verifyB2BAccess from bare SupabaseClient to SupabaseClient<Database>, using the
existing Database type import so database operations remain schema-checked.
---
Outside diff comments:
In `@src/app/api/referrals/track/route.ts`:
- Around line 101-115: Make commission creation idempotent in the referral
tracking flow: before inserting in the commissions insert block, detect an
existing commission for the same orderId and return or reuse it without calling
update_referral_stats_atomic. Add a database-level unique constraint on
commissions.order_id if supported, and handle duplicate-key insert errors so
concurrent retries cannot create duplicate rows or double-credit statistics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e26c9e93-31a9-464a-8f51-071bb8779bfe
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
migrations/019_admin_rls_policies.sqlmigrations/020_b2b_upgrade_requests.sqlpackage.jsonsrc/app/(admin)/dashboard/page.tsxsrc/app/(admin)/orders/page.tsxsrc/app/(admin)/products/page.tsxsrc/app/(dashboard)/upgrade-to-b2b/actions.tssrc/app/(dashboard)/upgrade-to-b2b/page.tsxsrc/app/api/admin/dashboard/route.tssrc/app/api/admin/orders/route.tssrc/app/api/admin/products/route.tssrc/app/api/admin/referrals/route.tssrc/app/api/referrals/track-click/route.tssrc/app/api/referrals/track/route.tssrc/components/admin/dashboard/KPICard.tsxsrc/components/admin/dashboard/ProductPerformanceChart.tsxsrc/components/admin/dashboard/QuickActions.tsxsrc/components/admin/dashboard/ReferralLeaderboard.tsxsrc/components/admin/dashboard/RevenueChart.tsxsrc/components/admin/orders/OrderStatusBadge.tsxsrc/components/referrals/ReferralCodeDisplay.tsxsrc/lib/tier-helpers.ts
| -- Admin access to orders (read + status updates only) | ||
| create policy "Admins can read all orders" | ||
| on public.orders | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| create policy "Admins can update order status" | ||
| on public.orders | ||
| for update | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')) | ||
| with check (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to order items | ||
| drop policy if exists "Admins can read all order items" on public.order_items; | ||
| create policy "Admins can read all order items" | ||
| on public.order_items | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to products (read + price/inventory updates) | ||
| drop policy if exists "Admins can read all products" on public.products; | ||
| create policy "Admins can read all products" | ||
| on public.products | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| drop policy if exists "Admins can update products" on public.products; | ||
| create policy "Admins can update products" | ||
| on public.products | ||
| for update | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')) | ||
| with check (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to commissions | ||
| drop policy if exists "Admins can read all commissions" on public.commissions; | ||
| create policy "Admins can read all commissions" | ||
| on public.commissions | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| drop policy if exists "Admins can update commission status" on public.commissions; | ||
| create policy "Admins can update commission status" | ||
| on public.commissions | ||
| for update | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')) | ||
| with check (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to commission payouts | ||
| drop policy if exists "Admins can read all commission payouts" on public.commission_payouts; | ||
| create policy "Admins can read all commission payouts" | ||
| on public.commission_payouts | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| drop policy if exists "Admins can update commission payouts" on public.commission_payouts; | ||
| create policy "Admins can update commission payouts" | ||
| on public.commission_payouts | ||
| for update | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')) | ||
| with check (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to referral stats | ||
| drop policy if exists "Admins can read all referral stats" on public.referral_stats; | ||
| create policy "Admins can read all referral stats" | ||
| on public.referral_stats | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to referrals | ||
| drop policy if exists "Admins can read all referrals" on public.referrals; | ||
| create policy "Admins can read all referrals" | ||
| on public.referrals | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); | ||
|
|
||
| -- Admin access to audit logs (read-only) | ||
| drop policy if exists "Admins can read audit logs" on public.admin_audit_logs; | ||
| create policy "Admins can read audit logs" | ||
| on public.admin_audit_logs | ||
| for select | ||
| using (exists (select 1 from public.users where users.id = auth.uid() and users.tier = 'admin')); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for any pre-existing admin policy on public.users
fd -e sql . migrations | xargs rg -n -i "on public\.users" -A5Repository: Hex-Tech-Lab/hex-diva
Length of output: 2059
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== migrations that mention users policies =="
fd -e sql . migrations | xargs rg -n -i 'create policy|drop policy|on public\.users|public\.users' -A 3 -B 1
echo
echo "== migrations/010_settings_rebuild.sql relevant section =="
sed -n '1,130p' migrations/010_settings_rebuild.sql | cat -n
echo
echo "== admin dashboard user query locations =="
rg -n "public\.users|auth\.uid|signupsThisMonth|b2bUsers|b2cUsers|totalUsers|tier" src/app/api/admin/dashboard/route.ts migrations/019_admin_rls_policies.sql -A 4 -B 2Repository: Hex-Tech-Lab/hex-diva
Length of output: 49667
Add admin read access for public.users.
Migration 010 only creates admin policies for platform_settings, settings_audit, and webhook data; it leaves public.users covered by the original select/update own-row policies. The dashboard route queries supabase.from('users') for tier distribution and signup counts, so non-service-role admin sessions can return incomplete or zero user KPIs (totalUsers, b2cUsers, b2bUsers, signupsThisMonth). Add an admin all-view policy for public.users if these admin dashboard metrics are needed.
🧰 Tools
🪛 SQLFluff (4.2.2)
[error] 21-21: Do not use special characters in identifiers.
(RF05)
[error] 24-24: The 'where' keyword should always start a new line.
(LT14)
[error] 26-26: Do not use special characters in identifiers.
(RF05)
[error] 29-29: The 'where' keyword should always start a new line.
(LT14)
[error] 30-30: The 'where' keyword should always start a new line.
(LT14)
[error] 33-33: Do not use special characters in identifiers.
(RF05)
[error] 34-34: Do not use special characters in identifiers.
(RF05)
[error] 37-37: The 'where' keyword should always start a new line.
(LT14)
[error] 40-40: Do not use special characters in identifiers.
(RF05)
[error] 41-41: Do not use special characters in identifiers.
(RF05)
[error] 44-44: The 'where' keyword should always start a new line.
(LT14)
[error] 46-46: Do not use special characters in identifiers.
(RF05)
[error] 47-47: Do not use special characters in identifiers.
(RF05)
[error] 50-50: The 'where' keyword should always start a new line.
(LT14)
[error] 51-51: The 'where' keyword should always start a new line.
(LT14)
[error] 54-54: Do not use special characters in identifiers.
(RF05)
[error] 55-55: Do not use special characters in identifiers.
(RF05)
[error] 58-58: The 'where' keyword should always start a new line.
(LT14)
[error] 60-60: Do not use special characters in identifiers.
(RF05)
[error] 61-61: Do not use special characters in identifiers.
(RF05)
[error] 64-64: The 'where' keyword should always start a new line.
(LT14)
[error] 65-65: The 'where' keyword should always start a new line.
(LT14)
[error] 68-68: Do not use special characters in identifiers.
(RF05)
[error] 69-69: Do not use special characters in identifiers.
(RF05)
[error] 72-72: The 'where' keyword should always start a new line.
(LT14)
[error] 74-74: Do not use special characters in identifiers.
(RF05)
[error] 75-75: Do not use special characters in identifiers.
(RF05)
[error] 78-78: The 'where' keyword should always start a new line.
(LT14)
[error] 79-79: The 'where' keyword should always start a new line.
(LT14)
[error] 82-82: Do not use special characters in identifiers.
(RF05)
[error] 83-83: Do not use special characters in identifiers.
(RF05)
[error] 86-86: The 'where' keyword should always start a new line.
(LT14)
[error] 89-89: Do not use special characters in identifiers.
(RF05)
[error] 90-90: Do not use special characters in identifiers.
(RF05)
[error] 93-93: The 'where' keyword should always start a new line.
(LT14)
[error] 96-96: Do not use special characters in identifiers.
(RF05)
[error] 97-97: Do not use special characters in identifiers.
(RF05)
[error] 100-100: The 'where' keyword should always start a new line.
(LT14)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@migrations/019_admin_rls_policies.sql` around lines 20 - 100, Add an admin
read-all RLS policy for public.users alongside the other admin read policies,
using the existing admins-can-read pattern based on auth.uid() and users.tier =
'admin'. Ensure the policy is safely recreated with a drop-if-exists step and
does not broaden admin update permissions.
| CREATE TABLE IF NOT EXISTS public.b2b_upgrade_requests ( | ||
| id uuid primary key default gen_random_uuid(), | ||
| user_id uuid not null unique references public.users(id) on delete cascade, | ||
| business_name text not null, | ||
| -- tax_id stored as plain text (accepted tradeoff, not yet hardened): | ||
| -- this repo has no column-level encryption/redaction convention in any prior | ||
| -- migration (checked 001-018), and app-layer admin access is already gated by | ||
| -- verifyAdminAccess()/ADMIN_EMAIL_WHITELIST (src/lib/admin/auth.ts), with RLS | ||
| -- restricting reads to the owning user or an admin-tier user (see policies below). | ||
| -- Before this table holds real business tax IDs in production, revisit: | ||
| -- pgcrypto column encryption (pgp_sym_encrypt/decrypt) or moving tax_id to a | ||
| -- separate table with its own tighter RLS + audit trail. | ||
| tax_id text not null, | ||
| business_address text, | ||
| credit_check_consented boolean default false, | ||
| status text not null default 'pending' check (status in ('pending', 'approved', 'rejected')), | ||
| reviewed_by uuid references public.users(id) on delete set null, | ||
| reviewed_at timestamp with time zone, | ||
| rejection_reason text, | ||
| created_at timestamp with time zone default now(), | ||
| updated_at timestamp with time zone default now() | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Rejected users have no self-service path to reapply for B2B — schema and application logic both need to account for it. The user_id unique constraint plus a user UPDATE policy scoped to status = 'pending' permanently locks a rejected row away from the user; submitB2BUpgradeRequest doesn't detect this and instead attempts a doomed INSERT, surfacing a confusing generic error.
migrations/020_b2b_upgrade_requests.sql#L5-L26: decide the intended resubmission model — either drop/relax theuniqueconstraint (e.g., partial unique index excludingrejected, or an admin-only "reset to pending" flow) so a corrected application can be recorded.migrations/020_b2b_upgrade_requests.sql#L58-L62: if self-service resubmission is intended, extend the user UPDATE policy to also allow updates when the currentstatus = 'rejected'(transitioning back topendingwith new data).src/app/(dashboard)/upgrade-to-b2b/actions.ts#L72-L99: add an explicitrejectedbranch that either calls.update()on the existing row (once the policy above permits it) or returns a clear, actionable message instead of falling through to a failing INSERT.
📍 Affects 2 files
migrations/020_b2b_upgrade_requests.sql#L5-L26(this comment)migrations/020_b2b_upgrade_requests.sql#L58-L62src/app/(dashboard)/upgrade-to-b2b/actions.ts#L72-L99
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@migrations/020_b2b_upgrade_requests.sql` around lines 5 - 26, Implement
self-service resubmission for rejected B2B requests: in
migrations/020_b2b_upgrade_requests.sql lines 5-26, replace the permanent
user_id uniqueness with a model permitting a new pending request after
rejection; in migrations/020_b2b_upgrade_requests.sql lines 58-62, extend the
user UPDATE policy to allow rejected rows to transition back to pending; and in
submitB2BUpgradeRequest within src/app/(dashboard)/upgrade-to-b2b/actions.ts
lines 72-99, detect rejected requests and update them with the new submission
data instead of attempting a failing INSERT.
| useEffect(() => { | ||
| fetchDashboardData(); | ||
| }, []); | ||
|
|
||
| async function fetchDashboardData() { | ||
| try { | ||
| setData((prev) => ({ ...prev, loading: true, error: '' })); | ||
|
|
||
| const response = await fetch('/api/admin/dashboard', { | ||
| method: 'GET', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch dashboard data: ${response.statusText}`); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
| if (result.success) { | ||
| setData((prev) => ({ | ||
| ...prev, | ||
| kpis: result.data.kpis, | ||
| revenueData: result.data.revenueData, | ||
| topProducts: result.data.topProducts, | ||
| referralLeaderboard: result.data.referralLeaderboard, | ||
| loading: false, | ||
| })); | ||
| } else { | ||
| throw new Error(result.error || 'Unknown error'); | ||
| } | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : 'Failed to load dashboard'; | ||
| setData((prev) => ({ ...prev, error: message, loading: false })); | ||
| console.error('Dashboard error:', err); | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Missing stale-response guards on effect-triggered fetches across admin pages. All three sites fetch data inside useEffect/async handlers with no AbortController or ignore-flag, so a newer trigger (refresh click, pagination, filter change) can have its response overwritten by a slower, older in-flight request.
src/app/(admin)/dashboard/page.tsx#L49-L84: add an abort/ignore-flag aroundfetchDashboardDataso a stale response from a previous "Refresh" click can't overwrite newer state.src/components/admin/dashboard/ReferralLeaderboard.tsx#L14-L36: guardenrichDatasimilarly so a slower response doesn't clobber a newer one whendatachanges again.src/app/(admin)/orders/page.tsx#L40-L80: guardfetchOrderssimilarly so rapid pagination/filter changes can't let an older response overwrite the current page's state.
🧰 Tools
🪛 React Doctor (0.7.6)
[warning] 49-49: fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead.
Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from useEffect.
(no-fetch-in-effect)
📍 Affects 3 files
src/app/(admin)/dashboard/page.tsx#L49-L84(this comment)src/components/admin/dashboard/ReferralLeaderboard.tsx#L14-L36src/app/(admin)/orders/page.tsx#L40-L80
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(admin)/dashboard/page.tsx around lines 49 - 84, Prevent stale
asynchronous responses from overwriting newer state: in
src/app/(admin)/dashboard/page.tsx lines 49-84, guard fetchDashboardData and its
Refresh-triggered updates with an AbortController or ignore flag; apply the same
protection to enrichData in
src/components/admin/dashboard/ReferralLeaderboard.tsx lines 14-36 and
fetchOrders in src/app/(admin)/orders/page.tsx lines 40-80, ensuring only the
latest request updates state and stale requests do not update success or error
state.
Source: Linters/SAST tools
| export function RevenueChart({ data }: RevenueChartProps) { | ||
| const formattedData = data.map((item) => ({ | ||
| ...item, | ||
| date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), | ||
| })); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Timezone-dependent date formatting causes hydration mismatch / off-by-one-day risk. Both sites call toLocaleDateString without an explicit timeZone, so server and client can render different text for the same underlying date depending on runtime locale/timezone.
src/components/admin/dashboard/RevenueChart.tsx#L19-L23: pass{ month: 'short', day: 'numeric', timeZone: 'UTC' }since the sourcedateis a UTC date-only string from the dashboard API.src/app/(admin)/orders/page.tsx#L167-L169: pass an explicit locale andtimeZone(e.g.,'en-US',timeZone: 'UTC') totoLocaleDateStringfororder.created_at.
🧰 Tools
🪛 React Doctor (0.7.6)
[error] 22-22: This can cause a hydration mismatch because toLocaleDateString() formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
(no-locale-format-in-render)
📍 Affects 2 files
src/components/admin/dashboard/RevenueChart.tsx#L19-L23(this comment)src/app/(admin)/orders/page.tsx#L167-L169
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/admin/dashboard/RevenueChart.tsx` around lines 19 - 23, Make
date formatting deterministic at both affected sites: in RevenueChart’s
formattedData mapping, add timeZone: 'UTC' to the existing en-US date options;
in src/app/(admin)/orders/page.tsx lines 167-169, update the order.created_at
toLocaleDateString call to use an explicit en-US locale and timeZone: 'UTC'.
Source: Linters/SAST tools
| referralCode, | ||
| baseUrl, | ||
| }: ReferralCodeDisplayProps) { | ||
| const [copied, setCopied] = useState(false) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Shared copied state makes both buttons show "Copied" together.
Copying the code sets the same copied flag used by the link button's label, so clicking one button flips the label on both, even though only one value was actually copied.
🔧 Suggested fix
- const [copied, setCopied] = useState(false)
+ const [copiedCode, setCopiedCode] = useState(false)
+ const [copiedLink, setCopiedLink] = useState(false)Update each handler/button pair to use its own state variable.
Also applies to: 39-57, 75-98
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/referrals/ReferralCodeDisplay.tsx` at line 21, Replace the
shared copied state in ReferralCodeDisplay with separate state variables for the
referral-code and link copy actions. Update each corresponding handler and
button label to read and update only its own state, so copying one value changes
only that button’s “Copied” status.
| const referralUrl = new URL(baseUrl) | ||
| referralUrl.searchParams.set('ref', referralCode) | ||
| const fullUrl = referralUrl.toString() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Unguarded new URL(baseUrl) can crash the component render.
If baseUrl is ever empty or malformed, new URL() throws synchronously outside any try/catch, failing the whole component render rather than degrading gracefully.
🛡️ Suggested guard
- const displayCode = formatReferralCodeForDisplay(referralCode)
- const referralUrl = new URL(baseUrl)
- referralUrl.searchParams.set('ref', referralCode)
- const fullUrl = referralUrl.toString()
+ const displayCode = formatReferralCodeForDisplay(referralCode)
+ let fullUrl = ''
+ try {
+ const referralUrl = new URL(baseUrl)
+ referralUrl.searchParams.set('ref', referralCode)
+ fullUrl = referralUrl.toString()
+ } catch {
+ fullUrl = baseUrl
+ }📝 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.
| const referralUrl = new URL(baseUrl) | |
| referralUrl.searchParams.set('ref', referralCode) | |
| const fullUrl = referralUrl.toString() | |
| const displayCode = formatReferralCodeForDisplay(referralCode) | |
| let fullUrl = '' | |
| try { | |
| const referralUrl = new URL(baseUrl) | |
| referralUrl.searchParams.set('ref', referralCode) | |
| fullUrl = referralUrl.toString() | |
| } catch { | |
| fullUrl = baseUrl | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/referrals/ReferralCodeDisplay.tsx` around lines 35 - 37, Guard
URL construction in ReferralCodeDisplay by handling invalid or empty baseUrl
values before or around new URL(baseUrl), preventing synchronous render failure
and degrading gracefully with the component’s existing fallback behavior. Keep
valid referral URL generation and referralCode query parameter handling
unchanged.
| export async function getUserTier( | ||
| userId: string, | ||
| supabase: SupabaseClient | ||
| ): Promise<UserTier> { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files:\n'
git ls-files | rg '(^|/)tier-helpers\.ts$|(^|/)db\.ts$|(^|/)supabase' || true
printf '\ntier-helpers outline:\n'
ast-grep outline src/lib/tier-helpers.ts --view expanded 2>/dev/null || true
printf '\ntier-helpers relevant content:\n'
sed -n '1,170p' src/lib/tier-helpers.ts | cat -n
printf '\ndb relevant content:\n'
sed -n '1,90p' src/lib/db.ts | cat -n
printf '\nSupabase type imports/usages in ts files:\n'
rg -n "SupabaseClient<Database>|SupabaseClient\(|createClient<" src/lib src -g '*.ts' -g '*.tsx' || trueRepository: Hex-Tech-Lab/hex-diva
Length of output: 10291
🌐 Web query:
Supabase JS createClient Database generic types SupabaseClient<Database>
💡 Result:
In Supabase JS, you can provide your generated database types to the createClient function by passing them as a generic parameter, which enables full end-to-end type safety for your queries, inserts, and updates [1][2][3]. To use the Database generic type, first generate your types using the Supabase CLI [4][3]: npx supabase gen types typescript --project-id "$PROJECT_REF" > database.types.ts Then, import the generated Database type and pass it to createClient [1][2]: import { createClient } from '@supabase/supabase-js' import { Database } from './database.types' const supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLISHABLE_KEY!) The SupabaseClient class is defined with several generic parameters that handle schema and Postgrest versioning [5][6]. While typically only the first generic parameter (Database) is required for most use cases, the full signature is SupabaseClient<Database, SchemaNameOrClientOptions, SchemaName> [5][6]. If you are working with complex schema configurations or specific Postgrest versions, you may need to specify these additional parameters, though they are often inferred automatically [6][7]. Passing the Database type ensures that: -.from('table') provides autocomplete for table names. -.select('column') verifies that columns exist. - Data returned from queries is correctly typed. - Insert and update operations validate the input object shape against your database schema [3][8]. It is recommended to regenerate your types whenever you make changes to your database schema (e.g., after running migrations) to keep them in sync with your actual database structure [3].
Citations:
- 1: https://supabase.com/docs/reference/javascript/typescript-support
- 2: https://supabase.com/docs/guides/api/rest/generating-types
- 3: https://www.rapidevelopers.com/supabase-tutorial/how-to-generate-types-from-supabase-schema
- 4: https://github.com/supabase/supabase/blob/master/apps/docs/content/guides/api/rest/generating-types.mdx
- 5: https://github.com/supabase/supabase-js/blob/21e9d4b7/packages/core/supabase-js/src/index.ts
- 6: https://github.com/supabase/supabase-js/blob/21e9d4b7/packages/core/supabase-js/src/SupabaseClient.ts
- 7: fix(types): export type utils for ssr use supabase/supabase-js#1491
- 8: https://supabase.github.io/server/documents/typescript-generics.html
Type the supabase parameters as SupabaseClient<Database>.
getUserTier and verifyB2BAccess accept a bare SupabaseClient, while the shared Supabase helpers return SupabaseClient<Database>. Use SupabaseClient<Database> here so .from()/.select() remain checked against the generated DB schema and avoid implicit loose typing.
🔧 Proposed fix
+import type { Database } from '`@/lib/db`'
import type { SupabaseClient } from '`@supabase/supabase-js`'
export async function getUserTier(
userId: string,
- supabase: SupabaseClient
+ supabase: SupabaseClient<Database>
): Promise<UserTier> {📝 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.
| export async function getUserTier( | |
| userId: string, | |
| supabase: SupabaseClient | |
| ): Promise<UserTier> { | |
| import type { Database } from '`@/lib/db`' | |
| import type { SupabaseClient } from '`@supabase/supabase-js`' | |
| export async function getUserTier( | |
| userId: string, | |
| supabase: SupabaseClient<Database> | |
| ): Promise<UserTier> { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/tier-helpers.ts` around lines 16 - 19, Update the supabase parameter
types in getUserTier and verifyB2BAccess from bare SupabaseClient to
SupabaseClient<Database>, using the existing Database type import so database
operations remain schema-checked.
Source: Coding guidelines
| const { data: user } = await supabase | ||
| .from('users') | ||
| .select('tier') | ||
| .eq('id', userId) | ||
| .single() | ||
|
|
||
| return (user?.tier as UserTier) || 'b2c' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Query error is silently discarded.
Only data is destructured; a genuine DB/network error (not just "no row found") is indistinguishable from a missing user and both fall back to 'b2c', hiding real failures from logs/observability.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/tier-helpers.ts` around lines 20 - 26, The user lookup in the
Supabase query must retain and handle the query error instead of silently
falling back through user?.tier. Update the flow around the users query to
inspect the returned error, propagate or log genuine database/network failures,
and preserve the 'b2c' fallback only for an absent user or tier.
Replace raw Tailwind-styled tables, selects, badges, and alert boxes with the established @astryxdesign/core components (Table, Selector, Badge, Banner, TextArea, CheckboxInput, Card) across the new admin dashboard/orders/products pages, B2B upgrade flow, and referral code display, matching the pattern already used in settings/audit/webhook admin UI. No behavior or business logic changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/admin/dashboard/ReferralLeaderboard.tsx (2)
31-33: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winValidate the API payload before updating state.
Body.json()returnsPromise<any>, soresultandresult.databypass strict checks and malformed rows can reach the currency renderers. Guardresultasunknownfirst, validatedataasReferralEntry[], and then callslice(0, 20).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/admin/dashboard/ReferralLeaderboard.tsx` around lines 31 - 33, Update the response handling around result in ReferralLeaderboard to treat the parsed payload as unknown, validate that it represents a successful response with data conforming to ReferralEntry[], and only then call slice(0, 20) and setEnrichedData. Reject malformed payloads before they reach the currency renderers while preserving the existing valid-data behavior.Source: Coding guidelines
23-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCancel superseded enrichment requests.
useEffectstarts a new fetch wheneverdatachanges, but it has no cleanup. Add anAbortController, pass the signal tofetch, and only applysetEnrichedDatafor the latest request.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/admin/dashboard/ReferralLeaderboard.tsx` around lines 23 - 45, Update the enrichData useEffect to create an AbortController, pass its signal to the /api/admin/referrals fetch, and return cleanup that aborts the request when data changes or the component unmounts. Ensure setEnrichedData, including the fallback in the catch block, only runs for the latest non-aborted request.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`(admin)/products/page.tsx:
- Around line 162-166: Replace the nested Link and Button controls in the
product row action at src/app/(admin)/products/page.tsx#L162-L166 and the
corresponding order row action at src/app/(admin)/orders/page.tsx#L182-L185 with
a single link control that preserves the existing edit destination, label,
styling, and sizing.
---
Outside diff comments:
In `@src/components/admin/dashboard/ReferralLeaderboard.tsx`:
- Around line 31-33: Update the response handling around result in
ReferralLeaderboard to treat the parsed payload as unknown, validate that it
represents a successful response with data conforming to ReferralEntry[], and
only then call slice(0, 20) and setEnrichedData. Reject malformed payloads
before they reach the currency renderers while preserving the existing
valid-data behavior.
- Around line 23-45: Update the enrichData useEffect to create an
AbortController, pass its signal to the /api/admin/referrals fetch, and return
cleanup that aborts the request when data changes or the component unmounts.
Ensure setEnrichedData, including the fallback in the catch block, only runs for
the latest non-aborted request.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: c6a40b08-a6a8-43ea-ad8b-cc3be65e3f50
📒 Files selected for processing (6)
src/app/(admin)/orders/page.tsxsrc/app/(admin)/products/page.tsxsrc/app/(dashboard)/upgrade-to-b2b/page.tsxsrc/components/admin/dashboard/ReferralLeaderboard.tsxsrc/components/admin/orders/OrderStatusBadge.tsxsrc/components/referrals/ReferralCodeDisplay.tsx
| renderCell: (product) => ( | ||
| <Link href={`/admin/products/${product.id}`}> | ||
| <Button variant="secondary" size="sm" label="Edit" /> | ||
| </Link> | ||
| ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | sed -n '1,120p'
echo
echo "Locate page files:"
fd -a 'page\.tsx$' . | rg 'products/page\.tsx|orders/page\.tsx' || true
echo
echo "Locate Astryx/package references:"
rg -n "Astryx|astrix|asterix|Link =" --glob '*.{ts,tsx,json}' . | sed -n '1,220p' || true
echo
echo "Relevant snippets:"
for f in 'src/app/(admin)/products/page.tsx' 'src/app/(admin)/orders/page.tsx'; do
if [ -f "$f" ]; then
echo "--- $f ---"
nl -ba "$f" | sed -n '1,230p' | rg -n -C 8 "renderCell|Link|Button" || true
fi
doneRepository: Hex-Tech-Lab/hex-diva
Length of output: 12264
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
files = [
Path('src/app/(admin)/products/page.tsx'),
Path('src/app/(admin)/orders/page.tsx'),
]
for path in files:
print(f'--- {path} ---')
text = path.read_text()
lines = text.splitlines()
for i, line in enumerate(lines, start=1):
if re.search(r'<Link\s*[^>]*href=|\bButton\b|renderCell|href=\`/admin/products/\$\{product\.id\}\`|href=\`/admin/orders/\$\{order\.id\}\`', line):
start=max(1,i-8); end=min(len(lines), i+12)
for n in range(start,end+1):
print(f'{n}: {lines[n-1]}')
print()
PY
echo "--- tracked package/config files ---"
git ls-files | rg '(^package\.json$|^pnpm-lock\.yaml$|^yarn\.lock$|^bun\.lockb?$|^next\.config\.|config|deps|component)' | sed -n '1,200p'
echo "--- all Link/Button imports/usages in TS/TSX ---"
rg -n "from ['\"][^'\"]*link['\"]|from ['\"][^'\"]*button['\"]|Link<|Button<" --glob '*.{ts,tsx}' src . | sed -n '1,240p' || trueRepository: Hex-Tech-Lab/hex-diva
Length of output: 26347
🌐 Web query:
MDN HTML button element interactive content button inside link not allowed button element interactive content
💡 Result:
In HTML, it is invalid to nest a button element inside a link ( element) or a link inside a button element [1][2][3]. Both elements are classified as interactive content [4][5][6]. According to the HTML specification, interactive content cannot contain other interactive content as descendants [6][1][7]. Because an element with an href attribute and a element are both interactive, nesting one inside the other violates the HTML content model [6][2][7]. While some browsers may attempt to render such invalid HTML, it is not guaranteed to work across all platforms and can lead to unpredictable behavior, such as conflicts in event handling or accessibility issues [1][3][8]. If you need to achieve the visual effect of a button that acts as a link, or a link that acts as a button, consider the following standard approaches: 1. Style a link as a button: Use an tag and apply CSS to make it look like a button [1][9][3]. This is generally the most recommended approach for navigation [3]. 2. Style a button as a link: Use a tag and style it with CSS to look like a link [1][2]. 3. Use a form: If you must use a for navigation (e.g., to match form-based UI), you can wrap the button in a element where the action attribute points to your desired URL [1][2][9]. 4. JavaScript: Use the onclick event attribute on a to navigate using window.location.href, although this is less accessible than using a standard link [1][9][8].
Citations:
- 1: https://stackoverflow.com/questions/6393827/can-i-nest-a-button-element-inside-an-a-using-html5
- 2: https://stackoverflow.com/questions/69156684/2021-version-of-the-is-a-inside-of-a-button-officially-allowed-ques
- 3: https://stackoverflow.com/questions/35192642/links-containing-buttons-2016-edition
- 4: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button
- 5: https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Content_categories
- 6: https://html.spec.whatwg.org/multipage/form-elements.html
- 7: https://html.spec.whatwg.org/dev/form-elements.html
- 8: https://stackoverflow.com/questions/802839/button-inside-of-anchor-link-works-in-firefox-but-not-in-internet-explorer
- 9: https://stackoverflow.com/questions/3341011/making-a-button-thats-a-link-in-html
Render one interactive control per row action.
Next Link wraps Button in both cells, creating an anchor with an interactive descendant. Use one link control instead of nesting Link around Button.
src/app/(admin)/products/page.tsx#L162-L166src/app/(admin)/orders/page.tsx#L182-L185
📍 Affects 2 files
src/app/(admin)/products/page.tsx#L162-L166(this comment)src/app/(admin)/orders/page.tsx#L182-L185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/`(admin)/products/page.tsx around lines 162 - 166, Replace the nested
Link and Button controls in the product row action at
src/app/(admin)/products/page.tsx#L162-L166 and the corresponding order row
action at src/app/(admin)/orders/page.tsx#L182-L185 with a single link control
that preserves the existing edit destination, label, styling, and sizing.
019: Postgres RLS is row-level only, so the admin update policies let an admin session change any column on orders/products/commissions/payouts, not just the status/price/inventory fields the comment documents. Added column-scoping triggers that reject updates touching columns outside the documented admin-editable set. 020: user_id is unique and the update policy required status='pending', so a rejected user had no way to resubmit (can't insert a second row, can't update the rejected row). Widened the policy to allow editing a rejected row back to 'pending' (with-check still pins the result to 'pending' only, so a user can never self-approve), and added a trigger to clear stale reviewer fields on resubmission. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@migrations/020_b2b_upgrade_requests.sql`:
- Around line 62-65: Add a non-admin BEFORE UPDATE guard for
public.b2b_upgrade_requests that prevents owners from modifying reviewed_by,
reviewed_at, or rejection_reason, while permitting the existing
rejected-to-pending resubmission trigger to clear those fields. Preserve the
current “Users can update own pending or rejected requests” RLS policy and
ensure the guard does not block that trigger’s required transition.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f8442708-d538-49f2-9742-e3c996424df4
📒 Files selected for processing (2)
migrations/019_admin_rls_policies.sqlmigrations/020_b2b_upgrade_requests.sql
| create policy "Users can update own pending or rejected requests" | ||
| on public.b2b_upgrade_requests | ||
| for update | ||
| using (auth.uid() = user_id and status in ('pending', 'rejected')) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Prevent request owners from modifying admin review metadata.
RLS is row-scoped, so this policy lets an owner of a pending request set reviewed_by, reviewed_at, or rejection_reason directly. The resubmission trigger only clears those fields on rejected → pending; it does not block a later pending → pending forged update. Add a non-admin BEFORE UPDATE guard that rejects changes to reviewer/decision columns, while allowing the trigger’s required clearing during resubmission.
As per coding guidelines, migrations/**/*.sql must “preserve Row-Level Security where applicable.”
🧰 Tools
🪛 SQLFluff (4.2.2)
[error] 62-62: Do not use special characters in identifiers.
(RF05)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@migrations/020_b2b_upgrade_requests.sql` around lines 62 - 65, Add a
non-admin BEFORE UPDATE guard for public.b2b_upgrade_requests that prevents
owners from modifying reviewed_by, reviewed_at, or rejection_reason, while
permitting the existing rejected-to-pending resubmission trigger to clear those
fields. Preserve the current “Users can update own pending or rejected requests”
RLS policy and ensure the guard does not block that trigger’s required
transition.
Source: Coding guidelines
Summary
Re-extracts the salvageable parts of the closed wave-7-admin-analytics branch (PR #25) into a clean, minimal branch, dropping everything that was broken, fabricated, or out of scope.
Included:
src/lib/tier-helpers.ts— B2C/B2B pricing + access-check utilitiessrc/app/(admin)/...,src/app/api/admin/{dashboard,orders,products})migrations/019_admin_rls_policies.sqlandmigrations/020_b2b_upgrade_requests.sql— renumbered from the original 015/016 (018 was the actual last-applied migration on main, not 014 as on the old branch)Migration fixes made during extraction:
using (true)/with check (true)admin policies — replaced with theexists(select 1 from public.users where id = auth.uid() and tier = 'admin')pattern already established in migs 006/010, matching how the rest of the admin RLS migration does it.tax_id textplain-storage tradeoff via code comment: no column-encryption/redaction precedent exists anywhere else in this repo's migrations, so it's accepted for now with a note on what to revisit (pgcrypto or a separate tighter-RLS table) before real business tax IDs land in production.Deliberately dropped (broken, unscoped, or unreviewable):
quality-engine.mtschangesTest plan
pnpm exec tsc --noEmit— zero errors🤖 Generated with Claude Code
Summary by Sourcery
Introduce an admin analytics surface with dashboard, orders, and products management, along with B2B upgrade and referral tracking capabilities backed by new RLS-safe migrations.
New Features:
Bug Fixes:
Enhancements:
Build:
Deployment:
Documentation:
Summary by cubic
Extracts stable admin analytics, B2B upgrade, and referral features. Hardens admin RLS and refactors UIs to
@astryxdesign/corecomponents.New Features
/api/admin/{dashboard,orders,products,referrals}routes and paginated orders/products tables./api/referrals/tracknow uses theupdate_referral_stats_atomicRPC; errors reported to Sentry.src/lib/tier-helpers.tsfor tier checks, discounts, and display; addsrechartsfor charts.@astryxdesign/corecomponents across pages (Table, Selector, Badge, Banner, TextArea, CheckboxInput, Card).Migration
exists(select 1 ... tier = 'admin')pattern; adds column‑scoping triggers to restrict admin updates to allowed fields.b2b_upgrade_requeststable with user/admin RLS; users can resubmit rejected requests back topendingand a trigger clears stale reviewer fields on resubmit;tax_idstored as plain text for now (documented tradeoff). Migrations renumbered to 019/020.Written for commit d79b09c. Summary will update on new commits.
Summary by CodeRabbit