7.0 KiB
7.0 KiB
Backend Phase 14 — Reviews, ratings & patient care records — Report
Date: 2026-07-09 · Track: backend · Status: complete, gate green.
Closes the trust loop (moderated reviews + an honest, recomputed-from-source public rating + low-rating safety alerts) and the continuity-of-care loop (encrypted, patient-scoped clinical notes under strict access).
What was built
Schema (one additive migration ReviewsAndPatientCareRecords, reviews schema)
Reviews— one per completed booking.UNIQUE(booking_id)(1:1),CHECK(rating BETWEEN 1 AND 5), guardedmoderation_status(+ reason/moderator/time), soft-delete filter. Indexes: uniquebooking_id,(nurse_profile_id, moderation_status),moderation_status. MarkedIAuditable→ the SaveChanges interceptor writes an append-onlyaudit_logsdiff on creation and every moderation transition.ReviewTagsMaster— seeded vocabulary (punctual/professional/clean/kind/communicative),UNIQUE(code).ReviewTagLinks— N:N join,UNIQUE(review_id, review_tag_master_id).PatientCareRecords— nurse-authored, patient-scoped (patient_id, not booking), nullablebooking_idprovenance,body_encrypted(ciphertext, no EF converter),(patient_id, recorded_at)index, soft-delete.NurseProfilegainedSetReviewAggregates(avg, count)— the single sanctioned write for the existingAverageRating/TotalReviewscolumns (no new aggregate table).
Features (CQRS, Baya.Application/Features/)
- Reviews/:
SubmitReviewCommand,ModerateReviewCommand,AttachReviewTagsCommand,ListReviewsForNurseQuery,GetReviewModerationQueueQuery,GetTagAggregatesQuery; plus the internalRecomputeNurseRatingfrom-source helper andReviewCache(cached public aggregate + eviction). - PatientCareRecords/:
WritePatientCareRecordCommand,GetPatientHistoryQuery.
Endpoints (5 controllers)
| Verb & route | Maps to | Auth |
|---|---|---|
POST /v1/bookings/{bookingId}/review |
SubmitReview | customer (owns booking) |
POST /v1/reviews/{reviewId}/tags |
AttachReviewTags | author / moderator |
PATCH /v1/reviews/{reviewId}/status |
ModerateReview | admin / moderator |
GET /v1/nurses/{nurseProfileId}/reviews |
ListReviewsForNurse | public |
GET /v1/nurses/{nurseProfileId}/review_tags |
GetTagAggregates | public |
GET /v1/admin/reviews/moderation_queue |
GetReviewModerationQueue | admin |
POST /v1/patients/{patientId}/care_records |
WritePatientCareRecord | nurse (confirmed booking) |
GET /v1/patients/{patientId}/care_records |
GetPatientHistory | owner / nurse / admin |
Key design decisions (non-obvious)
- Recompute is exclude-and-fold, in one transaction. A fresh LINQ query can't see the tracked, uncommitted
status change, so
RecomputeNurseRatingreadsCOUNT/SUM(rating)over the nurse's published reviews excluding the transitioning review (id0for a brand-new one), then folds in that review's new status in memory. Correct-from-source and single-commit — no+delta/-delta, no stale re-query, no second commit. - Care records encrypt manually (no EF value converter). Unlike the b3/b9 converter-backed columns,
body_encryptedstores ciphertext directly; the handlerEncrypts on write andDecrypts only after the access check passes — so no query path (list, projection) can ever surface plaintext.recorded_atisDateTime(UTC), notDateTimeOffset, so the SQLite test provider canORDER BYit. - AI verdict → initial disposition.
SubmitReviewcallsIReviewModerationService.ScreenAsync; the verdict maps to the initial status (Approve→published,Reject→hidden, else pending). The mock defaults clean text to a human-review Flag so the publish gate holds by default (reviews landpending_moderation);Seams:ReviewModeration:AutoApproveCleanopts into auto-publish. HumanModerateReviewalways overrides.
What is now testable and exactly how (the phase §7 steps)
Run the API against SQL Server; use Swagger/curl.
- Submit on a completed booking → accepted.
POST bookings/{completedId}/review {rating:5}as the owner →200,moderationStatus: pending_moderation. It does not appear inGET nurses/{id}/reviews(publish gate). - Submit on a cancelled booking → rejected; a second submit on an already-reviewed booking → conflict (1:1).
- Moderate publish recomputes up.
PATCH reviews/{id}/status {action:"publish"}→ the review appears in the public list; the aggregatepublishedCountincrements andaverageRatingreflects it. - Moderate hide recomputes down. Publish a 5★ and a 1★, then
hidethe 1★ → the public list drops it and the aggregate rises / count decrements (re-derived from source). - Low rating raises an alert.
POST … {rating:1}→ alow_ratingsupport_alertsrow exists (visible only on the admin/internal path — never in a user response; its id shows on the moderation queue row). - Write + read a care record. As a nurse with a confirmed booking,
POST patients/{P}/care_records→200; the stored column is ciphertext. As the owning customer or that nurse,GET …→ decrypted note, newest first. - Unauthorized nurse denied. A nurse with no confirmed booking for P →
403on both write and read.
Automated coverage: 13 Foundation handler tests (Baya.Test.Foundation/Reviews/) cover steps 1–7 incl. the
recompute-from-source math (hide lowers count and average) and ciphertext-at-rest; 4 API tests
(Baya.Test.Api/ReviewsApiTests.cs) cover the HTTP pipeline (public reads anonymous, admin/patient reads 401
without a token). Full suite: 346 pass, build clean (0 new warnings).
What is mocked / waiting on a real service
IReviewModerationService(introduced here) —MockReviewModerationService(keyword filter / pass-through), configSeams:ReviewModeration:{AutoApproveClean,BannedWords}. Make it real → a text classifier / LLM endpoint, registration-only swap (see reports/mocks-registry.md, row → 🟡).ModerateReviewCommandkeeps decision authority.- Reused seams:
IFieldEncryptor(clinical notes),ISearchIndexMaintainer(aggregate refresh),ISupportAlertService(low-rating alert),INotificationDispatcher(review-outcome notice),IPlatformConfig(min_rating_for_support_alert),ICacheService(aggregate cache).
Contracts produced
dev/contracts/domains/reviews-records.md(8 endpoints, enums, DTOs, care-record access matrix).dev/contracts/openapi/swagger.v1.jsonrefreshed (all 8 paths present).
Follow-ups for later phases (b15)
- Ticket system, partner centers, and the admin support-alert worklist console (this phase only raises
low_ratingalerts). SuspendNurse/ResolveSupportAlert/FlagConcernadmin actions.- Deferred by design: two-way double-blind reviews with timed reveal; a first-class
incidentsentity + ML fraud scoring; optional structuredvitals_encryptedon care records.