feat(kb): one vote per user account for article ratings (toggle/switch) #120

Merged
vas2000-work merged 2 commits from feat/kb-rating-per-user-votes into main 2026-06-06 00:51:47 +02:00
Owner

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

  • New table 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.
  • POST /helpful and /not_helpful now pass the authenticated u.id and toggle. Response KbArticleFeedbackResponse gains my_vote: Option<String> (the caller current vote).
  • New GET /kb/articles/{id}/vote returns 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

  • BREAKING: repeated POSTs no longer accumulate - the staff client must adopt the toggle model + read my_vote (client PR follows).
  • Existing non-zero counts stay until an article gets its first post-deploy vote, then are overwritten by the votes-derived value. Say if you want a zeroing/backfill migration.

Needs a gate run (just check + test with the tests/ mount).

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 - New table `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. - POST /helpful and /not_helpful now pass the authenticated u.id and toggle. Response `KbArticleFeedbackResponse` gains `my_vote: Option<String>` (the caller current vote). - New `GET /kb/articles/{id}/vote` returns 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 - BREAKING: repeated POSTs no longer accumulate - the staff client must adopt the toggle model + read my_vote (client PR follows). - Existing non-zero counts stay until an article gets its first post-deploy vote, then are overwritten by the votes-derived value. Say if you want a zeroing/backfill migration. 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>
style: cargo fmt
All checks were successful
E2E (staging) / Playwright against staging (pull_request) Successful in 54s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + compile + tests (pull_request) Successful in 4m5s
Build OCI container / Build and push mokosh-api image (push) Successful in 9m46s
f2e670f491
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vas2000-work deleted branch feat/kb-rating-per-user-votes 2026-06-06 00:51:47 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!120
No description provided.