Files
baya-monorepo/archive/build-chain/working-context/reports/backend-phase-14-report.md
T
2026-08-02 18:48:32 +03:30

7.0 KiB
Raw Blame History

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), guarded moderation_status (+ reason/moderator/time), soft-delete filter. Indexes: unique booking_id, (nurse_profile_id, moderation_status), moderation_status. Marked IAuditable → the SaveChanges interceptor writes an append-only audit_logs diff 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), nullable booking_id provenance, body_encrypted (ciphertext, no EF converter), (patient_id, recorded_at) index, soft-delete.
  • NurseProfile gained SetReviewAggregates(avg, count) — the single sanctioned write for the existing AverageRating/TotalReviews columns (no new aggregate table).

Features (CQRS, Baya.Application/Features/)

  • Reviews/: SubmitReviewCommand, ModerateReviewCommand, AttachReviewTagsCommand, ListReviewsForNurseQuery, GetReviewModerationQueueQuery, GetTagAggregatesQuery; plus the internal RecomputeNurseRating from-source helper and ReviewCache (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 RecomputeNurseRating reads COUNT/SUM(rating) over the nurse's published reviews excluding the transitioning review (id 0 for 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_encrypted stores ciphertext directly; the handler Encrypts on write and Decrypts only after the access check passes — so no query path (list, projection) can ever surface plaintext. recorded_at is DateTime (UTC), not DateTimeOffset, so the SQLite test provider can ORDER BY it.
  • AI verdict → initial disposition. SubmitReview calls IReviewModerationService.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 land pending_moderation); Seams:ReviewModeration:AutoApproveClean opts into auto-publish. Human ModerateReview always overrides.

What is now testable and exactly how (the phase §7 steps)

Run the API against SQL Server; use Swagger/curl.

  1. Submit on a completed booking → accepted. POST bookings/{completedId}/review {rating:5} as the owner → 200, moderationStatus: pending_moderation. It does not appear in GET nurses/{id}/reviews (publish gate).
  2. Submit on a cancelled booking → rejected; a second submit on an already-reviewed booking → conflict (1:1).
  3. Moderate publish recomputes up. PATCH reviews/{id}/status {action:"publish"} → the review appears in the public list; the aggregate publishedCount increments and averageRating reflects it.
  4. Moderate hide recomputes down. Publish a 5★ and a 1★, then hide the 1★ → the public list drops it and the aggregate rises / count decrements (re-derived from source).
  5. Low rating raises an alert. POST … {rating:1} → a low_rating support_alerts row exists (visible only on the admin/internal path — never in a user response; its id shows on the moderation queue row).
  6. Write + read a care record. As a nurse with a confirmed booking, POST patients/{P}/care_records200; the stored column is ciphertext. As the owning customer or that nurse, GET … → decrypted note, newest first.
  7. Unauthorized nurse denied. A nurse with no confirmed booking for P → 403 on both write and read.

Automated coverage: 13 Foundation handler tests (Baya.Test.Foundation/Reviews/) cover steps 17 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), config Seams:ReviewModeration:{AutoApproveClean,BannedWords}. Make it real → a text classifier / LLM endpoint, registration-only swap (see reports/mocks-registry.md, row → 🟡). ModerateReviewCommand keeps 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.json refreshed (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_rating alerts).
  • SuspendNurse / ResolveSupportAlert / FlagConcern admin actions.
  • Deferred by design: two-way double-blind reviews with timed reveal; a first-class incidents entity + ML fraud scoring; optional structured vitals_encrypted on care records.