Skip to content

feat: extract clean admin/B2B/referral pieces from wave-7-admin-analytics - #29

Open
TechHypeXP wants to merge 7 commits into
mainfrom
feat/admin-analytics-extracted
Open

feat: extract clean admin/B2B/referral pieces from wave-7-admin-analytics#29
TechHypeXP wants to merge 7 commits into
mainfrom
feat/admin-analytics-extracted

Conversation

@TechHypeXP

@TechHypeXP TechHypeXP commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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 utilities
  • Admin dashboard, orders, and products pages (src/app/(admin)/..., src/app/api/admin/{dashboard,orders,products})
  • B2B tier upgrade request flow (form + API route)
  • Referral click tracking + referral code display
  • migrations/019_admin_rls_policies.sql and migrations/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:

  • The B2B upgrade-requests migration originally had using (true) / with check (true) admin policies — replaced with the exists(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.
  • Documented the tax_id text plain-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):

  • Custom cart route
  • Stripe Connect integration
  • Stripe-based payouts admin surface
  • quality-engine.mts changes
  • Stray build/CLI artifacts that had leaked into the branch
  • Roster/scraper data changes (fabricated/unverified "scraped" data)

Test plan

  • pnpm exec tsc --noEmit — zero errors
  • Apply migrations 019/020 against a Supabase branch/staging and confirm RLS policies behave as expected for admin vs. non-admin users
  • Manual smoke test of admin dashboard/orders/products pages and B2B upgrade request submission

🤖 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:

  • Add admin dashboard UI and API exposing key KPIs, revenue trends, product performance, and referral leaderboard data.
  • Add admin orders and products pages with paginated tables wired to new /api/admin/orders and /api/admin/products endpoints.
  • Add a B2B upgrade request page and server actions that let authenticated users submit and track business-tier upgrade requests.
  • Add referral code display component for B2B users plus a fire-and-forget referral click-tracking endpoint.
  • Add shared tier helper utilities to centralize B2C/B2B/admin tier checks and discount calculations.

Bug Fixes:

  • Ensure referral stats updates use the update_referral_stats_atomic RPC instead of manual table updates to preserve atomicity.

Enhancements:

  • Integrate recharts-based revenue and product performance charts into the admin dashboard.
  • Enrich referral reporting with an admin-only referrals API used by a leaderboard component to show earned and pending commissions.

Build:

  • Add recharts dependency for dashboard charting components.

Deployment:

  • Add migrations defining admin RLS policies for analytics-related tables and a B2B upgrade requests table with user/admin RLS.

Documentation:

  • Document the plain-text tax_id storage tradeoff and future hardening options in the B2B upgrade request migration.

Summary by cubic

Extracts stable admin analytics, B2B upgrade, and referral features. Hardens admin RLS and refactors UIs to @astryxdesign/core components.

  • New Features

    • Admin dashboard with KPIs plus revenue/product charts; new /api/admin/{dashboard,orders,products,referrals} routes and paginated orders/products tables.
    • B2B tier upgrade request page and server action with validation and a one-pending-request rule.
    • Referral updates: click-tracking endpoint and /api/referrals/track now uses the update_referral_stats_atomic RPC; errors reported to Sentry.
    • Shared src/lib/tier-helpers.ts for tier checks, discounts, and display; adds recharts for charts.
    • UI: replaced ad‑hoc Tailwind with @astryxdesign/core components across pages (Table, Selector, Badge, Banner, TextArea, CheckboxInput, Card).
  • Migration

    • 019: Admin RLS policies for orders/products/commissions/commission_payouts/referrals using the exists(select 1 ... tier = 'admin') pattern; adds column‑scoping triggers to restrict admin updates to allowed fields.
    • 020: b2b_upgrade_requests table with user/admin RLS; users can resubmit rejected requests back to pending and a trigger clears stale reviewer fields on resubmit; tax_id stored as plain text for now (documented tradeoff). Migrations renumbered to 019/020.

Written for commit d79b09c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an admin dashboard with KPIs, revenue charting, product performance, referral leaderboard, and quick actions.
    • Added admin Orders and Products pages with pagination, filters, and refresh.
    • Added B2B upgrade request submission with status display and resubmission review-field reset.
    • Added referral click tracking plus referral code/link copy UI.
    • Introduced tier-based pricing/discount utilities.
  • Security
    • Tightened admin access with row-level security and column-scoped update enforcement.
  • Improvements
    • Updated referral stats tracking to use an atomic update approach; click tracking never blocks the caller flow.

