Skip to content

Claim comments (评论) — design

Audit row 8.8; Figma 286:9929 and 297:15763 (claim detail drawer). Figma draws only the empty state — heading “评论 (n)”, avatar + input + 发布, and an icon over “暂无评论” with a hint line — so the thread itself is designed here.

  1. A comment is a record. expense_claim_comment (migration 00250): claim_id, optional parent_id, author_member_id, body, created_at, edited_at, deleted_at. erp_app gets no DELETE; the table is on retention_test.go’s amendable list. Composite FKs keep a reply on the same claim as its parent ((org_id, claim_id, parent_id)).
  2. Edit history lives in audit_log. Every create, edit and delete writes audit.Write with the comment before and after, body included — the hash-chained trail already is the append-only revision store, so no revisions table. The row carries edited_at; the UI shows “edited”.
  3. Delete is soft. deleted_at is set, the body stays in the row (and in audit_log). The API returns body: null for a deleted comment and the UI shows “Comment deleted” in its place, so its replies keep their context. A deleted comment cannot be edited, liked or replied to (409).
  4. One level of replies. A reply’s parent must be a top-level comment (422 otherwise). The client renders replies indented under their parent; “Reply” on a reply replies to the parent.
  5. Likes — expense_claim_comment_like, one row per member per comment (named UNIQUE). Unlike sets withdrawn_at; like again clears it. Also no DELETE (amendable), per the brief. Likes are not audited: a reaction, not a statement.
  6. Who. Every route is expenses.claim.read and then Ruling R21’s CanRead on the claim: claimant, eligible/past approvers, read_all holders. A non-reader gets 404, like GET claim. Only the author edits or deletes (403 for another reader). A comment id that is not on the claim in the path is 404.
  7. Record language. Comments are record text, so CheckRecordText (en/ms, spec 11A.3) applies, with a DB CHECK twin. A zh-locale user writing Chinese gets the same 422 a Chinese claim title does; the UI shows it inline under the input.
  8. Wire. GET/POST /expenses/claims/{id}/comments, PATCH/DELETE .../comments/{commentId}, PUT/DELETE .../comments/{commentId}/like. The list is flat, oldest first, each item carrying parent_id, like_count, liked_by_me; the client groups. Mutations return the one comment and the client refetches the list. No pagination (a claim’s thread is small; add a cursor if one ever is not). Body 1–2000 characters, trimmed.
  9. UI. A ClaimComments section below the timeline: count heading, composer (initials avatar, input, 发布 — Enter posts), empty state, then threads. Each comment: initials avatar, name, relative time, “edited”, body, and actions Like (count), Reply, and — own comments only — Edit / Delete (delete asks to confirm). Names come from useMemberNames.