Reviews Features and Flows
Developer Resources Reviews Reviews Features and Flows Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Reviews module.
Use this page for the reviews domain: what it does for customers and admins, and how each flow behaves from start to finish.
Source Type Files or Docs What Was Extracted API apps/api/src/modules/reviews/customer/{product-review,my-review}/*.controller.ts, admin/{review,report}/*.controller.tsRoutes, permissions, rate limits Backend shared/*.service.ts, customer/admin servicesLifecycle, aggregate deltas, eligibility Schema packages/db/src/schema/reviews/*.ts4 tables, 5 enums, GENERATED aggregate Probe .omc/plans/reviews/probe-constraints.mjs107/0, both directions Error registry apps/api/src/common/types/error-codes.ts (// REVIEW)REVIEW_* codes
Field Value Module reviewsSubmodule N/A (product reviews + rating aggregate) Primary user value Buyers tell the truth about products they actually received; the product page renders a guaranteed-consistent average, count and distribution; moderators keep the page clean without ever rewriting a customer's words Actors Guest (public reads), customer (own reviews, reports), admin (moderation, abuse queue) Main entry points /api/mobile/products/{productId}/reviews(/:summary), /api/mobile/reviews* (7), /api/admin/reviews* (9), /api/admin/review-reports* (2)Main outputs Reviews, the rating aggregate, moderation events, abuse reports Related docs API , Backend
Actor Can Do Cannot Do Auth Requirement Notes Guest Read published reviews + summary for a product Write, report, see non-published states None (@Public()) Zeroes, never nulls, for unreviewed products Customer Check eligibility, list/detail own reviews (every state), create (5/hr), edit, withdraw, report (10/hr) Review without a delivered purchase (403), hold two live reviews for one product (409), edit a hidden/deleted review (409 → withdraw), report own review (409) JWT REVIEW_NOT_FOUND for another customer's reviewAdmin List (2 queues), detail, events timeline, approve, reject, hide, restore, bulk approve, recalculate, report queue, dismiss Create or edit review content — moderation changes visibility only Admin JWT + Reviews_* hide resolves the review's open reportsWorker Recalculate, moderation emails — BullMQ Hourly drift sweep logs every repair
Capability Surface Actor Route/Trigger State Read State Written Linked API Section Public reviews Guest Guest GET /api/mobile/products/:productId/reviewspublished reviews — API §4 Public summary Guest Guest GET .../reviews/summaryaggregate — API Eligibility Customer Customer GET /api/mobile/reviews/eligibilityorders, reviews — API My reviews Customer Customer GET /api/mobile/reviews(/:id)own rows — API Create Customer Customer POST /api/mobile/reviewseligibility review + aggregate API Edit Customer Customer PATCH /api/mobile/reviews/:idown row review + aggregate API Withdraw Customer Customer DELETE /api/mobile/reviews/:idown row deleted_at + aggregate API Report Customer Customer POST /api/mobile/reviews/:id/reportreview report row API Admin list/detail/events Admin Admin /api/admin/reviews*reviews + events — API Moderation Admin Admin /{id}/approve|reject|hide|restorereview + version status + reason API Bulk approve Admin Admin /bulk/approvereviews statuses API Recalculate Admin Admin /summary/:productId/recalculatereviews aggregate API Report queue Admin Admin /api/admin/review-reports*reports status API
A customer who received a product writes a review. The review publishes on submission — moderated reactively — and the aggregate moves in the same transaction.
Branch Condition Behavior Error/Result Not eligible No delivered purchase 403 REVIEW_NOT_ELIGIBLEAlready reviewed Live review exists 409 + existingReviewId Switch to edit Retry Same Idempotency-Key Original 201 replayed No collision Rate limit 5/hour 429 Back off — not a bug Product gone Missing/withdrawn 404 REVIEW_PRODUCT_NOT_FOUND
An edit of a published review returns it to pending and removes it from the aggregate in the same transaction. It disappears from the product page until a moderator approves. This is deliberate — new content is trusted, changed content is not — and it closes bait-and-switch without gating the honest first submission. Tell the customer before they submit the edit; it is the single most surprising behaviour in the module.
POST /api/mobile/reviews/:id/report — reason ∈ spam · offensive · fake · irrelevant · personal_information · other, detail optional (≤500, admin-only). A report never hides anything — it raises a counter that orders the moderator's queue, one report per customer per review. recorded: false with a 200 is a success (already reported) — never surface it as an error. Do not imply the review will be removed.
The detail response gained a rating block (averageRating, reviewCount, distribution). Detail responses only — listings do not carry it (a rating per card is a join per page); use the summary endpoint for the products a listing renders stars for. Zeroes, never nulls, for an unreviewed product — reviewCount is what tells you whether averageRating: 0 means "bad" or "nobody has said".
Two queues: ?status=pending&sort=oldestPending (edits awaiting approval) and ?reportedOnly=true&sort=mostReported (abuse).
Moderation actions all take the fetched version — two moderators cannot silently overwrite each other (409 REVIEW_STALE_VERSION otherwise). reject and hide require a reason; hide resolves the review's open reports in the same transaction.
Bulk approve ({ "ids": [...] }, max 100) returns { approved, skipped }.
Recalculate (/summary/:productId/recalculate) returns 202 — a repair path, never the write path.
Report queue rows carry the review's own text so a moderator decides without a second call; the reporting customer is deliberately never identified — do not build a UI that expects it.
No admin create and no admin edit of review content — moderation changes visibility; the customer's words survive every action, and product_review_event records each one with no UPDATE path anywhere.
From Event/Action To Guard Side Effects — create publisheddelivered purchase, no live review aggregate +1 publishedcustomer edit pendingversion aggregate −1 pendingcustomer edit pendingversion none (still out) pendingadmin approve publishedversion aggregate +1 pendingadmin reject rejectedversion + reason — publishedadmin hide hiddenversion + reason aggregate −1, reports resolved hiddenadmin restore publishedversion aggregate +1 any live customer withdraw deletedversion aggregate −1, slot freed
The aggregate is maintained by ±1 deltas inside the transaction ; the recalculate job is a repair path only.
Flow DB Writes Cache Effects Jobs Realtime Analytics Notifications Create review + aggregate product review cache — — — — Edit review + aggregate cache moderation email via outbox — — — Withdraw deleted_at + aggregate cache — — — — Report report row — — — — — Moderation status + reason + events cache moderation email — — — Recalculate aggregate cache queue — — log every repair
Scenario Trigger User/System Experience Recovery Source Stale version Concurrent edit/moderation 409 Re-fetch, show current, ask again REVIEW_STALE_VERSIONAlready exists Second review 409 + id Switch to edit REVIEW_ALREADY_EXISTSNot editable hidden/deleted 409 Offer withdraw REVIEW_NOT_EDITABLEReport own Self-report 409 Hide the control REVIEW_REPORT_SELFReport resolved Another moderator 409 Refresh REVIEW_REPORT_ALREADY_RESOLVEDBulk too large >100 ids 409 Split REVIEW_BULK_LIMIT_EXCEEDEDAggregate drift Anything Hourly sweep repairs logged recalculate
Actor capability diagram — §3/§4.
Sequence diagram per major flow — §5.1.
State machine diagram — §5.2/§7.
Data side-effect diagram — §9.
Error branch diagram — §10.
Feature Minor Behavior Actor Trigger User/System Result Backend Side Effect Source Eligibility canCreate is the keyCustomer Check Already accounts for purchase + existing — My list No filter returns every state Customer List Includes withdrawn — Edit Empty string clears, omit keeps Customer PATCH Opposite meanings asymmetry documented Edit editedAt rendered with publishedAtCustomer View Edited ≠ original both dates Withdraw Version as query param Customer DELETE Survives proxies — Report recorded:false = successCustomer Repeat report Already reported 200 Admin list Two named queues Admin Filters pending-oldest, reported-most — Hide Resolves reports Admin Hide Leaves abuse queue same tx Recalculate 202 Admin Request Async repair logged Summary Zeroes not nulls Guest View Count distinguishes — Detail rating Detail-only block Guest Product page Listings untouched —
Rule Business Reason Actor Impact Enforced In API Impact Backend Impact Tests One review per (customer, product) One honest vote Edit, not second partial unique on deleted_at IS NULL 409 — probe Auto-publish, moderate reactively Trust new content Instant visibility status default — — probe Edit → pending No bait-and-switch Surprise transition aggregate −1 — probe No verified-purchase flag Structural Badge from row order + item FKs — — probe Aggregate GENERATED No inconsistent average — GENERATED ALWAYS428C9 on write — probe Empty bucket decrement aborts No negative average — CHECK 23514 — probe Report never hides Free speech + moderation Counter only service — — spec No admin content writes Words survive — no UPDATE path — — probe
Product Decision User Benefit Engineering Benefit Alternative Tradeoff Risk Verified purchase structural No flag to forget FK from order line Manual flag Ineligible customers 403 Auto-publish Instant feedback Reactive queue Pre-moderation Bad content briefly live Reports Edit → pending Honest edits Aggregate consistent In-place edit Surprise Documented Aggregate ±1 in tx No drift window — Recompute always Write cost Recalculate repair No admin content edits Customer words survive Event log immutable Admin rewrite Moderation limited Hide/reject Detail-only rating No join per card — Listing ratings Second call Summary endpoint
Flow Edge Case Trigger Expected Behavior User/System Feedback Source Create Second review Live exists 409 + id switch to edit Create No purchase Ineligible 403 hide control Create Retry Same key Original 201 idempotency Edit Stale version Concurrent 409 re-fetch Edit Hidden review Edit hidden 409 offer withdraw Withdraw After withdraw New review Slot freed can create again Report Self Own review 409 hide control Report Repeat Already reported 200 recorded:false success Moderate Double action Two admins 409 refresh Recalculate During write Concurrent Repair path safe logged
Flow Reads Writes Cache Jobs/Events Response Fields Public reviews published reviews — product review — items, pagination Summary aggregate — product review — average, count, distribution Create eligibility review + aggregate invalidate — MyReview Edit own review review + aggregate invalidate moderation email MyReview Report review report row — — recorded Moderate review + version status + reason + events invalidate email admin row Recalculate reviews aggregate invalidate queue 202