Files
baya-monorepo/dev/contracts/domains/reviews-records.md
T
2026-07-09 15:30:03 +03:30

8.4 KiB
Raw Blame History

Contract — Reviews & Patient Care Records (backend phase b14)

The trust-and-continuity surface: a customer leaves one moderated review per completed booking; an admin/moderator transitions it (recomputing the nurse's public rating from source on every transition); the public reads only ever see published reviews + the aggregate; and nurses author encrypted, patient-scoped clinical notes readable only under a strict clinical-access rule. Assumes ../conventions/api-conventions.md + ../conventions/money-and-types.md. Machine schema: ../openapi/swagger.v1.json.

Status: live as of backend-phase-b14 · Frontend consumer: frontend-phase-f13-b14

There is no money in this domain. Ratings are integers 15; the aggregate averageRating is a decimal (2-dp, e.g. 4.5). Timestamps are ISO-8601. List query params are camelCase (page, pageSize, status). The response envelope is the standard { data, … }; the shapes below are the data.

Enums used

  • moderationStatus: pending_moderation | published | hidden | rejected. A review is born pending_moderation and is never public / never counted until published. Only published reviews are returned by any public read and only published reviews feed the aggregate.
  • Moderation action (the PATCH body): publish | hide | reject | unpublish. hide/reject require a reason; unpublish returns a published review to pending_moderation.
  • Review tag codes (seeded vocabulary): punctual | professional | clean | kind | communicative.

Reviews

POST api/v1/bookings/{bookingId}/review

  • Purpose: The customer submits the one review for a completed booking.
  • Auth: authenticated customer who owns the booking (tenancy enforced in the handler).
  • Path params: bookingId (long).
  • Request body: { "rating": 5, "body": "great care", "tagCodes": ["punctual","kind"] }rating 15 required; body optional (≤ 2000); tagCodes optional (validated against the active vocabulary).
  • Success 200 (data): SubmitReviewResult{ id, moderationStatus, lowRatingAlertRaised }. The status is pending_moderation by default (the AI pre-screen keeps clean text pending; a banned-word hit auto-hides).
  • Failure cases: 400 rating out of 15 / unknown tag code; 401 unauthenticated; 403 caller is not a customer; 404 booking not found or not owned (a cross-tenant booking is a not-found, never a leak); a plain failure when the booking is not completed/closed; 409 the booking is already reviewed (1:1).
  • Notes: A rating min_rating_for_support_alert (config, default 2) raises an internal low_rating support_alert (never surfaced on any user response). The new review does not appear in the public list until published.

POST api/v1/reviews/{reviewId}/tags

  • Purpose: Replace a review's standardized tags with exactly the requested set.
  • Auth: the review's author or a moderator (admin/super_admin/moderation) — enforced in the handler.
  • Path params: reviewId (long). Request body: { "tagCodes": ["punctual","professional"] }.
  • Success 200 (data): ReviewTagsResult{ reviewId, tagCodes }.
  • Failure cases: 400 unknown tag code; 401; 403 not the author and not a moderator; 404 review not found. The UNIQUE(reviewId, tagCode) forbids a duplicate tag (the set is de-duplicated server-side).

PATCH api/v1/reviews/{reviewId}/status

  • Purpose: Admin/moderator moderation transition (the human decision authority; always overrides the AI).
  • Auth: admin / moderator (dynamic-permission policy).
  • Path params: reviewId (long). Request body: { "action": "publish", "reason": null }hide/reject require a non-empty reason (≤ 500).
  • Success 200 (data): ModerateReviewResult{ id, moderationStatus, averageRating, totalReviews } (the recomputed-from-source nurse aggregate).
  • Failure cases: 400 unknown action / missing reason on hide|reject; 401; 403 non-admin; 404 review not found.
  • Notes: Every transition recomputes nurse_profiles.averageRating/totalReviews from the nurse's currently-published reviews (not an incremental delta) and refreshes the search index — in the same transaction as the status change — so hiding a low rating lowers the count and re-derives the average.

GET api/v1/nurses/{nurseProfileId}/reviews — public

  • Purpose: The public reviews for a nurse: the rating aggregate + a page of published reviews.
  • Auth: anonymous. Path params: nurseProfileId (long). Query: page (default 1), pageSize (default 20, max 100).
  • Success 200 (data): NurseReviewsResult{ aggregate: { averageRating, publishedCount }, reviews: PagedResult<ReviewListItemDto> }. ReviewListItemDto = { id, rating, body, tagCodes[], createdAt }never carries moderation internals. An unknown nurse returns a zero aggregate + empty page (200).
  • Notes: The publish gate is enforced at the query layer — a pending_moderation/hidden/rejected review is never returned and never counted. The aggregate is cached and invalidated on every transition.

GET api/v1/nurses/{nurseProfileId}/review_tags — public

  • Purpose: The per-nurse tag rollup ("% punctual") over published reviews.
  • Auth: anonymous. Path params: nurseProfileId (long).
  • Success 200 (data): NurseTagAggregatesResult{ publishedReviewCount, tags: TagAggregateDto[] } where TagAggregateDto = { code, labelFa, labelEn, count, percentage } (percentage of published reviews, 1-dp). The active seeded vocabulary is always returned (zero counts for a nurse with no published reviews).

GET api/v1/admin/reviews/moderation_queue — admin

  • Purpose: The moderation worklist.
  • Auth: admin / moderator. Query: status (default pending_moderation; any moderationStatus), page, pageSize.
  • Success 200 (data): PagedResult<ModerationQueueItemDto>{ id, bookingId, nurseProfileId, customerProfileId, rating, body, moderationStatus, moderationReason, lowRatingAlertId, createdAt }. The lowRatingAlertId is the linked internal alert (id only — support alerts stay internal).
  • Failure cases: 400 unknown status; 401; 403 non-admin.

Patient care records

Clinical bodies are encrypted at rest and returned decrypted only after the access check passes. The access rule is enforced in the handler, not just the route policy.

Access matrix (both endpoints):

Caller Write Read
Nurse with a confirmed (or in-progress/completed/disputed/closed) booking for the patient
Nurse without such a booking 403 403
The patient's owning customer (only nurses author)
Admin / super_admin (only nurses author)
Anyone else 403

POST api/v1/patients/{patientId}/care_records

  • Purpose: A nurse authors a clinical note for a patient (patient-scoped; the note is encrypted before persist).
  • Auth: authenticated nurse with a qualifying booking for the patient.
  • Path params: patientId (long). Request body: { "bookingId": 123, "body": "…" }bookingId optional provenance (which visit produced the note); body required (≤ 8000).
  • Success 200 (data): WriteCareRecordResult{ id, patientId, recordedAt }.
  • Failure cases: 401; 403 caller is not a nurse or has no qualifying booking for the patient; 404 patient not found; 400 empty body.

GET api/v1/patients/{patientId}/care_records

  • Purpose: The patient-scoped longitudinal history, newest first.
  • Auth: owning customer / nurse with a qualifying booking / admin (see the matrix).
  • Path params: patientId (long). Query: page, pageSize.
  • Success 200 (data): PagedResult<CareRecordDto>CareRecordDto = { id, patientId, bookingId, nurseProfileId, nurseName, body, recordedAt } (body decrypted). Ordered by recordedAt DESC.
  • Failure cases: 401; 403 no clinical access; 404 patient not found.
  • Notes: The record is patient-scoped, not booking-scoped — a new nurse taking over reads the whole history (not just their own booking's notes).