Conversation
Add documentation, helper scripts, and logs for a Prisma DB migration. Includes DATABASE_MIGRATION_COMPLETED.md and PLAYWRIGHT_TESTING_RESULTS.md documenting the migration (plan upgrade, regenerated Prisma Client, 28 migrations applied) and remaining issues (dashboard module loading error and /api/subscriptions/current 404). Adds mark-migrations.js to mark migrations as applied and verify-db-connection.js/.mjs to run quick table-count connectivity checks, plus build/dev server logs. These artifacts help verify the migration and guide next debugging steps (run the verify script or inspect the testing report for dashboard/API failures).
Introduce a full fraud detection subsystem: extend Prisma schema with enums and models (FraudEvent, CustomerRiskProfile, BlockedPhoneNumber, BlockedIP, IPActivityLog, DeviceFingerprint), add multiple /api/fraud routes (blocked-ips, blocked-phones, events, risk-profiles, stats, validate), and implement core libraries (fraud-detection.service, bd-rules, device-fingerprint, scoring, geo-ip, redis-client, index). Also add utilities and artifacts (add-admin-membership script, testing summary, seed/build logs) and fix a lucide-react HMR issue by changing SubscriptionRenewalModal to a dynamic import in dashboard-page-client.tsx. These changes enable server-side fraud checks, admin endpoints, and client stability during development.
Introduce Fraud Detection UI: new admin pages (overview, events, blocked-phones, blocked-ips, risk-profiles) and their corresponding client components. Client components implement listing, filtering, pagination and actions (block/unblock, approve) and use /api/fraud/* and /api/admin/stores endpoints; dialogs and Skeleton fallbacks are included. Update AdminSidebar to add a Fraud Detection section with navigation items and icons. Provides a complete frontend surface for monitoring and managing fraud-related data across stores.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a new fraud detection subsystem (scoring + persistence + admin UI/APIs) and adds several developer/migration utilities and status logs/docs, along with a dashboard HMR workaround for the subscription renewal modal.
Changes:
- Added a fraud detection module (
src/lib/fraud/*) plus Prisma schema models/enums to persist fraud events, blocks, and risk profiles. - Added admin fraud pages and client components under
/admin/fraud/*, and new/api/fraud/*endpoints for stats/events/blocks/risk profiles/validation. - Added several DB/migration verification scripts and committed migration/testing/build output documentation/logs.
Reviewed changes
Copilot reviewed 34 out of 38 changed files in this pull request and generated 21 comments.
Show a summary per file
| File | Description |
|---|---|
| verify-db-connection.mjs | New CLI script to verify Prisma DB connectivity (ESM variant). |
| verify-db-connection.js | New CLI script to verify Prisma DB connectivity (CommonJS variant). |
| src/lib/fraud/scoring.ts | Fraud scoring weights, thresholds, and scoring helpers. |
| src/lib/fraud/redis-client.ts | In-memory “Redis-like” rate limiter/block store. |
| src/lib/fraud/index.ts | Barrel export for the fraud module. |
| src/lib/fraud/geo-ip.ts | Free-tier GeoIP lookup with in-memory caching. |
| src/lib/fraud/fraud-detection.service.ts | Core fraud orchestration service integrating DB + signals + persistence. |
| src/lib/fraud/device-fingerprint.ts | Deterministic device fingerprint from request headers. |
| src/lib/fraud/bd-rules.ts | Bangladesh-specific heuristics (fake name/address, duplicate orders, etc.). |
| src/components/dashboard-page-client.tsx | Dashboard change to dynamically import subscription renewal modal. |
| src/components/admin/fraud/risk-profiles-client.tsx | Admin UI to browse customer risk profiles. |
| src/components/admin/fraud/fraud-events-client.tsx | Admin UI to browse/approve fraud events. |
| src/components/admin/fraud/fraud-dashboard-client.tsx | Admin fraud overview dashboard UI. |
| src/components/admin/fraud/blocked-phones-client.tsx | Admin UI to manage blocked phone numbers. |
| src/components/admin/fraud/blocked-ips-client.tsx | Admin UI to manage blocked IP addresses. |
| src/components/admin/admin-sidebar.tsx | Admin navigation updated with Fraud section links. |
| src/app/api/fraud/validate/route.ts | New endpoint to run fraud validation before order creation. |
| src/app/api/fraud/stats/route.ts | New endpoint returning fraud dashboard aggregates. |
| src/app/api/fraud/risk-profiles/route.ts | New endpoint returning paginated risk profiles. |
| src/app/api/fraud/events/route.ts | New endpoint returning paginated fraud events. |
| src/app/api/fraud/events/[id]/route.ts | New endpoint to approve a fraud event. |
| src/app/api/fraud/blocked-phones/route.ts | New endpoints to list/block/unblock phone numbers. |
| src/app/api/fraud/blocked-ips/route.ts | New endpoints to list/block/unblock IP addresses. |
| src/app/admin/fraud/risk-profiles/page.tsx | New admin page for risk profiles. |
| src/app/admin/fraud/page.tsx | New admin fraud overview page. |
| src/app/admin/fraud/events/page.tsx | New admin page for fraud events. |
| src/app/admin/fraud/blocked-phones/page.tsx | New admin page for blocked phones. |
| src/app/admin/fraud/blocked-ips/page.tsx | New admin page for blocked IPs. |
| seed-output.log | Added seed output log artifact. |
| prisma/schema.prisma | Added fraud detection relations/models/enums. |
| mark-migrations.js | Script to force-mark Prisma migrations as applied. |
| build-test.log | Added build output log artifact. |
| build-error-fresh.txt | Added build error log artifact. |
| add-admin-membership.mjs | Script to ensure super admin has org membership. |
| TESTING_SUMMARY_FINAL.md | Added dashboard/migration/testing status report. |
| PLAYWRIGHT_TESTING_RESULTS.md | Added Playwright testing report. |
| DATABASE_MIGRATION_COMPLETED.md | Added migration completion report (includes sensitive connection strings). |
| #!/usr/bin/env node | ||
| const { PrismaClient } = require('@prisma/client'); | ||
|
|
||
| const prisma = new PrismaClient(); |
There was a problem hiding this comment.
verify-db-connection.mjs is an ES module (because of the .mjs extension) but it uses require(), which will throw at runtime (require is not defined in ES module scope). Either convert this file to ESM import syntax or remove the .mjs variant and keep only the CommonJS .js script to avoid confusion.
| // ============================================================================ | ||
| // FRAUD DETECTION SYSTEM | ||
| // ============================================================================ | ||
|
|
||
| /// Risk level assigned by the fraud scoring engine | ||
| enum FraudRiskLevel { | ||
| NORMAL | ||
| SUSPICIOUS | ||
| HIGH_RISK | ||
| BLOCKED | ||
| } | ||
|
|
||
| /// Outcome of an automated fraud check on an order | ||
| enum FraudCheckResult { | ||
| PASSED | ||
| FLAGGED | ||
| BLOCKED | ||
| MANUAL_REVIEW | ||
| APPROVED | ||
| } | ||
|
|
||
| /// Reason category for blocking a phone or IP | ||
| enum BlockReason { | ||
| EXCESSIVE_ORDERS | ||
| HIGH_CANCELLATION_RATE | ||
| HIGH_RETURN_RATE | ||
| FRAUD_SCORE_EXCEEDED | ||
| MANUAL_BLOCK | ||
| MULTIPLE_ACCOUNTS | ||
| SUSPICIOUS_ACTIVITY | ||
| } | ||
|
|
||
| /// Tracks every order-level fraud check result | ||
| model FraudEvent { | ||
| id String @id @default(cuid()) | ||
| storeId String | ||
| orderId String? | ||
| phone String? | ||
| ipAddress String? | ||
| deviceFingerprint String? | ||
| fraudScore Int @default(0) | ||
| riskLevel FraudRiskLevel @default(NORMAL) | ||
| result FraudCheckResult @default(PASSED) | ||
| signals Json @default("[]") // Array of signal names that fired | ||
| details Json @default("{}") // Full scoring breakdown | ||
| resolvedBy String? // Admin user ID who resolved | ||
| resolvedAt DateTime? | ||
| resolutionNote String? | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) | ||
|
|
||
| @@index([storeId, createdAt]) | ||
| @@index([storeId, riskLevel]) | ||
| @@index([storeId, result]) | ||
| @@index([phone]) | ||
| @@index([ipAddress]) | ||
| @@index([orderId]) | ||
| @@map("fraud_events") | ||
| } | ||
|
|
||
| /// Per-store phone number risk profile | ||
| model CustomerRiskProfile { | ||
| id String @id @default(cuid()) | ||
| storeId String | ||
| phone String | ||
| totalOrders Int @default(0) | ||
| cancelledOrders Int @default(0) | ||
| returnedOrders Int @default(0) | ||
| riskScore Int @default(0) | ||
| riskLevel FraudRiskLevel @default(NORMAL) | ||
| isBlocked Boolean @default(false) | ||
| blockReason BlockReason? | ||
| blockedAt DateTime? | ||
| blockedBy String? // Admin user ID | ||
| unblockAt DateTime? // Auto-unblock time | ||
| lastOrderAt DateTime? | ||
| metadata Json @default("{}") | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) | ||
|
|
||
| @@unique([storeId, phone]) | ||
| @@index([storeId, isBlocked]) | ||
| @@index([storeId, riskLevel]) | ||
| @@index([phone]) | ||
| @@map("customer_risk_profiles") | ||
| } | ||
|
|
||
| /// Per-store blocked phone numbers (vendor-level manual blocks) | ||
| model BlockedPhoneNumber { | ||
| id String @id @default(cuid()) | ||
| storeId String | ||
| phone String | ||
| reason BlockReason @default(MANUAL_BLOCK) | ||
| note String? | ||
| blockedBy String // Admin/vendor user ID | ||
| blockedAt DateTime @default(now()) | ||
| expiresAt DateTime? // null = permanent | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) | ||
|
|
||
| @@unique([storeId, phone]) | ||
| @@index([storeId]) | ||
| @@index([phone]) | ||
| @@map("blocked_phone_numbers") | ||
| } | ||
|
|
||
| /// Per-store blocked IP addresses | ||
| model BlockedIP { | ||
| id String @id @default(cuid()) | ||
| storeId String | ||
| ipAddress String | ||
| reason BlockReason @default(MANUAL_BLOCK) | ||
| note String? | ||
| blockedBy String // Admin/vendor user ID | ||
| blockedAt DateTime @default(now()) | ||
| expiresAt DateTime? // null = permanent | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) | ||
|
|
||
| @@unique([storeId, ipAddress]) | ||
| @@index([storeId]) | ||
| @@index([ipAddress]) | ||
| @@map("blocked_ips") | ||
| } | ||
|
|
||
| /// Per-store IP activity tracking for velocity checks | ||
| model IPActivityLog { | ||
| id String @id @default(cuid()) | ||
| storeId String | ||
| ipAddress String | ||
| orderCount Int @default(0) | ||
| uniquePhoneNumbers Json @default("[]") // Array of phone strings | ||
| firstOrderAt DateTime @default(now()) | ||
| lastOrderAt DateTime @default(now()) | ||
| blockedUntil DateTime? | ||
| windowStart DateTime @default(now()) // Sliding window start | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) | ||
|
|
||
| @@unique([storeId, ipAddress]) | ||
| @@index([storeId, ipAddress]) | ||
| @@index([ipAddress, lastOrderAt]) | ||
| @@map("ip_activity_logs") | ||
| } | ||
|
|
||
| /// Device fingerprint tracking | ||
| model DeviceFingerprint { | ||
| id String @id @default(cuid()) | ||
| storeId String | ||
| fingerprint String // SHA-256 hash of IP+UA+browser+OS | ||
| ipAddress String? | ||
| userAgent String? | ||
| browser String? | ||
| os String? | ||
| uniquePhones Json @default("[]") | ||
| uniqueEmails Json @default("[]") | ||
| orderCount Int @default(0) | ||
| accountCount Int @default(0) | ||
| lastSeenAt DateTime @default(now()) | ||
| createdAt DateTime @default(now()) | ||
| updatedAt DateTime @updatedAt | ||
|
|
||
| store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) | ||
|
|
||
| @@unique([storeId, fingerprint]) | ||
| @@index([storeId]) | ||
| @@index([fingerprint]) | ||
| @@map("device_fingerprints") | ||
| } |
There was a problem hiding this comment.
The new Fraud Detection models/enums were added to schema.prisma, but there is no corresponding Prisma migration in prisma/migrations/. Since the new API routes and service write to these tables, deployments will fail at runtime unless a migration creates fraud_events, customer_risk_profiles, blocked_phone_numbers, etc. Generate and commit a migration (e.g. prisma migrate dev --name add_fraud_detection) and ensure it’s applied in all environments.
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session?.user?.id) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
| const storeId = searchParams.get("storeId"); | ||
| if (!storeId) { | ||
| return NextResponse.json( | ||
| { error: "storeId is required" }, | ||
| { status: 400 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
GET /api/fraud/events only checks authentication, but does not verify the caller is allowed to read fraud data for the requested storeId. This enables any authenticated user to enumerate fraud events across stores (IDOR). Use the shared apiHandler middleware with a permission + store access check (or requireStoreAccessCheck(storeId)) and/or ensure only super admins can access.
| export async function PATCH(request: NextRequest, context: RouteContext) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session?.user?.id) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { id } = await context.params; | ||
| const body = await request.json(); | ||
| const input = patchSchema.parse(body); | ||
|
|
||
| const fraud = FraudDetectionService.getInstance(); | ||
|
|
||
| if (input.action === "approve") { | ||
| await fraud.approveFraudEvent(id, session.user.id, input.note); | ||
| } |
There was a problem hiding this comment.
PATCH /api/fraud/events/[id] allows any authenticated user to approve events without verifying they are a super admin or have access to the event’s store. This should enforce admin permissions and verify the event belongs to a store the user can access before updating it.
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const session = await getServerSession(authOptions); | ||
| if (!session?.user?.id) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
| const storeId = searchParams.get("storeId"); | ||
| if (!storeId) { | ||
| return NextResponse.json( | ||
| { error: "storeId is required" }, | ||
| { status: 400 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
/api/fraud/blocked-ips endpoints only check authentication; they don’t verify the caller has access to the provided storeId. This allows listing/blocking/unblocking IPs for arbitrary stores (IDOR + privilege escalation). Use apiHandler permission checks and requireStoreAccessCheck(storeId) (or restrict to super admins) before mutating/returning data.
| /**/** | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| } } ); { status: 500 } { error: "Internal server error" }, return NextResponse.json( console.error("[RiskProfiles] Error:", error); } catch (error) { }); }, totalPages: Math.ceil(total / perPage), total, perPage, page, pagination: { profiles, return NextResponse.json({ ]); prisma.customerRiskProfile.count({ where }), }), take: perPage, skip: (page - 1) * perPage, orderBy: { riskScore: "desc" }, where, prisma.customerRiskProfile.findMany({ const [profiles, total] = await Promise.all([ }; ...(phone && { phone: { contains: phone } }), ...(blocked === "false" && { isBlocked: false }), ...(blocked === "true" && { isBlocked: true }), ...(riskLevel && { riskLevel }), storeId, const where: Prisma.CustomerRiskProfileWhereInput = { ); Math.max(1, parseInt(searchParams.get("perPage") || "20", 10)) 100, const perPage = Math.min( const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); const phone = searchParams.get("phone"); const blocked = searchParams.get("blocked"); const riskLevel = searchParams.get("riskLevel") as FraudRiskLevel | null; } ); { status: 400 } { error: "storeId is required" }, return NextResponse.json( if (!storeId) { const storeId = searchParams.get("storeId"); const { searchParams } = new URL(request.url); } return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session?.user?.id) { const session = await getServerSession(authOptions); try {export async function GET(request: NextRequest) {import type { FraudRiskLevel, Prisma } from "@prisma/client";import { prisma } from "@/lib/prisma";import { authOptions } from "@/lib/auth";import { getServerSession } from "next-auth/next";import { NextRequest, NextResponse } from "next/server"; */ * ───────────────────────────────── * GET /api/fraud/risk-profiles – List customer risk profiles * GET /api/fraud/risk-profiles – List customer risk profiles | ||
| * ───────────────────────────────── |
There was a problem hiding this comment.
This route file contains a large block of stray/garbled text at the top (lines 1–73) that makes the module invalid TypeScript and will break compilation. Remove the junk content so the file starts with a valid comment/import section.
| /**/** | |
| } } ); { status: 500 } { error: "Internal server error" }, return NextResponse.json( console.error("[RiskProfiles] Error:", error); } catch (error) { }); }, totalPages: Math.ceil(total / perPage), total, perPage, page, pagination: { profiles, return NextResponse.json({ ]); prisma.customerRiskProfile.count({ where }), }), take: perPage, skip: (page - 1) * perPage, orderBy: { riskScore: "desc" }, where, prisma.customerRiskProfile.findMany({ const [profiles, total] = await Promise.all([ }; ...(phone && { phone: { contains: phone } }), ...(blocked === "false" && { isBlocked: false }), ...(blocked === "true" && { isBlocked: true }), ...(riskLevel && { riskLevel }), storeId, const where: Prisma.CustomerRiskProfileWhereInput = { ); Math.max(1, parseInt(searchParams.get("perPage") || "20", 10)) 100, const perPage = Math.min( const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); const phone = searchParams.get("phone"); const blocked = searchParams.get("blocked"); const riskLevel = searchParams.get("riskLevel") as FraudRiskLevel | null; } ); { status: 400 } { error: "storeId is required" }, return NextResponse.json( if (!storeId) { const storeId = searchParams.get("storeId"); const { searchParams } = new URL(request.url); } return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session?.user?.id) { const session = await getServerSession(authOptions); try {export async function GET(request: NextRequest) {import type { FraudRiskLevel, Prisma } from "@prisma/client";import { prisma } from "@/lib/prisma";import { authOptions } from "@/lib/auth";import { getServerSession } from "next-auth/next";import { NextRequest, NextResponse } from "next/server"; */ * ───────────────────────────────── * GET /api/fraud/risk-profiles – List customer risk profiles * GET /api/fraud/risk-profiles – List customer risk profiles | |
| * ───────────────────────────────── | |
| /** | |
| * ───────────────────────────────── | |
| * GET /api/fraud/risk-profiles – List customer risk profiles | |
| * ───────────────────────────────── |
| fetch("/api/admin/stores?limit=100") | ||
| .then((r) => r.json()) | ||
| .then((data) => { | ||
| const list = data.stores || data.items || data || [] |
There was a problem hiding this comment.
The /api/admin/stores response shape is { data: stores, meta: ... }, but this code treats the entire JSON response as an array (data.stores || data.items || data || []). That will make list.map(...) throw and break the store selector. Parse data.data and update the request to use the perPage query param.
| fetch("/api/admin/stores?limit=100") | |
| .then((r) => r.json()) | |
| .then((data) => { | |
| const list = data.stores || data.items || data || [] | |
| fetch("/api/admin/stores?perPage=100") | |
| .then((r) => r.json()) | |
| .then((data) => { | |
| const list = Array.isArray(data?.data) ? data.data : [] |
| if (phones.size >= 3) signals.push("device:reused_fingerprint"); | ||
| if (existing.accountCount >= 3) signals.push("device:many_accounts"); | ||
| } else { | ||
| // Create new fingerprint record | ||
| await prisma.deviceFingerprint.create({ | ||
| data: { | ||
| storeId, | ||
| fingerprint, | ||
| ipAddress, | ||
| userAgent, | ||
| browser, | ||
| os, | ||
| uniquePhones: phone ? [phone] : [], | ||
| uniqueEmails: email ? [email] : [], | ||
| orderCount: 1, | ||
| accountCount: 1, | ||
| }, |
There was a problem hiding this comment.
device:many_accounts can never trigger as written: accountCount is set to 1 on create and never incremented on subsequent requests (the update only increments orderCount). Either increment accountCount where appropriate or remove this signal/weight to avoid dead code and misleading risk scoring.
| for (const migration of migrations) { | ||
| try { | ||
| await prisma.$executeRawUnsafe( | ||
| `INSERT INTO "_prisma_migrations" (id, checksum, finished_at, execution_time, migration_name, logs, rolled_back_at, started_at, applied_steps_count) VALUES ('${Date.now()}-${Math.random()}', '${migration}', NOW(), 0, '${migration}', 'Applied via script', NULL, NOW(), 1) ON CONFLICT DO NOTHING` | ||
| ); |
There was a problem hiding this comment.
This script uses $executeRawUnsafe with string interpolation to insert into _prisma_migrations, and it writes a fake checksum/random id. This can corrupt Prisma’s migration history (duplicates per migration_name are likely) and is unsafe if any input ever becomes dynamic. Prefer prisma migrate resolve --applied <migration> (or Prisma’s supported workflows) and avoid committing a script that bypasses Prisma’s migration integrity guarantees.
Remove a large block of malformed/garbled content in src/app/api/fraud/risk-profiles/route.ts and replace it with a placeholder header comment for the GET /api/fraud/risk-profiles endpoint. The previous implementation was non-functional; request handling logic should be reimplemented in a follow-up change. No other files were modified.
Integrate server-side fraud detection into the orders POST flow: dynamically import FraudDetectionService, estimate order total from product prices, call validateOrder, block requests with a 403 when disallowed, and log errors with a fail-open policy when the detector fails. Add a 'Fraud Detection' admin nav item and icon to the app sidebar. Remove an unused shouldBlockOrder import from the fraud service and add eslint-disable comments for @typescript-eslint/no-require-imports in mark-migrations and verify-db-connection scripts.
Swap the imported icon in src/components/app-sidebar.tsx: remove IconShieldAlert and add IconAlertTriangle from @tabler/icons-react. This updates the sidebar to use the alert-triangle icon variant.
Add dashboard fraud area: new pages for Overview, Events, Blocked Phones, Blocked IPs, and Risk Profiles under src/app/dashboard/fraud (each uses Suspense with client components and Skeleton fallbacks, and includes metadata). Update app-sidebar to include a "Fraud Detection" nav group with links for Overview, Events, Blocked Phones, Blocked IPs, and Risk Profiles (accessible to store owners). The previous admin-only /admin/fraud sidebar entry was removed in favor of the dashboard-scoped section.
Introduce store-scoped fraud UI clients and e2e coverage. Adds client components for blocked IPs, blocked phones, fraud dashboard, fraud events, and risk profiles (src/components/dashboard/fraud/*) and updates dashboard fraud pages to use these Store* clients instead of admin variants. Simplifies Playwright auth setup to rely on public routes and save a minimal storage state, and adds a comprehensive e2e test suite (e2e/fraud-detection-orders.spec.ts) with helpers to exercise order flows, fraud scoring, blocking, and dashboard visibility.
Add a Playwright headed e2e test suite (e2e/fraud-live-test.ts) that exercises fraud scenarios (legit, fake-name, rapid orders/rate-limit, high-value COD, and full UI checkout) and captures visual screenshots. Add quick DB query scripts (find-store-product.cjs, query-stores.mjs) for inspecting stores/products. Update playwright.config.ts to include a dedicated "fraud" project with slowed launch options for visual demos. Integrate fraud detection into the orders API (src/app/api/store/[slug]/orders/route.ts): call FraudDetectionService during order creation, log flagged events, block orders when detection returns disallow, and use fail-open semantics on service errors. Improve name-detection logic in bd-rules.ts to check both the full name and individual words so patterns like "Test User" are correctly flagged.
Add three Playwright scripts (fraud-test-browser.mjs, browser-only-test.mjs, browser-fraud-test-v2.mjs) for manual API and UI fraud testing and screenshots. Make order item validation tolerant of null variantId by allowing nullable variantId in the orders route. Update fraud dashboard pages to be server components that fetch the current store ID and redirect if missing, and pass storeId into client components. Refactor client components to use store-scoped API queries (include storeId in fetches), update blocked items shape (blockedAt/blockedBy), refresh lists after mutations, and adjust DELETE calls to use query params. Also fetch stats scoped to store and surface recentEvents from the stats payload.
| // ─── Product IDs from techbazar store ──────────────────────────────────────── | ||
| const CABLE_ID = "cmn2xdi47001xk8katognwckg"; // Anker Cable – price 149900 paisa (1,499 BDT) | ||
| const IPHONE_ID = "cmn2xdgch001qk8ka2zok3zwx"; // iPhone 15 – price 11900000 paisa (119,000 BDT) | ||
| const S24_ID = "cmn2xdeor001kk8ka3k5xhglh"; // Galaxy S24 – price 8999900 paisa (89,999 BDT) |
| const S24_ID = "cmn2xdeor001kk8ka3k5xhglh"; // Galaxy S24 – price 8999900 paisa (89,999 BDT) | ||
|
|
||
| // ─── Helpers ───────────────────────────────────────────────────────────────── | ||
| function fmt(paisa) { return `৳${(paisa / 100).toLocaleString("en-BD")}`; } |
Introduce a StoreSelector and make fraud dashboard components store-scoped. Replaced prop-based storeId with local state across blocked IPs, blocked phones, fraud dashboard, fraud events, and risk profiles clients; add guard clauses to avoid fetching until a store is selected, default loading to false, and render a prompt when no store is chosen. Also adjust UI layout to include the selector and conditional rendering for lists and forms.
Add phone normalization and tighten fraud workflows: introduce src/lib/fraud/phone-utils.ts (normalizePhone) and export it from the fraud index; normalize phone numbers in FraudDetectionService and storefront/checkout handlers to ensure consistent matching; persist normalized phone when creating orders and fraud events. Improve logging for fraud errors by including context (storeId/slug, error message and stack) while keeping fail-open behavior. Add a Prisma index on Order.customerPhone for faster lookups. Adjust Bangladesh fraud signal weights in scoring.ts to increase sensitivity for fake names, fake addresses and duplicate orders.
Close Prisma DeviceFingerprint model and make Elasticsearch optional. - Fixes prisma/schema.prisma by adding the missing closing brace for DeviceFingerprint. - Replaces static import of @elastic/elasticsearch with a lazy require inside a try/catch to avoid build failures when the package isn't installed. - Adds a lightweight ElasticsearchClient type alias, initializes the client with the same options, and logs success. - On import/init failure, logs an error (with install hint), sets esClient to null, and falls back to Postgres full-text search by setting configuredEngine = 'postgres'.
Prevent build-time failures when optional packages are not installed by using lazy requires and fallbacks. Key changes: - src/lib/ollama.ts: lazy-require 'ollama', add friendly error if missing and keep Ollama type fallback. - src/app/api/chat/assistant/route.ts: add a fallback Message type to avoid relying on the installed ollama package. - src/components/api-docs-viewer.tsx: add a lightweight fallback viewer and use a dynamic/eval import for swagger-ui-react to avoid build errors when it's not installed. - src/lib/rate-limit.ts, src/lib/redis-upstash.ts, src/lib/redis.ts: replace direct imports with lazy require patterns, log warnings, and fall back to mock Redis clients when @upstash/redis or ioredis are not available; add a mock Redis implementation in redis.ts. - Add build-output-current.txt (new file). These changes improve developer experience by allowing the app to run or build even if optional runtime dependencies are absent, while emitting clear warnings and actionable errors.
|
Automated review (GitHub Models): The repository contains documentation and scripts described in the pull request, indicating its changes have been merged and resolved. All main points are present and tested; only one non-blocking API endpoint remains as noted. Confidence: 0.95 Evidence:
|
| ? new Redis(config.url, { | ||
| maxRetriesPerRequest: config.maxRetriesPerRequest, | ||
| connectTimeout: config.connectTimeout, | ||
| keepAlive: config.keepAlive, | ||
| retryStrategy: (times) => { | ||
| if (times > 5) { | ||
| console.warn('[Redis] Max retries reached, using fallback'); | ||
| return null; | ||
| } | ||
| const delay = Math.min(times * 200, 2000); | ||
| return delay; | ||
| }, | ||
| }) |
| : new Redis({ | ||
| host: config.host, | ||
| port: config.port, | ||
| password: config.password, | ||
| tls: config.tls ? {} : undefined, | ||
| maxRetriesPerRequest: config.maxRetriesPerRequest, | ||
| connectTimeout: config.connectTimeout, | ||
| keepAlive: config.keepAlive, | ||
| }); |
| ? new Redis(config.url, { | ||
| maxRetriesPerRequest: 1, // Pub/sub doesn't need retries | ||
| }) |
| : new Redis({ | ||
| host: config.host, | ||
| port: config.port, | ||
| password: config.password, | ||
| tls: config.tls ? {} : undefined, | ||
| maxRetriesPerRequest: 1, | ||
| }); |
| } | ||
| const config = getRedisConfig(); const Redis = getRedisClass(); | ||
| rateLimitClient = config.url | ||
| ? new Redis(config.url) |
| : new Redis({ | ||
| host: config.host, | ||
| port: config.port, | ||
| password: config.password, | ||
| tls: config.tls ? {} : undefined, | ||
| }); |
This pull request implements a comprehensive set of fixes and improvements to the StormCom application, focusing on database migration, dashboard reliability, and developer tooling. The main achievements are a successful migration to a new Prisma plan with all schema issues resolved, the restoration of dashboard functionality (with one minor API 404 remaining), and the addition of scripts to assist with admin membership and migration management. Below are the most important changes grouped by theme:
Database Migration & Schema Fixes
DATABASE_MIGRATION_COMPLETED.md,PLAYWRIGHT_TESTING_RESULTS.md,TESTING_SUMMARY_FINAL.md) [1] [2] [3]Storemodel inprisma/schema.prisma, introducing support for fraud events, risk profiles, and related entities.Dashboard & Application Reliability
lucide-reactthrough dynamic imports. The dashboard now loads without error boundaries or module errors, with only the/api/subscriptions/currentAPI endpoint still returning 404 (non-blocking). (TESTING_SUMMARY_FINAL.md)PLAYWRIGHT_TESTING_RESULTS.md,TESTING_SUMMARY_FINAL.md) [1] [2]Developer Tooling & Scripts
add-admin-membership.mjs, a script to ensure the super admin user has OWNER or ADMIN membership in the first organization, improving admin access setup in development and staging environments.mark-migrations.js, a script to forcibly mark migrations as applied in the Prisma migration table, which is useful for restoring or synchronizing migration state after manual interventions.Testing & Verification
TESTING_SUMMARY_FINAL.md)Documentation & Status Reporting
DATABASE_MIGRATION_COMPLETED.md,PLAYWRIGHT_TESTING_RESULTS.md,TESTING_SUMMARY_FINAL.md) [1] [2] [3]