Rating System
Customer feedback collection and solver rating display after session completion
Rating System
The rating system captures customer feedback after sessions complete and maintains solver reputation metrics. When a customer finishes a support session and completes payment, they're prompted to rate their experience on a 1-5 star scale. These ratings aggregate into each solver's profile, helping future customers make informed decisions and giving solvers feedback on their service quality.
How It Works
Rating collection happens automatically within the customer session view. After a session is marked complete and payment is confirmed, the CompletedView component displays an interactive star rating interface. The customer taps their desired rating (1-5 stars), which triggers an API call to persist the feedback.
function CompletedView({ session, token }: { session: SessionData; token: string }) {
const [selectedRating, setSelectedRating] = useState<number | null>(null);
// Check if already rated
const { data: existingRating } = trpc.ratings.getBySession.useQuery(
{ sessionId: session.id, token },
{ enabled: !!token },
);
const submitRating = trpc.ratings.submit.useMutation();
const handleRate = async (rating: number) => {
setSelectedRating(rating);
await submitRating.mutateAsync({
sessionId: session.id,
token,
rating,
});
};
// ...
}The component queries for existing ratings on mount to prevent duplicate submissions. If a rating already exists for the session, the thank-you confirmation displays immediately instead of the rating interface.
Database Schema
Ratings are stored in a dedicated table with references to both the session and solver. The one-to-one relationship between sessions and ratings is enforced by a unique constraint on session_id.
export const ratings = pgTable(
"ratings",
{
id: uuid("id").primaryKey().defaultRandom(),
sessionId: uuid("session_id")
.notNull()
.unique()
.references(() => sessions.id, { onDelete: "cascade" }),
solverId: uuid("solver_id")
.notNull()
.references(() => solvers.id, { onDelete: "cascade" }),
rating: integer("rating").notNull(),
comment: text("comment"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => [
check("rating_range", sql`${table.rating} >= 1 AND ${table.rating} <= 5`),
]
);A database-level check constraint ensures ratings stay within the valid 1-5 range, providing defense-in-depth beyond application validation.
API Reference
The ratings router exposes four procedures for managing feedback:
ratings.submit
Submits a new rating for a completed session. This is a public procedure that validates the session token to ensure only the actual customer can rate.
submit: publicProcedure
.input(z.object({
sessionId: z.string().uuid(),
token: z.string().min(1),
rating: z.number().int().min(1).max(5),
comment: z.string().max(500).optional(),
}))
.mutation(async ({ input }) => {
// Validates token, checks session is completed,
// prevents duplicates, creates rating,
// and updates solver stats
})Ratings below 3 stars are logged for potential admin review, allowing the team to follow up on negative experiences.
ratings.getBySession
Checks if a session has already been rated. Used by the UI to determine whether to show the rating form or thank-you message.
ratings.getBySolver
Retrieves paginated ratings for a specific solver. Requires authentication. Returns ratings with session metadata for context.
ratings.getSolverSummary
Returns a solver's rating summary including average rating, total count, and distribution across 1-5 stars. This is a public procedure used for displaying solver profiles.
Solver Stats Update
When a rating is submitted, the solver's aggregate statistics are automatically recalculated. The updateSolverRating helper queries all ratings for the solver and updates their stats JSONB field.
async function updateSolverRating(solverId: string): Promise<void> {
const [result] = await db
.select({
avgRating: sql<number>`avg(${ratings.rating})`,
totalRatings: sql<number>`count(*)`,
})
.from(ratings)
.where(eq(ratings.solverId, solverId));
// Update solver.stats with new averageRating and totalRatings
}The average is rounded to one decimal place for clean display.
Display Components
Two components are available for showing ratings throughout the application:
SolverRating
Full rating display with proportionally filled stars, numeric average, and review count. Supports three sizes (sm, md, lg) and optional count display.
<SolverRating rating={4.5} totalRatings={42} size="md" />SolverRatingCompact
Minimal display showing a single filled star, numeric rating, and count in parentheses. Ideal for cards and list items where space is limited.
<SolverRatingCompact rating={4.5} totalRatings={42} />Security Considerations
Several measures prevent rating abuse:
- Token validation - Only requests with valid session access tokens can submit ratings
- Completion check - Sessions must be in
completedstatus before rating - Duplicate prevention - The unique constraint on
session_idprevents multiple ratings per session - Input validation - Zod schemas enforce rating range (1-5) and comment length (500 chars max)
Testing
The rating system includes comprehensive tests covering all edge cases:
bun run test server/routers/ratings.test.ts
bun run test components/ui/solver-rating.test.tsxKey test scenarios include:
- Token validation (wrong token rejected)
- Session status check (can't rate active sessions)
- Duplicate prevention (existing rating throws CONFLICT)
- Input validation (rating out of range, comment too long)
- Solver stats update calculation