feat(kb): one vote per user account for article ratings (toggle/switch) #120
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/kb-rating-per-user-votes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
KB article ratings were a global per-article counter incremented on every click, so a user could inflate both thumbs-up and thumbs-down without limit. Make it one vote per user account, mutually exclusive, toggleable.
Change
migrations/032_kb_article_votes.sql: (tenant_id, article_id, user_id, vote CHECK helpful|not_helpful), UNIQUE(article_id, user_id), FK cascade, indexes on article_id/tenant_id. Tenant isolation at the service layer (no RLS, consistent with all post-024 tables).record_vote(tenant, article, user_id, vote)in one tx: same vote again -> DELETE (un-vote); different -> INSERT ... ON CONFLICT DO UPDATE (switch). Recompute counts from the votes table and sync the denormalized kb_articles.helpful_count/not_helpful_count so list/detail stay join-free and cannot drift past the distinct-voter count.KbArticleFeedbackResponsegainsmy_vote: Option<String>(the caller current vote).GET /kb/articles/{id}/votereturns counts + the caller my_vote (no mutation) so the client can show vote state on load.Tests
tests/knowledge_base.rs: helpful->1/my_vote=helpful; reclick->0/un-voted; switch helpful->not_helpful; a second user votes independently; total never exceeds distinct voters; GET vote returns each caller own vote.Confirm
Needs a gate run (just check + test with the tests/ mount).
KB article ratings were a global per-article counter bumped by UPDATE ... = + 1, so a single user could click thumbs-up/down repeatedly and inflate both helpful_count and not_helpful_count. Add a kb_article_votes table (UNIQUE (article_id, user_id)) and replace the global increment with a per-user, mutually exclusive, toggleable vote. record_vote runs in one transaction: resolve the tenant-scoped article (404 otherwise), read the user's existing vote, then DELETE if it equals the new vote (un-vote) or UPSERT via ON CONFLICT (article_id, user_id) DO UPDATE (insert / flip the opposite vote in place). Counts are recomputed as COUNT(*) FILTER over the votes table and written back to the denormalized kb_articles.helpful_count / not_helpful_count caches so list/detail reads stay join-free and cannot drift. The feedback response gains my_vote: Option<String> (the caller's vote after the toggle). increment_helpful / increment_not_helpful now route through record_vote and the handlers pass u.id. A new GET /kb/articles/{id}/vote returns current counts plus the caller's my_vote without mutating, so the staff KB detail page can render the active thumb on load. No RLS on the new table, matching every table added after the 024 RLS sweep (tenant isolation enforced at the service layer). Portal KB is read-only and untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>