Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/pc-listings/[id]/components/PcReportListingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function PcReportListingModal(props: Props) {
const [description, setDescription] = useState('')
const [error, setError] = useState('')

const createReport = api.pcListings.createReport.useMutation()
const createReport = api.pcListingReports.create.useMutation()
const { user } = useUser()

// Reset form when modal opens/closes
Expand All @@ -64,7 +64,7 @@ function PcReportListingModal(props: Props) {
pcListingId: props.pcListingId,
reason,
description: description.trim() || undefined,
} satisfies RouterInput['pcListings']['createReport'])
} satisfies RouterInput['pcListingReports']['create'])

// Track content flagging in analytics
if (user?.id) {
Expand Down
20 changes: 14 additions & 6 deletions src/schemas/pcListing.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod'
import { PAGINATION, CHAR_LIMITS } from '@/data/constants'
import { HumanVerificationTokenSchema } from '@/features/human-verification/shared/schema'
import { JsonValueSchema } from '@/schemas/common'
import { JsonValueSchema, SortDirectionSchema } from '@/schemas/common'
import { CreatePcListingBaseSchema } from '@/schemas/listingCreate'
import { REVIEW_RISK_FILTERS, ReviewRiskFilterSchema } from '@/schemas/submissionRisk'
import { ApprovalStatus, PcOs, ReportReason, ReportStatus } from '@orm'
Expand Down Expand Up @@ -274,6 +274,8 @@ export const UnpinPcListingCommentSchema = z.object({
})

// PC Listing Report schemas
export const PcListingReportSortField = z.enum(['createdAt', 'updatedAt', 'status', 'reason'])

export const CreatePcListingReportSchema = z.object({
pcListingId: z.string().uuid(),
reason: z.nativeEnum(ReportReason),
Expand All @@ -286,11 +288,17 @@ export const UpdatePcListingReportSchema = z.object({
reviewNotes: z.string().max(1000).optional(),
})

export const GetPcListingReportsSchema = z.object({
status: z.nativeEnum(ReportStatus).optional(),
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
})
export const GetPcListingReportsSchema = z
.object({
search: z.string().optional(),
status: z.nativeEnum(ReportStatus).optional(),
reason: z.nativeEnum(ReportReason).optional(),
sortField: PcListingReportSortField.optional(),
sortDirection: SortDirectionSchema.optional(),
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
Comment on lines +298 to +299

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce integer pagination at schema boundary.

page and limit currently accept floats. That can propagate to Prisma skip/take and fail request execution for inputs like 1.5.

Suggested fix
-    page: z.number().min(1).default(1),
-    limit: z.number().min(1).max(100).default(20),
+    page: z.coerce.number().int().min(1).default(1),
+    limit: z.coerce.number().int().min(1).max(100).default(20),
📝 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
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(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/schemas/pcListing.ts` around lines 298 - 299, The page and limit schema
entries currently allow floats; update the PC listing schema's page and limit
validators to require integers by adding the .int() constraint (or use
z.coerce.number().int() if you need to accept numeric strings) so values like
1.5 are rejected at the schema boundary; adjust the validators for the fields
named page and limit in the schema (retain the existing .min/.max/.default) so
Prisma skip/take always receive integers.

})
.optional()

// PC Listing Verification schemas
export const VerifyPcListingSchema = z.object({
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { listingsRouter } from './routers/listings'
import { listingVerificationsRouter } from './routers/listingVerifications'
import { mobileRouter } from './routers/mobile'
import { notificationsRouter } from './routers/notifications'
import { pcListingReportsRouter } from './routers/pcListingReports'
import { pcListingsRouter } from './routers/pcListings'
import { performanceScalesRouter } from './routers/performanceScales'
import { permissionLogsRouter } from './routers/permissionLogs'
Expand All @@ -47,6 +48,7 @@ export const appRouter = createTRPCRouter({
activity: activityRouter,
listings: listingsRouter,
pcListings: pcListingsRouter,
pcListingReports: pcListingReportsRouter,
apiKeys: apiKeysRouter,
devices: devicesRouter,
cpus: cpusRouter,
Expand Down
2 changes: 1 addition & 1 deletion src/server/api/routers/listings/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ import { isUserBanned } from '@/server/utils/query-builders'
import { sanitizeInput, validatePagination } from '@/server/utils/security-validation'
import { checkSpamContent } from '@/server/utils/spam-check'
import { withSavepoint } from '@/server/utils/transactions'
import { validateCustomFields } from '@/server/utils/validate-custom-fields'
import { updateListingVoteCounts } from '@/server/utils/vote-counts'
import { handleListingVoteTrustEffects } from '@/server/utils/vote-trust-effects'
import { roleIncludesRole } from '@/utils/permission-system'
import { ms } from '@/utils/time'
import { ApprovalStatus, Prisma, Role, TrustAction } from '@orm/client'
import { validateCustomFields } from './validation'

const EDIT_TIME_LIMIT_MINUTES = 60
const EDIT_TIME_LIMIT = ms.minutes(EDIT_TIME_LIMIT_MINUTES)
Expand Down
1 change: 0 additions & 1 deletion src/server/api/routers/listings/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { coreRouter } from './core'
export { commentsRouter } from './comments'
export { adminRouter } from './admin'
export { validateCustomFields } from './validation'
228 changes: 228 additions & 0 deletions src/server/api/routers/pcListingReports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { ResourceError } from '@/lib/errors'
import { TrustService } from '@/lib/trust/service'
import { DeleteReportSchema, GetReportByIdSchema } from '@/schemas/listingReport'
import {
CreatePcListingReportSchema,
GetPcListingReportsSchema,
UpdatePcListingReportSchema,
} from '@/schemas/pcListing'
import { createTRPCRouter, permissionProcedure, protectedProcedure } from '@/server/api/trpc'
import { ReportSubmissionService } from '@/server/services/report-submission.service'
import { paginate } from '@/server/utils/pagination'
import { validateEnum, sanitizeInput, validatePagination } from '@/server/utils/security-validation'
import { PERMISSIONS } from '@/utils/permission-system'
import { ApprovalStatus, ReportReason, ReportStatus, TrustAction } from '@orm'
import { type Prisma } from '@orm/client'

export const pcListingReportsRouter = createTRPCRouter({
stats: permissionProcedure(PERMISSIONS.VIEW_STATISTICS).query(async ({ ctx }) => {
const [pending, underReview, resolved, dismissed] = await Promise.all([
ctx.prisma.pcListingReport.count({ where: { status: ReportStatus.PENDING } }),
ctx.prisma.pcListingReport.count({ where: { status: ReportStatus.UNDER_REVIEW } }),
ctx.prisma.pcListingReport.count({ where: { status: ReportStatus.RESOLVED } }),
ctx.prisma.pcListingReport.count({ where: { status: ReportStatus.DISMISSED } }),
])

return {
total: pending + underReview + resolved + dismissed,
pending,
underReview,
resolved,
dismissed,
}
}),

get: permissionProcedure(PERMISSIONS.VIEW_USER_BANS)
.input(GetPcListingReportsSchema)
.query(async ({ ctx, input }) => {
const {
search,
status,
reason,
sortField = 'createdAt',
sortDirection = 'desc',
} = input ?? {}

const { page, limit } = validatePagination(input?.page, input?.limit, 50)
const sanitizedSearch = search ? sanitizeInput(search) : undefined
const offset = (page - 1) * limit

const where: Prisma.PcListingReportWhereInput = {}

if (sanitizedSearch) {
where.OR = [
{ pcListing: { game: { title: { contains: sanitizedSearch, mode: 'insensitive' } } } },
{ reportedBy: { name: { contains: sanitizedSearch, mode: 'insensitive' } } },
{ description: { contains: sanitizedSearch, mode: 'insensitive' } },
]
}

if (status) where.status = status
if (reason) where.reason = reason

const orderBy: Prisma.PcListingReportOrderByWithRelationInput = {}
if (sortField && sortDirection) orderBy[sortField] = sortDirection

const [reports, total] = await Promise.all([
ctx.prisma.pcListingReport.findMany({
where,
orderBy,
skip: offset,
take: limit,
include: {
pcListing: {
include: {
game: { select: { id: true, title: true } },
author: { select: { id: true, name: true } },
cpu: true,
gpu: true,
emulator: { select: { id: true, name: true } },
},
},
reportedBy: { select: { id: true, name: true, email: true } },
reviewedBy: { select: { id: true, name: true } },
},
}),
ctx.prisma.pcListingReport.count({ where }),
])

return {
reports,
pagination: paginate({ total: total, page, limit: limit }),
}
}),

byId: permissionProcedure(PERMISSIONS.VIEW_USER_BANS)
.input(GetReportByIdSchema)
.query(async ({ ctx, input }) => {
const report = await ctx.prisma.pcListingReport.findUnique({
where: { id: input.id },
include: {
pcListing: {
include: {
game: true,
author: { select: { id: true, name: true, email: true } },
cpu: true,
gpu: true,
emulator: true,
performance: true,
},
},
reportedBy: { select: { id: true, name: true, email: true } },
reviewedBy: { select: { id: true, name: true } },
},
})

return report || ResourceError.listingReport.notFound()
}),

create: protectedProcedure.input(CreatePcListingReportSchema).mutation(async ({ ctx, input }) => {
const { pcListingId, reason, description } = input
const userId = ctx.session.user.id

validateEnum(reason, Object.values(ReportReason), 'reason')

const reportSubmissionService = new ReportSubmissionService(ctx.prisma)

return await reportSubmissionService.createPcListingReport({
pcListingId,
reportedById: userId,
reason,
description,
})
}),

updateStatus: permissionProcedure(PERMISSIONS.MANAGE_USER_BANS)
.input(UpdatePcListingReportSchema)
.mutation(async ({ ctx, input }) => {
const { reportId, status, reviewNotes } = input
const reviewerId = ctx.session.user.id

validateEnum(status, Object.values(ReportStatus), 'status')

const report = await ctx.prisma.pcListingReport.findUnique({
where: { id: reportId },
include: { pcListing: true },
})

if (!report) {
return ResourceError.listingReport.notFound()
}

if (
status === ReportStatus.RESOLVED &&
report.pcListing?.status === ApprovalStatus.APPROVED
) {
await ctx.prisma.pcListing.update({
where: { id: report.pcListingId },
data: {
status: ApprovalStatus.REJECTED,
processedAt: new Date(),
processedByUserId: reviewerId,
processedNotes: `Rejected due to report: ${reviewNotes || 'No additional notes'}`,
},
})
}

const trustService = new TrustService(ctx.prisma)

if (status === ReportStatus.RESOLVED) {
await trustService.logAction({
userId: report.reportedById,
action: TrustAction.REPORT_CONFIRMED,
metadata: {
reportId,
pcListingId: report.pcListingId,
reviewedBy: reviewerId,
reason: report.reason,
},
})
} else if (status === ReportStatus.DISMISSED) {
await trustService.logAction({
userId: report.reportedById,
action: TrustAction.FALSE_REPORT,
metadata: {
reportId,
pcListingId: report.pcListingId,
reviewedBy: reviewerId,
reason: report.reason,
reviewNotes,
},
})
}
Comment on lines +169 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against duplicate trust effects on repeated status updates.

If the same report is set to RESOLVED/DISMISSED multiple times, trust actions are logged repeatedly because current state isn’t checked before logging.

Suggested guard
+      if (report.status === status) {
+        return ctx.prisma.pcListingReport.findUnique({ where: { id: reportId } })
+      }
📝 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
if (status === ReportStatus.RESOLVED) {
await trustService.logAction({
userId: report.reportedById,
action: TrustAction.REPORT_CONFIRMED,
metadata: {
reportId,
pcListingId: report.pcListingId,
reviewedBy: reviewerId,
reason: report.reason,
},
})
} else if (status === ReportStatus.DISMISSED) {
await trustService.logAction({
userId: report.reportedById,
action: TrustAction.FALSE_REPORT,
metadata: {
reportId,
pcListingId: report.pcListingId,
reviewedBy: reviewerId,
reason: report.reason,
reviewNotes,
},
})
}
if (report.status === status) {
return ctx.prisma.pcListingReport.findUnique({ where: { id: reportId } })
}
if (status === ReportStatus.RESOLVED) {
await trustService.logAction({
userId: report.reportedById,
action: TrustAction.REPORT_CONFIRMED,
metadata: {
reportId,
pcListingId: report.pcListingId,
reviewedBy: reviewerId,
reason: report.reason,
},
})
} else if (status === ReportStatus.DISMISSED) {
await trustService.logAction({
userId: report.reportedById,
action: TrustAction.FALSE_REPORT,
metadata: {
reportId,
pcListingId: report.pcListingId,
reviewedBy: reviewerId,
reason: report.reason,
reviewNotes,
},
})
}
🤖 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/server/api/routers/pcListingReports.ts` around lines 169 - 192, The
handler currently always calls trustService.logAction when status ===
ReportStatus.RESOLVED or DISMISSED which causes duplicate trust effects on
repeated updates; modify the code that performs logging (the block that calls
trustService.logAction) to first compare the incoming status with the report's
existing status (e.g., report.status or the fetched existingReport.status) and
only call trustService.logAction when the status is actually changing to
RESOLVED or to DISMISSED (i.e., previousStatus !== newStatus && newStatus ===
ReportStatus.RESOLVED/DISMISSED), preserving the same metadata and
reviewerId/reason fields so repeated identical updates become no-ops for trust
logging.


return ctx.prisma.pcListingReport.update({
where: { id: reportId },
data: {
status,
reviewNotes,
reviewedById: reviewerId,
reviewedAt: new Date(),
},
include: {
pcListing: {
include: {
game: { select: { title: true } },
author: { select: { name: true } },
},
},
reportedBy: { select: { name: true } },
reviewedBy: { select: { name: true } },
},
})
Comment on lines +152 to +212

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make updateStatus atomic across all writes.

pcListing update, trust logging, and pcListingReport update are separate writes. A mid-flow failure can persist partial state (e.g., listing rejected but report still not updated).

🤖 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/server/api/routers/pcListingReports.ts` around lines 152 - 212, The
multiple DB writes (prisma.pcListing.update, TrustService.logAction,
prisma.pcListingReport.update) must be executed inside a single transaction to
avoid partial state; wrap these operations in ctx.prisma.$transaction and use
the transactional client for all writes (create a transaction-scoped
TrustService with new TrustService(tx) and call tx.pcListing.update,
txTrustService.logAction, then tx.pcListingReport.update inside the transaction
callback) and return the pcListingReport result from the transaction so all
updates commit or roll back atomically.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

goal is to align pcListings with listings and then abstract and improve

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Producdevity, understood — aligning the two routers first makes sense before introducing a shared transaction abstraction. Would you like me to open a follow-up issue to track wrapping the updateStatus writes in a transaction once that alignment work is done?


🧠 Learnings used
Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.136Z
Learning: Applies to src/server/api/routers/**/*.ts : Use specialized procedures such as `protectedProcedure`, `adminProcedure`, and `permissionProcedure(...)` instead of ad hoc permission checks in routers

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.136Z
Learning: Applies to src/server/api/routers/**/*.ts : Do not put raw Prisma queries or business logic in routers

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.137Z
Learning: Routers in `src/server/api/routers/` are thin orchestration layers; they handle auth context, schema-validated input, repository/service calls, and response formatting

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.137Z
Learning: Applies to src/server/{repositories,services}/**/*.ts : Use transactions for multi-step writes that must stay consistent in Prisma operations

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.137Z
Learning: Listings and PC Listings must stay behaviorally aligned for voting, comments, moderation, trust effects, notifications, and approval flows

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.137Z
Learning: A Listing is a handheld compatibility report: game plus handheld device plus emulator; A PC Listing is a PC compatibility report: game plus PC hardware plus emulator

}),

delete: permissionProcedure(PERMISSIONS.MANAGE_USER_BANS)
.input(DeleteReportSchema)
.mutation(async ({ ctx, input }) => {
const report = await ctx.prisma.pcListingReport.findUnique({
where: { id: input.id },
})

if (!report) return ResourceError.listingReport.notFound()

return ctx.prisma.pcListingReport.delete({
where: { id: input.id },
Comment on lines +218 to +225

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid check-then-delete race in report deletion.

findUnique followed by delete can race and produce an unhandled not-found failure if another request deletes the same row between calls.

🤖 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/server/api/routers/pcListingReports.ts` around lines 218 - 225, Avoid the
check-then-delete race by removing the separate pcListingReport.findUnique call
and performing the delete atomically with ctx.prisma.pcListingReport.delete,
wrapping that call in a try/catch; if the delete throws the Prisma "not found"
error (e.g., PrismaClientKnownRequestError with the P2025 code), return
ResourceError.listingReport.notFound(), otherwise rethrow or log unexpected
errors. Locate the current usages of pcListingReport.findUnique and
pcListingReport.delete in this handler and replace the two-step flow with the
single delete + error handling logic.

})
}),
})
Comment on lines +17 to +228

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move report workflow/business logic out of the router.

