Files
baya-monorepo/archive/docs/flows/reviews.md
T
2026-08-02 20:01:31 +03:30

12 KiB
Raw Blame History

Flow — reviews

Last verified: 2026-08-02 against commit c841bde

Actor(s): customer (author) · admin (moderator) · public/anonymous (reader) · Status: partial Client: real · Server: real (moderation half unreachable — RBAC) Business source: product/business/11-reviews-trust-and-safety.md Integration: docs/integration/domains/reviews.md

What it does

After a visit is finished, the family rates the nurse 15, optionally writes a note and picks trait tags. The review is not public on submit — it is pre-screened, parked in pending_moderation, and only an admin decision makes it visible. The nurse's public star average is derived from published reviews only, and is recomputed from source on every moderation transition so hiding a bad review actually lowers the average.

Screens

Step Route Component / notes
1 /fa/bookings Completed-and-unreviewed booking shows a compact star-strip CTA — BookingsScreen.tsx:16,233 gates it on useReviewEligibility
2 /fa/bookings/[id] CTA «ثبت نظر» / passive «در حال بررسی» — [id]/page.tsx:52, useMyReviewForBooking fires only when completed/closed
3 /fa/bookings/[id]/review The form: RatingInput + body (2000 chars) + ReviewTagSelector; moderation notice shown before submit (review/page.tsx:169-174). Already-reviewed → a read-only state card, never a second form (:93)
4 /fa/search/nurse/[nurseId] «نظرات» tab — aggregate + infinite published list (page.tsx:288)
5 /fa/admin/reviews Moderation queue + publish/hide/reject/unpublish dialog (admin/reviews/page.tsx:61-62) — renders an error state today, see Known gaps
(none) The nurse has no screen to read their own reviews. No route consumes useNurseReviews outside step 4

API

Client seam: services/reviews/apis/index.ts:11 selects reviewsClientApi because USE_REVIEWS_MOCK = false (constants.ts:14). Shapes: docs/integration/domains/reviews.md.

Call Endpoint Notes
getReviewEligibility clientApi.ts:67 GET /bookings/{id}/review_eligibility [Authorize] BookingReviewsController.cs:33GetReviewEligibilityQuery.Handler.cs:24-33. Probed 200
getMyReviewForBooking clientApi.ts:78 GET /bookings/{id}/my_review BookingReviewsController.cs:39. Probed 200
createReview clientApi.ts:91 POST /bookings/{id}/review BookingReviewsController.cs:26SubmitReviewCommand.Handler.cs. Not probed (would mutate the demo world)
getNurseReviews clientApi.ts:54 GET /nurses/{id}/reviews anonymousNursesController.cs:21,38. Probed 200
listModerationQueue clientApi.ts:99 GET /admin/reviews/moderation_queue AdminReviewsController.cs:20 DynamicPermission. Probed HTTP 403
moderateReview clientApi.ts:113 PATCH /reviews/{id}/status ReviewsController.cs:32-33 DynamicPermission. Probed HTTP 403 (authz fires before the handler)
(no client method) GET /nurses/{id}/review_tags anonymous, NursesController.cs:44. Probed 200 — returns the code/labelFa/labelEn/count/percentage rollup. Unwired
(no client method) POST /reviews/{id}/tags ReviewsController.cs:27, plain [Authorize]. Probed with a customer token: 404 "Review not found" — authz passes, no UI calls it

Rules that must hold

Rule Value Source of truth
Rating range 15 DB CHECK CK_Reviews_Rating (HARDCODED) · business/11
One review per booking UNIQUE(booking_id) on reviews.Reviews ReviewConfig.cs (HARDCODED DB); handler pre-checks and returns 409 (SubmitReviewCommand.Handler.cs:57)
Reviewable only when finished booking status completed or closed SubmitReviewCommand.Handler.cs:54; eligibility is server-decided, the client never infers it
Low-rating alert threshold ≤ 2SupportAlertType.LowRating CONFIG key min_rating_for_support_alert; SubmitReviewCommand.Handler.cs:112-119
Not public on submit clean text → pending_moderation (a human-review flag) unless Seams:ReviewModeration:AutoApproveClean MockReviewModerationService.cs:28; the key is absent from appsettings*.json so the default false holds
Only published is public and counted hidden/rejected/pending excluded from both the list and the aggregate ReviewModerationStatus.cs:12-22; verified live — nurse 2 holds a seeded hidden 1★ and returns averageRating 0.00 / publishedCount 0
Aggregate recomputed from source, never incremented COUNT/SUM(rating) over the nurse's published reviews excluding the transitioning row, then the row's new status folded in; average rounded to 2 dp away-from-zero RecomputeNurseRating.cs:33-43verified in the handler, no += 1 path exists. Called by ModerateReviewCommand.Handler.cs:45 on every transition and by an auto-published/auto-hidden submit (SubmitReviewCommand.Handler.cs:101)
Recompute is atomic with the transition status change + aggregate + b7 search-index reindex staged on one unit of work, one CommitAsync ModerateReviewCommand.Handler.cs:42-47; RecomputeNurseRating.cs:46
Public row carries no author probed payload is {id, rating, body, tagCodes, createdAt}no author field at all, stronger than the masking REQ-026 describes live probe of GET /nurses/1/reviews
Tag codes are codes labels are i18n keys; never render a raw code review/page.tsx:123,202 fall back to the code only when the key is missing

