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.
Decisions
Section titled “Decisions”- A comment is a record.
expense_claim_comment(migration 00250):claim_id, optionalparent_id,author_member_id,body,created_at,edited_at,deleted_at. erp_app gets no DELETE; the table is on retention_test.go’samendablelist. Composite FKs keep a reply on the same claim as its parent ((org_id, claim_id, parent_id)). - Edit history lives in audit_log. Every create, edit and delete writes
audit.Writewith the comment before and after, body included — the hash-chained trail already is the append-only revision store, so no revisions table. The row carriesedited_at; the UI shows “edited”. - Delete is soft.
deleted_atis set, the body stays in the row (and in audit_log). The API returnsbody: nullfor 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). - 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.
- Likes —
expense_claim_comment_like, one row per member per comment (named UNIQUE). Unlike setswithdrawn_at; like again clears it. Also no DELETE (amendable), per the brief. Likes are not audited: a reaction, not a statement. - Who. Every route is
expenses.claim.readand then Ruling R21’sCanReadon 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. - 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. - Wire.
GET/POST /expenses/claims/{id}/comments,PATCH/DELETE .../comments/{commentId},PUT/DELETE .../comments/{commentId}/like. The list is flat, oldest first, each item carryingparent_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. - UI. A
ClaimCommentssection 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 fromuseMemberNames.