This router currently owns heavy Prisma access and workflow rules (status transitions + trust side effects). It should be a thin orchestration layer delegating to repository/service methods.

As per coding guidelines, routers in src/server/api/routers/**/*.ts should avoid raw Prisma/business logic and remain thin orchestration layers.

🤖 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/server/api/routers/pcListingReports.ts` around lines 17 - 228, The router
contains heavy Prisma/workflow logic (notably in updateStatus, get, byId, and
delete) and should delegate to service/repository methods; extract business
logic and DB calls into a new PcListingReportService (or repository) with
methods like getReports(input,pagination), getById(id),
createPcListingReport(payload) (you already have ReportSubmissionService),
updateStatus(reportId, status, reviewNotes, reviewerId) which performs status
transitions, pcListing updates, and TrustService logging, and deleteReport(id);
then simplify the router handlers (stats/get/byId/create/updateStatus/delete) to
validate input/permissions, call the corresponding PcListingReportService
methods, and return results so the router contains only orchestration and not
raw Prisma queries or workflow rules.

Sources: Coding guidelines, Learnings

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

is it at least consistent with listingsReport (in terms of structure and feature parity)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find listing report router files
fd -t f 'listingReport' --exec echo {}
fd -t f 'listing-report' --exec echo {}

# Show the listing report router if it exists
rg -l 'listingReport' src/server/api/routers/ 2>/dev/null

Length of output: 840


🏁 Script executed:

#!/bin/bash
cat src/server/api/routers/listingReports.ts

Length of output: 11900



@Producdevity Yes, pcListingReports.ts is highly consistent with listingReports.ts — the core six procedures (stats, get, byId, create, updateStatus, delete) are nearly line-for-line mirrors, with the same permission gates, pagination/sanitize pattern, sort logic, and trust action flows.

Three gaps vs listingReports.ts:

Procedure listingReports pcListingReports
getUserReportStats
getUserReports ✅ (already cross-queries pcListingReport)
checkUserHasReports

Note that getUserReports in listingReports.ts already aggregates both handheld and PC reports into a unified view, so pcListingReports may intentionally omit it — but getUserReportStats and checkUserHasReports currently only cover handheld listings.

One thing pcListingReports.ts actually fixes: the delete guard in listingReports.ts is missing a return:

// listingReports.ts — bug: result is discarded, delete proceeds anyway
if (!report) ResourceError.listingReport.notFound()

// pcListingReports.ts — correct
if (!report) return ResourceError.listingReport.notFound()

Worth backporting that fix. The original refactor suggestion (extracting to a service layer) still applies equally to both routers since listingReports.ts has the same raw-Prisma pattern.


🧠 Learnings used
Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.136Z
Learning: Applies to src/server/api/routers/**/*.ts : Use specialized procedures such as `protectedProcedure`, `adminProcedure`, and `permissionProcedure(...)` instead of ad hoc permission checks in routers

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.136Z
Learning: Applies to src/server/api/routers/**/*.ts : Do not put raw Prisma queries or business logic in routers

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.137Z
Learning: Routers in `src/server/api/routers/` are thin orchestration layers; they handle auth context, schema-validated input, repository/service calls, and response formatting

Learnt from: CR
Repo: Producdevity/EmuReady PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-06-05T11:58:33.137Z
Learning: Applies to src/server/{repositories,services}/**/*.ts : Use transactions for multi-step writes that must stay consistent in Prisma operations

42 changes: 0 additions & 42 deletions src/server/api/routers/pcListings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,48 +581,6 @@ describe('pcListings trust integration', () => {
})
})

describe('createReport', () => {
it('creates a PC report and emits a moderator notification event', async () => {
const { caller, prisma } = createCaller()
prisma.pcListing.findUnique.mockResolvedValue({
id: LISTING_ID,
authorId: AUTHOR_ID,
author: { id: AUTHOR_ID },
})

const report = await caller.createReport({
pcListingId: LISTING_ID,
reason: ReportReason.SPAM,
description: ' needs review ',
})

expect(report.id).toBe('00000000-0000-4000-a000-000000000030')
expect(prisma.pcListingReport.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
pcListingId: LISTING_ID,
reportedById: USER_ID,
description: 'needs review',
}),
}),
)
expect(mockEmitNotificationEvent).toHaveBeenCalledWith({
eventType: 'report.created',
entityType: 'pcListingReport',
entityId: '00000000-0000-4000-a000-000000000030',
triggeredBy: USER_ID,
includeTriggeredBy: true,
payload: {
reportId: '00000000-0000-4000-a000-000000000030',
contentId: LISTING_ID,
contentType: 'PC Compatibility Report',
actionUrl: `/pc-listings/${LISTING_ID}`,
pcListingId: LISTING_ID,
},
})
})
})

describe('byId', () => {
it('hides review risk profiles for non-reviewers', async () => {
mockRepositoryGetByIdWithDetails.mockResolvedValueOnce({
Expand Down
Loading
Loading