How to test

  1. Log in as 09120000010 (customer Mohammadi) — see testing-setup.md.
  2. Open /fa/bookings. Expect: bookings 1, 2, 3, 4, 7, 8, 10 listed; only booking 8 shows the star-strip «ثبت نظر» CTA (probed: booking 8 → canReview: true; bookings 3 and 4 → canReview: false, reason: "already_reviewed").
  3. Open /fa/bookings/4. Expect: the passive CTA reads «مشاهدهٔ نظر شما», and /fa/bookings/4/review renders the read-only card — 5 stars, the Persian body, chips «وقت‌شناس» + «حرفه‌ای», status chip «منتشرشده» (published). No form.
  4. Open /fa/bookings/8/review. Expect: the form, with the blue moderation notice above it. Submit 4 stars + any body. Expect: the page flips to the state card with status pending_moderation («در انتظار بررسی»). This is the only genuinely writable review path left in the seeded world.
  5. Open /fa/search/nurse/1 → «نظرات» tab. Expect: average ۵٫۰ and ۱ review — the single published 5★. The review you just wrote in step 4 must not appear.
  6. Anonymous check (no login): curl -s --noproxy '*' http://localhost:5002/api/v1/nurses/2/reviews. Expect: averageRating 0.00, publishedCount 0 — nurse 2 carries a seeded hidden 1★ and it is correctly excluded from both the list and the aggregate. This is the from-source recompute proof.
  7. Log in as 09120000020 (super_admin) and open /fa/admin/reviews. Expect (today): an error state, not a queue — the endpoint returns 403. See Known gaps.

Seeded-world caveat: three reviews exist — booking 4 published 5★ (nurse 1), booking 3 pending_moderation 4★ (nurse 1), one hidden 1★ (nurse 2) (DemoLifecycleSeeder.Social.cs:29-85). Because moderation 403s, no review submitted during testing can ever become public — the publish half of the loop is not demonstrable end to end.

Known gaps

  • GET /admin/reviews/moderation_queue returns 403 for the seeded super_admin (09120000020) and finance (09120000021) — DynamicPermission grants on the literal role admin; no seeded account holds it. /fa/admin/reviews is therefore a dead screen. (AdminReviewsController.cs:20)
  • PATCH /reviews/{reviewId}/status returns 403 for the same reason (ReviewsController.cs:33, probed against a non-existent id so authz is provably the blocker). No review can leave pending_moderation, so the submit→publish loop is untestable and every new review is permanently invisible.
  • With AutoApproveClean absent from config and moderation 403, published is only reachable via the seeder. A demo customer's review never reaches the nurse's profile.
  • ReviewModerationStatus.Rejected is unreachable end to end: the submit path maps a banned-word ModerationDecision.Reject to Hidden, not Rejected (SubmitReviewCommand.Handler.cs:77), and the only other producer is the 403'd admin PATCH. The client still maps a rejected chip (review/page.tsx:48).
  • GET /nurses/{id}/review_tags is unwired — the "% of reviewers said X" rollup is served by the API (probed) but no client method exists in clientApi.ts; the profile builds chips from per-review tagCodes instead.
  • POST /reviews/{reviewId}/tags is unwired — no screen lets an author amend tags after submit.
  • The client hardcodes the five tag codes in services/reviews/types.ts:36 instead of reading the server's review_tags master. A newly seeded tag is invisible to the UI, and an unseeded code makes submit fail with Unknown review tag(s) (SubmitReviewCommand.Handler.cs:67).
  • The nurse cannot see their own reviews. No nurse route consumes any reviews hook — a nurse learns of a 1★ only through the notification the moderation handler dispatches (ModerateReviewCommand.Handler.cs:54), which itself cannot fire while moderation 403s.
  • Eligibility distinguishes not_found from not_owner with a 200 (GetReviewEligibilityQuery.Handler.cs:24-27), letting any authenticated caller probe whether an arbitrary booking id exists. Contradicts the repo's 404-not-403 tenancy invariant (INV-7).
  • The low-rating support alert is raised on submit (rating ≤ 2) but has no reachable triage surface — admin support-alert reads are behind the same 403.
  • Stale doc comments assert the opposite of the shipped config: constants.ts:5-12 says "Mock is primary this phase" and clientApi.ts:50 says "USE_REVIEWS_MOCK = true", while the flag is false. services/reviews/apis/mockApi.ts (14.6 KB) is now dead behind that flag.