[CSM Portal] one canonical person reference per case field, nested product reference, and ABT team-picker family filter - #1342
Conversation
Case responses carried each person twice: createdBy alongside createdByUser,
assignedEngineer alongside assignedEngineerUser, built from the same upstream
data in the same block. Collapse each pair into a single field holding the
canonical UserReference {id, email, name}, on case detail, case search, case
comments, case attachments, case activity entries and generic comments, across
both data sources. UserRef and CommentUserRef go with them; AssignedEngineerRef
stays, still used by acknowledgedBy and by the update response's assignedTo.
The product catalogue reference now hangs off the deployed product instead of
sitting beside it as a second top-level field. They are different records with
different ids, and the flat shape gave no clue which was which. A case can name
a catalogue product with no deployed instance of it, so DeployedProductRef.id
and displayName are nullable and the reference is still returned in that case,
carrying the product alone.
relatedCase silently dropped its record kind while the adjacent parentCase
mapped it. Both references can point at something other than a case, so a
consumer was left guessing on one of them.
The Go entity-service (4cf093d) collapsed createdBy/createdByUser and assignedEngineer/assignedEngineerUser into one canonical UserReference each, and renested the product catalogue entry under deployedProduct.product instead of a sibling top-level product field. Update the webapp's backend types and every call site (case detail, search, announcements, comments, attachments, activity feed) to read the new shape, including the comment/ activity author-name fallbacks that depended on the old flat fields.
Mirrors entity-service commit 4cf093d in the BFF's own openapi.yaml. The BFF forwards these responses as raw bytes (no Go decode of these fields exists), so this is documentation only: createdBy/assignedEngineer become single UserReference objects on case detail, case search, comments, attachments, activities, and conversation messages; deployedProduct nests the product catalogue entry and is itself nullable at the field level.
📝 WalkthroughWalkthroughThe PR standardizes actor data on nullable ChangesCanonical API and domain contracts
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…family-filter commit
… family POST /teams/search accepts a case-insensitive filters.family; SearchTeams excludes any team whose Family doesn't exactly match, including teams with no family at all. AbtDashboardHeader now derives the family to request from the current dashboard's type (cre -> cre-abt, sre -> sre-abt) via the new abtFamilyForDashboardType, so a cre-type dashboard's picker offers only cre-abt teams instead of every team in the registry (previously every non-ABT cre team, e.g. "Americas CS Team - Integraion", showed up alongside the real ABT teams). CsmDashboardPage's own useTeams call for resolving the signed-in user's own team into a groupId stays unfiltered on purpose: that user's team can be outside the current dashboard's family (a non-ABT cre team member viewing a cre dashboard), so it can no longer share a query key/cache entry with the header's now family-scoped picker query.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
entity-service/internal/repository/case_repo.go (1)
134-134: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPopulate
namefor the assignee, created comment, and search creator instead of hardcoding empty strings.Three
UserReferenceconstructions in this file leaveNameas an empty string, even thoughUserReferenceis documented as "nameare populated everywhere" (entity.go, entity-service/openapi.yaml). An empty string here is an absence sentinel, which the domain guideline explicitly disallows.
GetCaseByIDassignee (lines 134, 159, 184-187):aeisLEFT JOIN users ae ON ae.id = c.assigned_engineer— the sameuserstable already joined and queried foru.emailon the creator. The comment "this data source stores no email on the assignee join" does not hold:ae.emailis simply not selected.CreateCaseComment(lines 207-226): theINSERT ... RETURNINGclause only returns the author's id, soNameare hardcoded to"".SearchCases(lines 543-548, 573-575): the query selects onlyu.emailfor the creator (no name concatenation, unlike the assignee'sae.first_name || ' ' || ae.last_name), soNamestays"".Consumers rendering the assignee, a freshly created comment's author, or a search result's creator name will see a blank field even though the schema promises it is always populated.
🛠️ Proposed fixes
var ( aeID, aeName *string + aeEmail *string pcID, pcNum *string rcID, rcNum *stringa.id, a.name, a.tier, - ae.id, ae.first_name || ' ' || ae.last_name, + ae.id, ae.first_name || ' ' || ae.last_name, ae.email, pc.id, pc.number,- &aeID, &aeName, + &aeID, &aeName, &aeEmail, &pcID, &pcNum,if aeID != nil { - // This data source stores no email on the assignee join, only id and name. - cv.AssignedEngineer = domain.NewUserReference(*aeID, "", *aeName) + cv.AssignedEngineer = domain.NewUserReference(*aeID, *aeEmail, *aeName) }- const query = ` - INSERT INTO case_comments (case_id, type, content, created_by) - VALUES ($1, $2::comment_type_enum, $3, $4) - RETURNING id, case_id, type, content, created_by, created_at` + const query = ` + WITH inserted AS ( + INSERT INTO case_comments (case_id, type, content, created_by) + VALUES ($1, $2::comment_type_enum, $3, $4) + RETURNING id, case_id, type, content, created_by, created_at + ) + SELECT i.id, i.case_id, i.type, i.content, i.created_by, u.email, + TRIM(u.first_name || ' ' || u.last_name), i.created_at + FROM inserted i JOIN users u ON u.id = i.created_by` var c domain.CaseComment - var createdByID string + var createdByID, createdByEmail, createdByName string err := r.db.QueryRow(ctx, query, req.CaseID, string(req.Type), req.Content, req.CreatedBy, - ).Scan(&c.ID, &c.CaseID, &c.Type, &c.Content, &createdByID, &c.CreatedOn) + ).Scan(&c.ID, &c.CaseID, &c.Type, &c.Content, &createdByID, &createdByEmail, &createdByName, &c.CreatedOn)- // The insert returns only the author's id; email and display name would need - // a further join, so the reference carries the id alone. - c.CreatedBy = domain.NewUserReference(createdByID, "", "") + c.CreatedBy = domain.NewUserReference(createdByID, createdByEmail, createdByName)u.email, + TRIM(u.first_name || ' ' || u.last_name), p.id, p.name,- var creatorEmail string + var creatorEmail, creatorName string if err := rows.Scan( &cv.ID, &cv.Number, &cv.InternalID, &caseType, &subject, &description, &severity, &issueType, &cv.State, &engagementType, &workState, &createdAt, &updatedAt, - &creatorEmail, + &creatorEmail, &creatorName,- cv.CreatedBy = domain.NewUserReference("", creatorEmail, "") + cv.CreatedBy = domain.NewUserReference("", creatorEmail, creatorName)Also applies to: 159-159, 184-187, 207-226, 543-548, 573-575
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@entity-service/internal/repository/case_repo.go` at line 134, Populate UserReference email and name in GetCaseByID, CreateCaseComment, and SearchCases instead of using empty strings: select ae.email alongside the assignee fields, return the created comment author’s email and name from the INSERT RETURNING clause, and select the creator’s concatenated first_name/last_name in SearchCases. Wire these returned values into each UserReference construction while preserving existing creator and assignee mappings.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@entity-service/internal/repository/case_repo.go`:
- Line 134: Populate UserReference email and name in GetCaseByID,
CreateCaseComment, and SearchCases instead of using empty strings: select
ae.email alongside the assignee fields, return the created comment author’s
email and name from the INSERT RETURNING clause, and select the creator’s
concatenated first_name/last_name in SearchCases. Wire these returned values
into each UserReference construction while preserving existing creator and
assignee mappings.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a8da522c-f097-4ffc-875f-80cd60116bcf
📒 Files selected for processing (15)
apps/csm-portal/backend/openapi.yamlapps/csm-portal/webapp/src/api/backend/mappers.test.tsapps/csm-portal/webapp/src/api/backend/mappers.tsapps/csm-portal/webapp/src/api/backend/types.tsapps/csm-portal/webapp/src/features/csm-announcements/api/useSearchAnnouncements.tsapps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseActivities.test.tsapps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseActivities.tsapps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.tsentity-service/internal/domain/entity.goentity-service/internal/repository/case_repo.goentity-service/internal/service/sn_case_service.goentity-service/internal/service/sn_case_service_test.goentity-service/internal/service/sn_comment_service.goentity-service/internal/service/sn_user_reference_test.goentity-service/openapi.yaml
Purpose
Two independent CSM portal improvements, stacked in this PR:
UserReferenceshape was added alongside older ones, and one existing field (relatedCase) was missing data a sibling field (parentCase) already carried.No related issue for either.
Goals
Case response cleanup:
createdBy,assignedEngineer) instead of a field plus a*Usersibling carrying the same data, across case detail, case search, comments, attachments, activities, and conversation messages.deployedProduct.product), instead of sitting beside it as a same-looking but distinct top-level field.relatedCasenow carries atypediscriminator (case/incident/change_request/problem), matching whatparentCasealready provides, so a consumer can route to the correct record kind.ABT team-picker family filter:
POST /teams/searchaccepts an optionalfilters.familyso a caller can ask for only the teams belonging to a given family.Approach
Case response cleanup, bottom-up:
UserReference {id, email, name}per site across both backing data sources; nested the product catalogue entry under the deployed product, keepingid/displayNamenullable so a case that names a catalogue product with no deployed instance of it still returns the product rather than dropping it; added the missing type mapping onrelatedCase.openapi.yamlupdated to match, with a new test pinning the "product without a deployed product" edge case.openapi.yamldoc updated to match, including the conversation-messages endpoint which shares the same underlying comment shape.This is a breaking response-shape change to an internal API with a single consumer (the CSM webapp, updated in the same PR), so it ships as a straight replacement rather than an additive migration.
ABT team-picker family filter:
filters.familyonPOST /teams/searchdoes a case-insensitive exact match against each team's configured family, excluding teams with no family at all when a family filter is given.AbtDashboardHeaderderives the family from the current dashboard's type via a newabtFamilyForDashboardTypehelper (cre → cre-abt, sre → sre-abt) and passes it throughuseTeams. The dashboard page's own separateuseTeamscall — used only to resolve the signed-in user's own team into a group id — is deliberately left unfiltered, since a user's real team can be outside the dashboard's own family; it no longer shares a query key with the header's now-scoped query.openapi.yamlupdated for the new request/response fields.User stories
N/A — both are internal API/UX cleanups, not distinct user stories.
Release note
Case responses now return one person reference per role (creator, assignee) and nest the product catalogue reference under the deployed product; related-case references now include their record type. The ABT dashboard's team picker now shows only the teams that belong to that dashboard's own family, instead of every team in the registry.
Documentation
N/A — both changes are documented entirely in this PR's
openapi.yamlupdates; no external product docs describe either internal API/behavior.Training
N/A — no training content covers either change.
Certification
N/A — no certification exam covers either change.
Marketing
N/A — internal API/UX cleanups, no user-facing feature to promote.
Automation tests
Security checks
go vetandeslintboth ran clean insteadSamples
N/A
Related PRs
None.
Migrations (if applicable)
N/A — no data migration. The case-response change is a response-shape change; the team-family filter is an additive request/response field.
Test environment
Local development environment (macOS), Go 1.26+, Node/npm per
apps/csm-portal/webapp/package.json.Learning
N/A