Newmusicyy111 and others added 5 commits July 25, 2026 21:29
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>
@netlify

netlify Bot commented Jul 25, 2026

Copy link
Copy Markdown

Deploy Preview for hex-diva failed.

Name Link
🔨 Latest commit d79b09c
🔍 Latest deploy log https://app.netlify.com/projects/hex-diva/deploys/6a662419be3a6e000892fd4e

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hex-diva Error Error Jul 26, 2026 3:14pm

@sourcery-ai

sourcery-ai Bot commented Jul 25, 2026

Copy link
Copy Markdown

🧙 Sourcery is reviewing your pull request!


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds admin RLS policies, dashboard APIs and pages, a B2B upgrade-request workflow, tier utilities, referral tracking, referral sharing UI, and Recharts visualizations.

Changes

B2B upgrade workflow

Layer / File(s) Summary
Upgrade request schema, actions, tier helpers
migrations/020_b2b_upgrade_requests.sql, src/app/(dashboard)/upgrade-to-b2b/actions.ts, src/lib/tier-helpers.ts
Adds the upgrade-request table, RLS policies, authenticated submission/status actions, and tier-based pricing utilities.
Upgrade request page
src/app/(dashboard)/upgrade-to-b2b/page.tsx
Adds the consent-gated B2B upgrade form with loading, error, success, and navigation states.

Admin dashboard

