# Backend Phase 14 — Reviews, ratings & patient care records > **Mission:** close the trust loop and the continuity-of-care loop. Let a customer leave **one > moderated review per completed booking**, run that review through a moderation pipeline, and keep the > nurse's public rating **honest** by recomputing it from source on *every* status transition — so hiding > a 1-star never leaves an inflated average. Auto-raise an internal safety alert on low ratings. Separately, > let nurses author **encrypted, patient-scoped clinical notes** that accumulate into a longitudinal care > history a new nurse can read before taking over — under strict clinical access control. This is a > brand-survival area: buyers are vulnerable people cared for unobserved at home. > > **Track:** backend · **Depends on:** [backend-phase-9](backend-phase-9.md) (completed bookings + dispute window), [backend-phase-3](backend-phase-3.md) (profiles/patients), [backend-phase-1](backend-phase-1.md) (`support_alerts`, `platform_configs`, `audit_logs`, notifications), [backend-phase-7](backend-phase-7.md) (search aggregates) · **Unlocks:** the reviews UI ([frontend-phase-13-b14](../frontend/frontend-phase-13-b14.md)) > **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional. --- ## 1. Context — where this sits This is the trust-and-continuity phase. By now bookings can reach a **completed/closed** state (b9), the nurse and customer profiles + patients exist (b3), the platform can raise internal `support_alerts` and read config (b1), and the search index carries a denormalized nurse rating/count that must stay current (b7). This phase turns those pieces into the two things families actually judge a marketplace on after the visit: **a trustworthy public rating** and **clinical continuity across nurses**. Two distinct sub-domains live here, and they must not be conflated: 1. **Reviews & ratings** — public, social-proof, moderated, aggregate-driving. 2. **Patient care records** — private, clinical, encrypted, **patient-scoped** (not booking-scoped), accessed only by people with a clinical right to see them. **What already exists (do not rebuild):** - **Completed bookings + dispute window** — [backend-phase-9](backend-phase-9.md) built `bookings` with the 3-amount split, the lifecycle that reaches a **completed/closed** status, `booking_sessions`, `booking_care_instructions` (the two-stage clinical disclosure gate), `visit_verifications` (EVV), and set `dispute_window_ends_at = completed_at + dispute_window_hours` on completion. Read these statuses and relations; do not re-model booking lifecycle. - **Profiles & patients** — [backend-phase-3](backend-phase-3.md) built `customer_profiles`, `nurse_profiles` (including the denormalized **aggregate rating/count fields** you recompute here), and `patients` (with customer tenancy). The aggregate columns on `nurse_profiles` are *owned* by the nurse domain but **written by this phase** on every review transition. - **Platform signals & config** — [backend-phase-1](backend-phase-1.md) built `support_alerts` (the internal-only staff worklist + `RaiseSupportAlert` API), `platform_configs` (the typed cached accessor — including `min_rating_for_support_alert`), `audit_logs` (append-only, written by the SaveChanges interceptor on sensitive entities), and the in-app `notifications` write. **Reuse all of these** — you *raise* alerts, you do not define the table. - **Search aggregates** — [backend-phase-7](backend-phase-7.md) built `nurse_search_index` behind the `INurseSearch` seam, with **maintenance hooks** to refresh a nurse's denormalized rating/count. Every review transition that changes the nurse aggregate must trigger that refresh — do not write the search index directly; call the b7 maintenance hook. - **Cross-cutting seams** — [backend-phase-0](backend-phase-0.md) introduced `IFieldEncryptor` (the field encryptor you use for clinical notes), `ICacheService`, `IDateTimeProvider`, and `INotificationDispatcher`. **Reuse `IFieldEncryptor`** for `patient_care_records`; do not introduce a new encryption seam. > The **ticket/messaging** system (`tickets`, `ticket_participants`, `ticket_messages`), `partner_centers`, > and the admin support-alert *worklist console* land in [backend-phase-15](backend-phase-15.md). This phase > *raises* alerts and *consumes* the existing `support_alerts` raise API; it does **not** build the ticket > system or the alert worklist UI. **(DEFERRED → b15.)** ## 2. Required reading (do this first) - [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and [`../_shared/backend-conventions-checklist.md`](../_shared/backend-conventions-checklist.md). - **Product — business rules (source of truth):** [`product/business/11-reviews-trust-and-safety.md`](../../../product/business/11-reviews-trust-and-safety.md) — the one-per-completed-booking rule, recompute-on-every-transition, the configurable low-rating threshold, the "patient is not the sole information source" principle, and why this is a brand-survival area. - **Product — data model (source of truth):** [`product/data-model/10-reviews-and-records.md`](../../../product/data-model/10-reviews-and-records.md) — `reviews` (rating 1–5 CHECK, body, moderation status + fields; 1:1 → `bookings`, N:1 → `customer_profiles`/`nurse_profiles`), `review_tags_master`/`review_tag_links` (N:N), and `patient_care_records` (nurse-authored, encrypted, **patient-scoped**, strict access). Read the **"Why"** notes — they encode the guards you must enforce. - **Booking statuses you gate on** — re-read the `bookings` status enum and `dispute_window_ends_at` from [backend-phase-9](backend-phase-9.md)'s contract (`dev/contracts/domains/bookings.md`) so you key review eligibility off the *exact* completed/closed status values, not a guess. - **Config & alerts you reuse** — [backend-phase-1](backend-phase-1.md)'s handoff and `dev/contracts/domains/platform-signals.md` (or equivalent) for the `RaiseSupportAlert` signature, the `support_alerts` shape, and the typed config accessor for `min_rating_for_support_alert`. - **Search refresh you trigger** — [backend-phase-7](backend-phase-7.md)'s handoff for the `INurseSearch` maintenance hook that refreshes a nurse's aggregate rating/count in `nurse_search_index`. - **Code to mirror (existing patterns):** an existing feature folder under `Baya.Application/Features//{Commands|Queries}//` (request `record` + `internal sealed` handler + `OperationResult`), an `IEntityTypeConfiguration` under `Persistence/Configuration/Config/`, a controller under `Baya.Web.Api/Controllers/V1/` (`sealed`, `BaseController`, `ISender`, `base.OperationResult(...)`), and how prior phases call `IFieldEncryptor` for encrypted columns (b3 IBAN, b9 care instructions). - **Contract conventions:** [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) (envelope, routes, status codes) and [`money-and-types.md`](../../contracts/conventions/money-and-types.md) (this phase has **no money**, but follow the type/format rules for ids/enums/timestamps). ## 3. Scope — build this A vertical slice per capability: entity + EF config + migration → command/query handler(s) → controller endpoint → contract. Everything async with `CancellationToken`; reads are `AsNoTracking()` + `.Select()` projection + pagination; writes go through `IUnitOfWork` with a single `CommitAsync`. ### 3.1 Entities, configs & migration Add these tables (exact names below) as a single additive EF Core migration. One `IEntityTypeConfiguration` per entity in `Persistence/Configuration/ReviewsConfig/`. - **`reviews`** — one review per **completed** booking. - Columns: `id` (BIGINT PK), `booking_id` (FK → `bookings`, **UNIQUE** — enforces 1:1), `customer_profile_id` (FK → `customer_profiles`), `nurse_profile_id` (FK → `nurse_profiles`), `rating` (TINYINT/INT, **CHECK `rating BETWEEN 1 AND 5`**), `body` (NVARCHAR, nullable free text), `moderation_status` (enum: `pending_moderation` | `published` | `hidden` | `rejected`; default `pending_moderation`), `moderation_reason` (NVARCHAR, nullable — set on hide/reject), `moderated_by_id` (FK → users, nullable), `moderated_at` (datetimeoffset, nullable), plus the audit fields stamped by the interceptor (`CreatedAt/ModifiedAt/CreatedById/ModifiedById`) and soft-delete. - Indexes: unique on `booking_id`; index on `(nurse_profile_id, moderation_status)` for the public published-only list and the recompute query; index on `moderation_status` for the moderation queue. - Soft-delete query filter (`!IsDeleted`). - **`review_tags_master`** — standardized tag vocabulary. - Columns: `id` (BIGINT PK), `code` (e.g. `punctual`, `professional`, `clean`, `kind`; UNIQUE), `label_fa` (NVARCHAR), `label_en` (NVARCHAR), `is_active` (BIT), display order. **Seed** a starter vocabulary (at minimum: `punctual`, `professional`, `clean`, `kind`, `communicative`). - **`review_tag_links`** — N:N join `reviews` ↔ `review_tags_master`. - Columns: `id` (BIGINT PK) or composite key, `review_id` (FK), `review_tag_master_id` (FK), **UNIQUE `(review_id, review_tag_master_id)`** (no duplicate tag on a review). - **`patient_care_records`** — nurse-authored, **encrypted**, **patient-scoped** clinical notes. - Columns: `id` (BIGINT PK), `patient_id` (FK → `patients`, **the scoping key — not `booking_id`**), `booking_id` (FK → `bookings`, **nullable**, provenance only — which visit produced the note), `nurse_profile_id` (FK → `nurse_profiles`, the author), `body_encrypted` (VARBINARY/NVARCHAR holding the `IFieldEncryptor`-encrypted clinical note — **never plaintext**), optional encrypted structured fields (e.g. `vitals_encrypted`) if the digest schema carries them, `recorded_at` (datetimeoffset), plus audit + soft-delete. - Indexes: `(patient_id, recorded_at DESC)` for the longitudinal history read. > **Aggregate columns are not yours to add.** `nurse_profiles` already carries the denormalized > `average_rating` / `review_count` (or equivalently named) columns from b3. You **write** them on every > transition; you do not re-create them. If they are missing, add them to the b3 entity in place and note > it in your report (do not fork a parallel aggregate table). ### 3.2 Reviews — commands & queries Feature folder `Baya.Application/Features/Reviews/`. - **`SubmitReviewCommand`** (`Commands/SubmitReview/`) — customer submits `rating` (1–5) + `body` (+ optional tag codes) for a booking. - Guards (return `OperationResult.FailureResult`/`NotFoundResult`, **never throw**): the booking exists and is owned by the calling customer (tenancy via `ICurrentUser` → `customer_profiles`); the booking is in a **completed/closed** status (reject `cancelled`/`expired`/in-progress); **no review already exists** for that booking (1:1). Insert as `pending_moderation`. If tag codes were passed, also write `review_tag_links` in the same transaction (validate codes against `review_tags_master`). - FluentValidation: `rating` in 1..5, `body` length bound. - On create, if `rating <= min_rating_for_support_alert` (config, default 2), **call the b1 `RaiseSupportAlert`** with a `low_rating` type and the `review_id`/`booking_id` linkage (see §3.4). - **`ModerateReviewCommand`** (`Commands/ModerateReview/`) — admin/AI transition: `publish` | `hide` | `reject` | `unpublish`. Sets `moderation_status`, `moderation_reason` (on hide/reject), `moderated_by_id`, `moderated_at`. **In the same transaction**: (a) recompute the nurse aggregate from source (§3.3); (b) the audit interceptor writes the transition to `audit_logs`. After commit, trigger the b7 search-index refresh for that nurse (§3.4). Optionally notify the review author of the outcome via `INotificationDispatcher`. - The AI verdict (auto pre-screen) runs through `IReviewModerationService` on `SubmitReview` (§4); the *decision authority* is still this command — a verdict can pre-set `pending_moderation` with a flag, or auto-`published`/auto-`hidden` per config, but the human path must always be able to override. - **`AttachReviewTagsCommand`** (`Commands/AttachReviewTags/`) — add/replace `review_tag_links` for a review the caller owns (or admin). Enforce the unique `(review_id, review_tag_master_id)`. - **`ListReviewsForNurseQuery`** (`Queries/ListReviewsForNurse/`) — **public**, paginated, returns **`published` only** + the nurse aggregate (avg rating + count). `AsNoTracking()` + `.Select()` projection; cache the aggregate read through `ICacheService` with invalidation on transition. - **`GetReviewModerationQueueQuery`** (`Queries/GetReviewModerationQueue/`) — **admin**, paginated, filter by `moderation_status` (default `pending_moderation`), sortable, includes any linked low-rating alert id. - **`GetTagAggregatesQuery`** (`Queries/GetTagAggregates/`) — per-nurse tag rollup ("% punctual" = links for that tag over published reviews of that nurse). Paginated/bounded; from published reviews only. ### 3.3 Aggregate recompute (internal domain service) - **`RecomputeNurseRating`** — an internal application service (not an endpoint), invoked by **every** `ModerateReviewCommand` transition *and* on `SubmitReview` only insofar as a brand-new review is `pending_moderation` and therefore must **not** yet count. Recompute `nurse_profiles.average_rating` and `review_count` **from the source** — i.e. `AVG(rating)`/`COUNT(*)` over the nurse's **currently `published`** reviews — never by incremental `+delta`/`-delta`. This is the fix for inflated-rating-after- hide drift: hiding a 1-star *lowers* the count and re-derives the average from what remains public. Do it inside the same transaction as the status change. ### 3.4 Patient care records — commands & queries Feature folder `Baya.Application/Features/PatientCareRecords/`. - **`WritePatientCareRecordCommand`** (`Commands/WritePatientCareRecord/`) — a nurse authors a note for a **patient** (optionally tagged with the `booking_id` that produced it). Encrypt `body` via `IFieldEncryptor.Encrypt(...)` before persisting to `body_encrypted`. Guard: the calling nurse must have a **confirmed** (or active/completed) booking for that patient — a nurse cannot write notes for a patient they were never assigned to. - **`GetPatientHistoryQuery`** (`Queries/GetPatientHistory/`) — patient-scoped longitudinal history, paginated, ordered `recorded_at DESC`. Decrypt each `body_encrypted` via `IFieldEncryptor.Decrypt(...)` **only after** the access check passes. **Strict access (§5):** the owning customer (patient's `customer_profile`), any nurse with a **confirmed** booking for that patient, and admin — *nobody else*. ### 3.5 REST endpoints Controllers under `Baya.Web.Api/Controllers/V1/` (`sealed`, `BaseController`, inject `ISender`, `[controller]`/`[action]` snake_case tokens, `base.OperationResult(...)`, narrowest authorize policy, OTP/ refund-grade rate limiting not required here but keep public review-read sensibly limited): | Verb & route | Maps to | Auth | | --- | --- | --- | | `POST /v1/bookings/{booking_id}/review` | `SubmitReviewCommand` | customer (owns booking) | | `POST /v1/reviews/{id}/tags` | `AttachReviewTagsCommand` | review owner / admin | | `PATCH /v1/reviews/{id}/status` | `ModerateReviewCommand` | admin / moderator | | `GET /v1/nurses/{nurse_profile_id}/reviews` | `ListReviewsForNurseQuery` | public | | `GET /v1/nurses/{nurse_profile_id}/review-tags` | `GetTagAggregatesQuery` | public | | `GET /v1/admin/reviews/moderation-queue` | `GetReviewModerationQueueQuery` | admin | | `POST /v1/patients/{patient_id}/care-records` | `WritePatientCareRecordCommand` | nurse (confirmed booking) | | `GET /v1/patients/{patient_id}/care-records` | `GetPatientHistoryQuery` | owning customer / nurse w/ confirmed booking / admin | ### 3.6 Out of scope (DEFERRED — build the seam/hook, not the feature) - Two-way (nurse-reviews-customer) double-blind reviews with timed reveal — **(DEFERRED)**, see `product/business/11-reviews-trust-and-safety.md` (c). - First-class `incidents` entity + ML fraud scoring — **(DEFERRED)**; manual suspension + `support_alerts` cover it now. - The ticket system, partner centers, and the admin **support-alert worklist console** — **(DEFERRED → [backend-phase-15](backend-phase-15.md))**. You *raise* alerts here; b15 builds the worklist. - `SuspendNurse` / `ResolveSupportAlert` / `FlagConcern` admin actions — **(DEFERRED → b15)** with the support backoffice. ## 4. Mocks & seams in this phase | Seam | Owner | Mock behaviour | Registry | | --- | --- | --- | --- | | **`IReviewModerationService`** (AI moderation) — **INTRODUCED here** | this phase | `Task ScreenAsync(string reviewText, CancellationToken)` returning a verdict (`Approve`/`Flag`/`Reject` + reason). Mock = a **keyword filter / pass-through**: clean text → `Approve` (or "needs human review" per config), banned-word hit → `Flag`. No external call. Selection by config/registration. | **add row** | | `IFieldEncryptor` (field encryption) — **REUSE from [b0](backend-phase-0.md)** | b0 | local symmetric key; `Encrypt`/`Decrypt`. Clinical notes go through it. Do not redefine. | reuse | | `INurseSearch` maintenance hook — **REUSE from [b7](backend-phase-7.md)** | b7 | refreshes the nurse aggregate in `nurse_search_index`. Call it after every aggregate-changing transition. | reuse | | `support_alerts` `RaiseSupportAlert` — **REUSE from [b1](backend-phase-1.md)** | b1 | inserts an internal alert row. Call it on low ratings. | reuse | | `INotificationDispatcher` — **REUSE from [b0](backend-phase-0.md)/[b1](backend-phase-1.md)** | b0/b1 | in-app write (no push at MVP). Optional review-outcome notice. | reuse | Register `IReviewModerationService` (interface in `Application/Contracts/`, mock impl in Infrastructure) via a `ServiceConfiguration/` extension — never inline in `Program.cs`. Record it in [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) with: seam, file, what's faked, config keys (e.g. banned-word list, auto-approve toggle), how to make it real (point `ScreenAsync` at a real text classifier/LLM endpoint without touching `ModerateReviewCommand`), status 🟡. ## 5. Critical rules you must not get wrong - **Review eligibility — completed/closed bookings only.** Create a review **only** for a booking in the completed/closed status, owned by the calling customer. **Never** for `cancelled`/`expired`/in-progress bookings, non-existent bookings, or another customer's booking. (Anti-fraud, brand integrity.) - **Enforce 1:1 — no duplicate reviews.** A unique constraint on `reviews.booking_id` is the authoritative backstop; the handler also checks first and returns a clean `OperationResult` failure, never a raw DB exception, on a second submit. - **Recompute the nurse aggregate on EVERY transition, FROM SOURCE.** Publish, hide, reject, **and** unpublish must all recompute `nurse_profiles.average_rating`/`review_count` from the nurse's **currently `published`** reviews — `AVG`/`COUNT` over the source set, **not** an incremental `+delta`/`-delta`. This is the explicit fix for the inflated-rating-after-hide drift: hiding a 1-star lowers the count and re-derives the average. Do it transactionally with the status change, then refresh the b7 search index. - **Publish gate — `pending_moderation` is NEVER public.** Reviews default to `pending_moderation` and must never be rendered by any public/customer-facing query. `ListReviewsForNurseQuery` returns **`published` only**; the aggregate counts **`published` only**. Filter at the query layer, not just the UI. - **`patient_care_records` is PATIENT-scoped, not booking-scoped.** A new nurse taking over **must** read the prior history before accepting — do not silo notes per booking. The scoping key is `patient_id`; `booking_id` is nullable provenance only. - **Strict clinical access + encrypted at rest.** `patient_care_records` are readable **only** by: the owning customer (the patient's `customer_profile`), nurses with a **confirmed** booking for that patient, and admin. Enforce in the authorization layer (not just the route policy). All clinical fields are **encrypted via `IFieldEncryptor`** — never store, log, or project plaintext clinical content; decrypt only after the access check passes. A nurse **without** a confirmed booking for that patient is **denied** read and write. - **Low-rating → `support_alerts` must fire reliably.** It is a **safety signal**. On a review at/below `min_rating_for_support_alert` (config, default 2), raise the alert in the same flow; a failure to raise must surface (it is not a best-effort fire-and-forget that can be silently swallowed). - **`support_alerts` are internal-only.** They must never appear in any user-facing response or join. You *raise* them; their worklist UI is b15. - **Append-only audit.** Every review transition writes `audit_logs` via the interceptor — never mutate or delete prior audit rows. The low-rating threshold is **config-driven** (read via the b1 typed accessor), never hard-coded. ## 6. Definition of Done The shared [definition-of-done.md](../_shared/definition-of-done.md), plus: - [ ] `reviews`, `review_tags_master`, `review_tag_links`, `patient_care_records` exist via one additive migration with the constraints in §3.1 (unique `booking_id`, `rating` CHECK 1–5, unique `(review_id, review_tag_master_id)`); `review_tags_master` is seeded. - [ ] `SubmitReview`/`ModerateReview`/`AttachReviewTags`/`ListReviewsForNurse`/`GetReviewModerationQueue`/ `GetTagAggregates`/`WritePatientCareRecord`/`GetPatientHistory` are implemented as CQRS features with validators and the §3.5 endpoints, returning the standard `OperationResult` envelope. - [ ] Every moderation transition recomputes `nurse_profiles` **from source** and triggers the b7 search refresh; the recompute is covered by a test that proves hide lowers both count and average. - [ ] Low rating raises a `support_alerts` row using the b1 API and the config threshold; verified by a test. - [ ] `patient_care_records` are encrypted at rest via `IFieldEncryptor` and gated by the strict clinical access rule; a nurse without a confirmed booking is denied (tested). - [ ] `IReviewModerationService` is introduced behind a DI seam with a keyword/pass-through mock and a registry row. - [ ] `dotnet build Baya.sln` zero new warnings; `dotnet test Baya.sln` green including this phase's tests. - [ ] The contract `dev/contracts/domains/reviews-records.md` is written and the `swagger.json` snapshot is refreshed; the `server/CLAUDE.md` *Project map* notes the two new feature areas + the `IReviewModerationService` seam. ## 7. How to test (what a human can verify after this phase) Run the API (`dotnet run --project src/API/Baya.Web.Api/...`) against a reachable SQL Server; use Swagger or curl. Expected results below become the "what can be tested" section of your report. 1. **Submit on a completed booking → accepted.** As the owning customer, `POST /v1/bookings/{completed_id}/review` with `rating: 5`. → `200`, review created with `moderation_status: pending_moderation`. It does **not** appear in `GET /v1/nurses/{id}/reviews` yet (publish gate). 2. **Submit on a cancelled booking → rejected.** Same call against a `cancelled`/`expired` booking → an `OperationResult` failure (not a 500). A second submit on the already-reviewed booking → failure (1:1). 3. **Moderate publish recomputes up.** `PATCH /v1/reviews/{id}/status` `publish` → review appears in the public list; `GET /v1/nurses/{id}/reviews` aggregate `review_count` increments and `average_rating` reflects it. 4. **Moderate hide recomputes down.** Publish a 5★ and a 1★, then `hide` the 1★ → public list drops it and the aggregate `average_rating` rises / `review_count` decrements (re-derived from source — not stale). 5. **Low rating raises an alert.** Submit `rating: 1` → a `support_alerts` row of the low-rating type exists (visible only on the admin/internal path, never in any user response). 6. **Write + read a care record with access control.** As a nurse **with a confirmed booking** for patient P, `POST /v1/patients/{P}/care-records` with a clinical note → `200`; the stored column is ciphertext (not plaintext). As the owning customer or that nurse, `GET /v1/patients/{P}/care-records` → decrypted note returned, newest first. 7. **Unauthorized nurse denied.** As a nurse **without** any confirmed booking for patient P, both the write and the read of P's care records → `403`/access-denied `OperationResult`. ## 8. Hand off & document (close the phase) - **Docs to update (same change):** `server/CLAUDE.md` *Project map* — add the `Features/Reviews` and `Features/PatientCareRecords` areas, the four new tables + their config folder, and the `IReviewModerationService` seam (where it's registered). If you had to add the aggregate columns to `nurse_profiles`, note it. If you discovered/decided any business rule not already in the product docs, reflect it in [`product/business/11-reviews-trust-and-safety.md`](../../../product/business/11-reviews-trust-and-safety.md) or [`product/data-model/10-reviews-and-records.md`](../../../product/data-model/10-reviews-and-records.md) (no invented rules — record decisions, and regenerate the HTML view per `product/CLAUDE.md` if you touched Markdown). - **Contract to write:** publish **`dev/contracts/domains/reviews-records.md`** (the §3.5 routes, request/ response shapes, the `moderation_status` enum, the `review_tag` codes, the care-record access-rule matrix, status codes, examples) per [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md), and refresh the `swagger.json` snapshot per [`../../contracts/openapi/README.md`](../../contracts/openapi/README.md) so [frontend-phase-13-b14](../frontend/frontend-phase-13-b14.md) can derive its types (it does not guess shapes). - **Handoff & report:** write `shared-working-context/backend/handoff/after-backend-phase-14.md` (reviews + care-records endpoints are live; what f13 can now build; what's mocked — the AI moderation seam; the publish-gate and clinical-access rules the frontend must respect), append your phase summary to `shared-working-context/backend/STATUS.md`, write `reports/backend-phase-14-report.md` (what was built, what is now testable and exactly how — the §7 steps — what is mocked + how to make it real, contracts produced, follow-ups for b15), and update [`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) with the `IReviewModerationService` row → 🟡. - **Memory:** save a `project`-type memory note for the non-obvious decisions here — the **recompute-from- source (not delta)** rule and where it's invoked, the **patient-scoped (not booking-scoped)** care-record access matrix, and the `IReviewModerationService` seam selection — with a one-line `MEMORY.md` pointer.