Layer / File(s) Summary
Admin access policies and update scoping
migrations/019_admin_rls_policies.sql
Adds admin-tier policies and trigger-based column allowlists for administrative updates.
Admin API data flows
src/app/api/admin/*/route.ts
Adds admin-protected endpoints for dashboard metrics, paginated orders and products, and enriched referral payout data.
Admin dashboard and management UI
src/app/(admin)/**, src/components/admin/**, package.json
Adds dashboard cards and charts, referral leaderboard rendering, order/product tables, pagination, status badges, navigation actions, and the Recharts dependency.

Referral tracking

Layer / File(s) Summary
Referral tracking and sharing
src/app/api/referrals/track-click/route.ts, src/app/api/referrals/track/route.ts, src/components/referrals/ReferralCodeDisplay.tsx
Adds non-blocking click recording, atomic referral-stat updates, and copyable referral code/link presentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR’s main work: extracting admin, B2B, and referral pieces from the prior analytics branch.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/admin-analytics-extracted
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/admin-analytics-extracted

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

❤️ Share

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Extract admin analytics, B2B upgrade flow, referrals tracking, and RLS policies

✨ Enhancement 🐞 Bug fix ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add admin dashboard, orders, and products surfaces backed by new admin API routes.
• Introduce B2B upgrade request workflow with new table, RLS policies, and UI.
• Harden referral tracking/stats updates (click endpoint + atomic stats RPC).
Diagram

graph TD
  A["Admin pages"] --> B["Admin API routes"] --> C[("Supabase DB")]
  D["B2B upgrade page"] --> E["Server actions"] --> C
  F["Referral click API"] --> C
  G["SQL migrations"] --> C
  A --> H{{"Recharts"}}

  subgraph Legend
    direction LR
    _ui["UI"] ~~~ _api["API/Actions"] ~~~ _db[("Database")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Make admin pages server-rendered (RSC) with direct data fetching
  • ➕ Eliminates client-side loading states and duplicate fetch logic
  • ➕ Avoids exposing multiple admin endpoints to the browser (fewer surfaces)
  • ➖ More refactor across app routing/layout conventions
  • ➖ Harder to reuse existing client-only UI primitives/components
2. Upgrade charting to Recharts v3 now
  • ➕ Avoids landing on a deprecated major line (v2 warns it’s inactive)
  • ➕ Reduces future migration risk when chart features expand
  • ➖ Potential breaking changes and extra QA now
  • ➖ May require more UI tweaks than needed for the extraction goal
3. Return fully enriched referral leaderboard from /api/admin/dashboard
  • ➕ Removes extra /api/admin/referrals roundtrip on dashboard load
  • ➕ Keeps dashboard data consistent (single snapshot)
  • ➖ More expensive dashboard query path
  • ➖ Couples dashboard endpoint to referral payout semantics

Recommendation: The overall extraction approach (thin client pages + gated admin API routes + RLS) is reasonable for a minimal salvage. The main strategic adjustment worth considering is bumping Recharts to v3 before this becomes entrenched, since v2 is explicitly deprecated in the lockfile metadata. If review bandwidth is tight, keep v2 for now but open a follow-up ticket and pin a planned migration.

Files changed (23) +2183 / -8

Enhancement (18) +1884 / -0
page.tsxAdmin dashboard page with KPI cards, charts, and leaderboard +154/-0

Admin dashboard page with KPI cards, charts, and leaderboard

• Implements a client-side admin dashboard that fetches KPIs and chart datasets from /api/admin/dashboard. Renders KPI cards, quick actions, revenue/product charts, and a referral leaderboard with refresh and loading/error states.

src/app/(admin)/dashboard/page.tsx

page.tsxAdmin orders list with status filtering and pagination +216/-0

Admin orders list with status filtering and pagination

• Adds a client-side orders management page that queries /api/admin/orders with offset/limit and optional status filtering. Displays a table of orders with totals, item counts, status badges, and pagination controls.

src/app/(admin)/orders/page.tsx

page.tsxAdmin products list with pagination and stock status +197/-0

Admin products list with pagination and stock status

• Adds a client-side products management page that queries /api/admin/products with offset/limit. Displays inventory/pricing rows and an in-stock/out-of-stock indicator with pagination controls.

src/app/(admin)/products/page.tsx

actions.tsServer actions to submit and query B2B upgrade requests +137/-0

Server actions to submit and query B2B upgrade requests

• Implements cookie-restored, request-scoped Supabase auth for server actions without @supabase/ssr. Enforces one-pending-request-per-user behavior, validates required fields/consent, inserts b2b_upgrade_requests rows, and provides a status lookup helper.

src/app/(dashboard)/upgrade-to-b2b/actions.ts

page.tsxB2B upgrade request form UI with consent gating +187/-0

B2B upgrade request form UI with consent gating

• Adds a client page that collects business details and credit-check consent, then calls submitB2BUpgradeRequest. Shows success/error states and redirects to /dashboard/referrals after submission.

src/app/(dashboard)/upgrade-to-b2b/page.tsx

route.tsAdmin dashboard API aggregating KPIs, revenue trend, and top products +161/-0

Admin dashboard API aggregating KPIs, revenue trend, and top products

• Adds GET /api/admin/dashboard gated by verifyAdminAccess, querying orders/users/referral_stats/order_items to compute summary KPIs and chart datasets. Returns a consolidated payload for the admin dashboard UI.

src/app/api/admin/dashboard/route.ts

route.tsAdmin orders API with pagination and optional status filter +72/-0

Admin orders API with pagination and optional status filter

• Adds GET /api/admin/orders gated by verifyAdminAccess. Supports offset/limit and status filtering, joins users for email, counts order items, and returns rows plus an exact total count.

src/app/api/admin/orders/route.ts

route.tsAdmin products API with pagination +50/-0

Admin products API with pagination

• Adds GET /api/admin/products gated by verifyAdminAccess. Returns paginated product rows with an exact total count for UI pagination.

src/app/api/admin/products/route.ts

route.tsAdmin referrals API enriching leaderboard with pending payouts +83/-0

Admin referrals API enriching leaderboard with pending payouts

• Adds GET /api/admin/referrals gated by verifyAdminAccess. Joins referral_stats with user identity and computes pending payout totals from commission_payouts for a richer leaderboard dataset.

src/app/api/admin/referrals/route.ts

route.tsService-role endpoint to track referral link clicks +82/-0

Service-role endpoint to track referral link clicks

• Adds POST /api/referrals/track-click that looks up active referrals by code and inserts a referral_clicks row using the admin client. Returns success immediately (fire-and-forget) while surfacing insert failures to Sentry for observability.

src/app/api/referrals/track-click/route.ts

KPICard.tsxReusable KPI card component for dashboard summaries +31/-0

Reusable KPI card component for dashboard summaries

• Adds a small presentational component for KPI tiles, including optional trend indicators.

src/components/admin/dashboard/KPICard.tsx

ProductPerformanceChart.tsxPie chart for top product revenue using Recharts +65/-0

Pie chart for top product revenue using Recharts

• Adds a Recharts-based pie chart showing the top products by revenue, with tooltip and legend styling consistent with the admin theme.

src/components/admin/dashboard/ProductPerformanceChart.tsx

QuickActions.tsxQuick navigation actions for admin workflows +33/-0

Quick navigation actions for admin workflows

• Adds a dashboard widget with links/buttons to common admin pages (orders, products, settings, audit logs, back to store).

src/components/admin/dashboard/QuickActions.tsx

ReferralLeaderboard.tsxDashboard referral leaderboard with enrichment fetch +88/-0

Dashboard referral leaderboard with enrichment fetch

• Adds a client component that renders the leaderboard and enriches initial dashboard-provided data by fetching /api/admin/referrals for names and pending totals.

src/components/admin/dashboard/ReferralLeaderboard.tsx

RevenueChart.tsxLine chart of revenue over time using Recharts +59/-0

Line chart of revenue over time using Recharts

• Adds a Recharts line chart for the last 30 days of revenue with themed axes, tooltip, and legend.

src/components/admin/dashboard/RevenueChart.tsx

OrderStatusBadge.tsxOrder status badge styling helper for admin orders table +23/-0

Order status badge styling helper for admin orders table

• Adds a small UI helper to render order status with consistent color coding and capitalization.

src/components/admin/orders/OrderStatusBadge.tsx

ReferralCodeDisplay.tsxReferral code display with copy-to-clipboard for code and full link +111/-0

Referral code display with copy-to-clipboard for code and full link

• Adds a client component to show a user’s referral code and a fully parameterized referral URL, with copy actions and a fallback message when no code exists.

src/components/referrals/ReferralCodeDisplay.tsx

tier-helpers.tsCentralize tier detection, pricing discounts, and upgrade metadata +135/-0

Centralize tier detection, pricing discounts, and upgrade metadata

• Introduces utilities for reading a user’s tier, determining B2B eligibility, computing discounted prices, formatting tier names, and describing upgrade benefits. Intended to standardize B2B/B2C/admin logic across UI and server code.

src/lib/tier-helpers.ts

Bug fix (1) +15 / -8
route.tsUse atomic RPC for referral stats updates during referral tracking +15/-8

Use atomic RPC for referral stats updates during referral tracking

• Replaces a manual referral_stats UPDATE with an RPC call to update_referral_stats_atomic, aligning with an “atomic operations only” rule. Treats RPC failures as non-fatal since commission creation already succeeded and stats can reconcile later.

src/app/api/referrals/track/route.ts

Other (4) +284 / -0
019_admin_rls_policies.sqlAdd admin-tier RLS policies across commerce and referral tables +100/-0

Add admin-tier RLS policies across commerce and referral tables

• Creates/refreshes RLS policies enabling admin-tier users to select and update specific operational tables (orders, products, commissions, payouts, referrals, referral_stats, audit logs). Policies consistently use an EXISTS check against public.users.tier = 'admin' to align with prior migrations and app-level verifyAdminAccess gating.

migrations/019_admin_rls_policies.sql

020_b2b_upgrade_requests.sqlIntroduce b2b_upgrade_requests table with user/admin RLS policies +69/-0

Introduce b2b_upgrade_requests table with user/admin RLS policies

• Adds a new b2b_upgrade_requests table (one row per user) for B2B upgrade intake, including business metadata and review fields. Enables RLS with separate user self-access policies and admin-tier select/update policies; includes an explicit comment documenting plaintext tax_id tradeoffs and future hardening options.

migrations/020_b2b_upgrade_requests.sql

package.jsonAdd Recharts dependency for admin dashboard charts +1/-0

Add Recharts dependency for admin dashboard charts

• Adds recharts to dependencies to render revenue and product performance visualizations in the admin dashboard.

package.json

pnpm-lock.yamlLockfile updates for Recharts and transitive dependencies +114/-0

Lockfile updates for Recharts and transitive dependencies

• Adds resolved versions for recharts and its transitive packages (e.g., react-smooth, victory-vendor, lodash).

pnpm-lock.yaml

@qodo-code-review

qodo-code-review Bot commented Jul 25, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (7) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 12 rules

Grey Divider


Action required

1. Admin APIs lose auth 🐞 Bug ≡ Correctness
Description
Admin API routes verify admin via verifyAdminAccess(request) but then query using a fresh anon
Supabase client without restoring the request session, so RLS policies depending on auth.uid() will
filter everything out and return empty/incorrect admin data.
Code

src/app/api/admin/dashboard/route.ts[R16-37]

+    // Create request-scoped Supabase client
+    const supabase = getSupabase();
+
+    // Fetch KPIs (using admin access from verified context)
+    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'),
+
Relevance

⭐⭐⭐ High

Likely functional break: new anon client lacks session so auth.uid() null and RLS blocks admin
reads.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The admin routes create a new anon-key client for DB reads after verifying admin, while RLS policies
in mig 019 require auth.uid() to be an admin-tier user; without restoring cookies into that
client, auth.uid() is null and RLS will filter out rows. Other routes (e.g. /api/auth/me)
demonstrate the correct cookie-based session restoration pattern.

src/app/api/admin/dashboard/route.ts[5-37]
src/lib/db.ts[23-33]
src/lib/admin/auth.ts[77-105]
migrations/019_admin_rls_policies.sql[20-31]
src/app/api/auth/me/route.ts[11-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Admin API handlers (dashboard/orders/products/referrals) verify admin access, but create a *separate* Supabase client via `getSupabase()` and never restore the request’s `sb-access-token`/`sb-refresh-token` into that client. Since the new RLS policies require `auth.uid()` to be an admin-tier user, these queries will return empty/incorrect results.

## Issue Context
`verifyAdminAccess()` already shows the intended cookie-to-session restoration pattern, and `/api/auth/me` uses the same pattern.

## Fix Focus Areas
- src/app/api/admin/dashboard/route.ts[16-50]
- src/app/api/admin/orders/route.ts[22-43]
- src/app/api/admin/products/route.ts[21-32]
- src/app/api/admin/referrals/route.ts[16-36]

## Suggested fix
Choose one:
1) Create a shared helper like `getRequestScopedSupabase(request)` that calls `getSupabase({ auth: { autoRefreshToken:false, persistSession:false }})` and then `supabase.auth.setSession(...)` from request cookies, and use that client for all admin queries.
2) If you intend to bypass RLS for admin surfaces, switch these handlers to `getSupabaseAdmin()` *after* `verifyAdminAccess` passes.

Also consider adding/ensuring any needed admin SELECT policies (e.g., on `public.users`) if you go with (1), because these handlers read from `users` directly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Rejected B2B requests stuck 🐞 Bug ≡ Correctness
Description
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.
Code

src/app/(dashboard)/upgrade-to-b2b/actions.ts[R66-100]

+    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
+
Relevance

⭐⭐⭐ High

Clear correctness bug: rejected users will hit UNIQUE(user_id) on insert; likely fixed to allow
resubmission/update.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The migration makes user_id unique and restricts user updates to pending only; the action does not
handle the rejected state and attempts an insert for it, which will fail for any user with an
existing row.

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-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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



Remediation recommended

3. Empty referrals query 500 🐞 Bug ☼ Reliability
Description
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.
Code

src/app/api/admin/referrals/route.ts[R28-49]

+    // 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;
Relevance

⭐⭐⭐ High

Guarding empty-result DB queries to avoid 500s matches prior accepted reliability fixes for
empty/missing rows.

PR-#23

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The route derives referrerIds from referralStats and immediately uses it in .in() filters
without checking for the empty-array case.

src/app/api/admin/referrals/route.ts[19-47]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


4. Migration rerun can fail 🐞 Bug ☼ Reliability
Description
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.
Code

migrations/020_b2b_upgrade_requests.sql[R28-69]

+-- 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'));
Relevance

⭐⭐⭐ High

Team previously accepted making migrations/policies idempotent via DROP IF EXISTS + CREATE to avoid
rerun failures.

PR-#23

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The table creation is guarded, but indexes and policies are not; migration 019 demonstrates the
established drop+create policy approach used elsewhere in this repo.

migrations/020_b2b_upgrade_requests.sql[5-69]
migrations/019_admin_rls_policies.sql[10-18]
PR-#23

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


5. Dashboard KPIs miscomputed 🐞 Bug ≡ Correctness
Description
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).
Code

src/app/api/admin/dashboard/route.ts[R20-89]

+    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;
Relevance

⭐⭐ Medium

KPI window/label consistency is product/semantics; not an obvious runtime failure and no strong
precedent found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The endpoint explicitly filters orders to 90 days and users to 365 days, but then labels revenue as
all-time in the UI and computes AOV/YoY from mismatched subsets; growthYoY is computed against a
‘last year’ count derived from the same 90-day dataset.

src/app/api/admin/dashboard/route.ts[20-89]
src/app/(admin)/dashboard/page.tsx[115-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View more (6)
6. any in referrals mapping 📘 Rule violation ⚙ Maintainability
Description
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.
Code

src/app/api/admin/referrals/route.ts[R29-40]

+    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 }])
+    );
Relevance

⭐⭐ Medium

Repo often uses Supabase casts like as any; unclear team will enforce new no-any rule here.

PR-#23

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids direct any usage in changed TS/TSX. The added route maps/refines Supabase
results using any annotations.

Rule 1913764: Disallow use of the any type in TypeScript source
src/app/api/admin/referrals/route.ts[29-40]
src/app/api/admin/referrals/route.ts[53-56]
src/app/api/admin/referrals/route.ts[59-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


7. as any in Supabase calls 📘 Rule violation ⚙ Maintainability
Description
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.
Code

src/app/(dashboard)/upgrade-to-b2b/actions.ts[R66-70]

+    const { data: existingRequest } = await supabase
+      .from('b2b_upgrade_requests' as any)
+      .select('id, status')
+      .eq('user_id', user.id)
+      .single() as any
Relevance

⭐⭐ Medium

Repo uses as any in Supabase code elsewhere; no clear accepted/rejected precedent on removing it.

PR-#23

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids any direct any usage in modified TS/TSX. The added code explicitly casts
query builder usage and results to any via as any in the B2B upgrade request flow.

Rule 1913764: Disallow use of the any type in TypeScript source
src/app/(dashboard)/upgrade-to-b2b/actions.ts[66-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


8. Fire-and-forget click inserts 🐞 Bug ☼ Reliability
Description
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.
Code

src/app/api/referrals/track-click/route.ts[R36-76]

+    // 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) {
Relevance

⭐⭐ Medium

Fire-and-forget is intentional; change affects endpoint semantics/latency. No clear repo precedent
on awaiting inserts.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler explicitly does not await the insert and immediately returns a 200 response, so the
write happens after the main request path completes.

src/app/api/referrals/track-click/route.ts[36-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


9. user_id breaks camelCase 📘 Rule violation ⚙ Maintainability
Description
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.
Code

src/app/(admin)/orders/page.tsx[R9-17]

+interface Order {
+  id: string;
+  user_id: string;
+  email: string;
+  status: string;
+  total: number;
+  created_at: string;
+  item_count: number;
+}
Relevance

⭐⭐ Medium

No clear repo precedent on keeping API snake_case vs mapping to camelCase in TS interfaces.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The camelCase rule requires non-component identifiers to be camelCase without underscores. The added
admin pages and API response mapping introduce multiple snake_case identifiers.

Rule 1913776: Use camelCase for non-component identifiers in JS/TS
src/app/(admin)/orders/page.tsx[9-17]
src/app/(admin)/products/page.tsx[8-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


10. any in orders mapping 📘 Rule violation ⚙ Maintainability
Description
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.
Code

src/app/api/admin/orders/route.ts[R45-53]

+    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,
+    }));
Relevance

⭐⭐ Medium

Explicit (order: any) is common in repo; no clear precedent that team enforces no-any here.

PR-#23

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist forbids any explicit any usage. The added code uses (order: any) when constructing
the API response.

Rule 1913764: Disallow use of the any type in TypeScript source
src/app/api/admin/orders/route.ts[45-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


11. any in dashboard calculations 📘 Rule violation ⚙ Maintainability
Description
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.
Code

src/app/api/admin/dashboard/route.ts[R69-94]

+    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;
Relevance

⭐⭐ Medium

No strong precedent that team removes any in data aggregation; they’ve accepted similar patterns
before.

PR-#23

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule disallows direct any usage in changed TS/TSX. The dashboard API adds multiple
any annotations in reducers/filters/loops that process DB results.

Rule 1913764: Disallow use of the any type in TypeScript source
src/app/api/admin/dashboard/route.ts[69-90]
src/app/api/admin/dashboard/route.ts[108-118]
src/app/api/admin/dashboard/route.ts[126-131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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



Informational

12. Relative ./actions import used 📘 Rule violation ⚙ Maintainability
Description
The upgraded B2B page imports an internal module using a relative path (./actions) instead of the
required absolute @/ alias. This breaks the internal-import aliasing compliance rule.
Code

src/app/(dashboard)/upgrade-to-b2b/page.tsx[6]

+import { submitB2BUpgradeRequest } from './actions'
Relevance

⭐ Low

Prior review explicitly rejected changing same-folder relative imports to @/ alias.

PR-#9

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires internal imports not to use ./ or ../ and to use @/ instead. The added
page imports ./actions via a relative path.

Rule 1913798: Use absolute @/ alias for internal imports instead of relative paths
src/app/(dashboard)/upgrade-to-b2b/page.tsx[6-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An internal import uses a relative path (`./actions`) instead of the required `@/` alias.

## Issue Context
The project `tsconfig.json` defines `@/*` paths; internal modules should be imported via `@/` to avoid fragile relative paths.

## Fix Focus Areas
- src/app/(dashboard)/upgrade-to-b2b/page.tsx[6-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. style props in Recharts 📘 Rule violation ⚙ Maintainability
Description
The new Recharts components use inline style objects (style={{...}}, contentStyle={{...}},
labelStyle={{...}}, wrapperStyle={{...}}) instead of Tailwind utility classes. This violates the
rule disallowing inline style attributes/props.
Code

src/components/admin/dashboard/RevenueChart.tsx[R31-44]

+          <LineChart data={formattedData} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
+            <CartesianGrid strokeDasharray="3 3" stroke="rgba(100, 116, 139, 0.3)" />
+            <XAxis dataKey="date" stroke="rgb(148, 163, 184)" style={{ fontSize: '12px' }} />
+            <YAxis stroke="rgb(148, 163, 184)" style={{ fontSize: '12px' }} />
+            <Tooltip
+              contentStyle={{
+                backgroundColor: 'rgb(15, 23, 42)',
+                border: '1px solid rgb(71, 85, 105)',
+                borderRadius: '0.5rem',
+              }}
+              labelStyle={{ color: 'rgb(203, 213, 225)' }}
+              formatter={(value) => `$${typeof value === 'number' ? value.toFixed(2) : value}`}
+            />
+            <Legend wrapperStyle={{ color: 'rgb(203, 213, 225)' }} />
Relevance

⭐ Low

Prior reviews rejected inline style={...} usage in favor of Tailwind-only styling; likely same
outcome here.

PR-#9

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule forbids using style props for visual styling in JSX/TSX. The added chart
components pass multiple inline style objects to Recharts components.

Rule 1913823: Disallow inline style attributes; use Tailwind utility classes instead
src/components/admin/dashboard/RevenueChart.tsx[31-44]
src/components/admin/dashboard/ProductPerformanceChart.tsx[50-59]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Recharts components are configured with inline style objects, which violates the no-inline-styles rule.

## Issue Context
Where possible, express styling via Tailwind classes on wrapper elements. For Recharts-specific styling that only accepts style objects, consider creating a shared theme helper and ensure the project explicitly allows that exception; otherwise redesign to avoid inline styles.

## Fix Focus Areas
- src/components/admin/dashboard/RevenueChart.tsx[31-44]
- src/components/admin/dashboard/ProductPerformanceChart.tsx[50-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +66 to +70
const { data: existingRequest } = await supabase
.from('b2b_upgrade_requests' as any)
.select('id, status')
.eq('user_id', user.id)
.single() as any

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +69 to +94
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +45 to +53
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,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +29 to +40
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 }])
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +9 to +17
interface Order {
id: string;
user_id: string;
email: string;
status: string;
total: number;
created_at: string;
item_count: number;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +20 to +89
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +28 to +49
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +66 to +100
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

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

Comment on lines +28 to +69
-- 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'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

Comment on lines +36 to +76
// 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

No 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 second commissions row and re-running update_referral_stats_atomic, double-crediting the referrer's commission stats. Consider checking for an existing commission by order_id before insert, and/or adding a unique constraint on commissions.order_id at 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

📥 Commits

Reviewing files that changed from the base of the PR and between ef61d22 and b74c734.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • migrations/019_admin_rls_policies.sql
  • migrations/020_b2b_upgrade_requests.sql
  • package.json
  • src/app/(admin)/dashboard/page.tsx
  • src/app/(admin)/orders/page.tsx
  • src/app/(admin)/products/page.tsx
  • src/app/(dashboard)/upgrade-to-b2b/actions.ts
  • src/app/(dashboard)/upgrade-to-b2b/page.tsx
  • src/app/api/admin/dashboard/route.ts
  • src/app/api/admin/orders/route.ts
  • src/app/api/admin/products/route.ts
  • src/app/api/admin/referrals/route.ts
  • src/app/api/referrals/track-click/route.ts
  • src/app/api/referrals/track/route.ts
  • src/components/admin/dashboard/KPICard.tsx
  • src/components/admin/dashboard/ProductPerformanceChart.tsx
  • src/components/admin/dashboard/QuickActions.tsx
  • src/components/admin/dashboard/ReferralLeaderboard.tsx
  • src/components/admin/dashboard/RevenueChart.tsx
  • src/components/admin/orders/OrderStatusBadge.tsx
  • src/components/referrals/ReferralCodeDisplay.tsx
  • src/lib/tier-helpers.ts

Comment on lines +20 to +100
-- 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'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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" -A5

Repository: 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 2

Repository: 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.

Comment thread migrations/019_admin_rls_policies.sql
Comment on lines +5 to +26
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()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 the unique constraint (e.g., partial unique index excluding rejected, 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 current status = 'rejected' (transitioning back to pending with new data).
  • src/app/(dashboard)/upgrade-to-b2b/actions.ts#L72-L99: add an explicit rejected branch 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-L62
  • src/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.

Comment on lines +49 to +84
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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 around fetchDashboardData so a stale response from a previous "Refresh" click can't overwrite newer state.
  • src/components/admin/dashboard/ReferralLeaderboard.tsx#L14-L36: guard enrichData similarly so a slower response doesn't clobber a newer one when data changes again.
  • src/app/(admin)/orders/page.tsx#L40-L80: guard fetchOrders similarly 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-L36
  • src/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

Comment thread src/app/(admin)/orders/page.tsx Outdated
Comment on lines +19 to +23
export function RevenueChart({ data }: RevenueChartProps) {
const formattedData = data.map((item) => ({
...item,
date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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 source date is a UTC date-only string from the dashboard API.
  • src/app/(admin)/orders/page.tsx#L167-L169: pass an explicit locale and timeZone (e.g., 'en-US', timeZone: 'UTC') to toLocaleDateString for order.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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +35 to +37
const referralUrl = new URL(baseUrl)
referralUrl.searchParams.set('ref', referralCode)
const fullUrl = referralUrl.toString()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Comment thread src/lib/tier-helpers.ts
Comment on lines +16 to +19
export async function getUserTier(
userId: string,
supabase: SupabaseClient
): Promise<UserTier> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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' || true

Repository: 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:


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.

Suggested change
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

Comment thread src/lib/tier-helpers.ts
Comment on lines +20 to +26
const { data: user } = await supabase
.from('users')
.select('tier')
.eq('id', userId)
.single()

return (user?.tier as UserTier) || 'b2c'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Validate the API payload before updating state.

Body.json() returns Promise<any>, so result and result.data bypass strict checks and malformed rows can reach the currency renderers. Guard result as unknown first, validate data as ReferralEntry[], and then call slice(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 win

Cancel superseded enrichment requests.

useEffect starts a new fetch whenever data changes, but it has no cleanup. Add an AbortController, pass the signal to fetch, and only apply setEnrichedData for 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

📥 Commits

Reviewing files that changed from the base of the PR and between b74c734 and 643b060.

📒 Files selected for processing (6)
  • src/app/(admin)/orders/page.tsx
  • src/app/(admin)/products/page.tsx
  • src/app/(dashboard)/upgrade-to-b2b/page.tsx
  • src/components/admin/dashboard/ReferralLeaderboard.tsx
  • src/components/admin/orders/OrderStatusBadge.tsx
  • src/components/referrals/ReferralCodeDisplay.tsx

Comment on lines +162 to +166
renderCell: (product) => (
<Link href={`/admin/products/${product.id}`}>
<Button variant="secondary" size="sm" label="Edit" />
</Link>
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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
done

Repository: 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' || true

Repository: 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:


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-L166
  • src/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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 643b060 and d79b09c.

📒 Files selected for processing (2)
  • migrations/019_admin_rls_policies.sql
  • migrations/020_b2b_upgrade_requests.sql

Comment on lines +62 to +65
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'))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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 rejectedpending; it does not block a later pendingpending 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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants