Skip to content

Commit 79f4478

Browse files
committed
VOIP-1271-service-agent-case-contact-notes-endpoints
Add service_agents endpoints for case contact-linking and notes, reusing existing admin-only domain logic for the agent-facing surface. - bin-openapi-manager: Add put: to service_agents/contact_cases/{id} for contact attach/detach - bin-openapi-manager: Add service_agents/contact_cases/{id}/notes and .../notes/{note_id} paths (list/create/delete), POST body restricted to text only (author derived server-side, never client-supplied) - bin-api-manager: Add ServiceAgentCaseUpdateContact, mirroring admin CaseUpdateContact with PermissionAll gate - bin-api-manager: Add ServiceAgentCaseNoteList/Create/Delete; Create derives author from the caller's own agent identity (cmcasenote.AuthorTypeAgent + a.AgentID()); Delete restricts to the note's own author via a list-then-match lookup (no single-note-get RPC exists); both reject non-agent identities (accesskey/delegate/direct) since AgentID() is meaningless for them - bin-api-manager: Add PutServiceAgentsContactCasesId and Get/Post/DeleteServiceAgentsContactCasesIdNotes* server handlers - bin-api-manager: Add servicehandler and server-level tests covering agent-permission success, tenant isolation, direct/accesskey rejection, and note-ownership enforcement (author mismatch, nil author, not found)
1 parent 937746b commit 79f4478

18 files changed

Lines changed: 2344 additions & 64 deletions

bin-api-manager/gens/openapi_redoc/api.html

Lines changed: 166 additions & 64 deletions
Large diffs are not rendered by default.

bin-api-manager/gens/openapi_redoc/openapi.json

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17949,6 +17949,182 @@
1794917949
}
1795017950
}
1795117951
},
17952+
"/service_agents/contact_cases/{id}/notes": {
17953+
"get": {
17954+
"summary": "List notes for a case",
17955+
"description": "List all notes for the case of the given ID. Notes are an internal,\nagent-facing annotation on a Case and never appear in any\ncustomer-facing webhook or response.\n",
17956+
"tags": [
17957+
"Service Agent"
17958+
],
17959+
"parameters": [
17960+
{
17961+
"name": "id",
17962+
"in": "path",
17963+
"required": true,
17964+
"schema": {
17965+
"type": "string",
17966+
"format": "uuid",
17967+
"example": "550e8400-e29b-41d4-a716-446655440000"
17968+
},
17969+
"description": "The unique identifier of the case. Returned from the `GET /service_agents/contact_cases` response."
17970+
}
17971+
],
17972+
"responses": {
17973+
"200": {
17974+
"description": "Successful response.",
17975+
"content": {
17976+
"application/json": {
17977+
"schema": {
17978+
"$ref": "#/components/schemas/ContactManagerCaseNoteListResponse"
17979+
}
17980+
}
17981+
}
17982+
},
17983+
"400": {
17984+
"$ref": "#/components/responses/BadRequest"
17985+
},
17986+
"401": {
17987+
"$ref": "#/components/responses/Unauthenticated"
17988+
},
17989+
"403": {
17990+
"$ref": "#/components/responses/PermissionDenied"
17991+
},
17992+
"404": {
17993+
"$ref": "#/components/responses/NotFound"
17994+
},
17995+
"500": {
17996+
"$ref": "#/components/responses/InternalError"
17997+
}
17998+
}
17999+
},
18000+
"post": {
18001+
"summary": "Create a note on a case",
18002+
"description": "Create a new note on the case of the given ID, authored by the\ncalling agent. The author is derived server-side from the caller's\nown agent identity -- the request only carries the note text, so an\nagent can never author a note as another agent or as the system.\nNotes are an internal, agent-facing annotation on a Case and never\nappear in any customer-facing webhook or response.\n",
18003+
"tags": [
18004+
"Service Agent"
18005+
],
18006+
"parameters": [
18007+
{
18008+
"name": "id",
18009+
"in": "path",
18010+
"required": true,
18011+
"schema": {
18012+
"type": "string",
18013+
"format": "uuid",
18014+
"example": "550e8400-e29b-41d4-a716-446655440000"
18015+
},
18016+
"description": "The unique identifier of the case. Returned from the `GET /service_agents/contact_cases` response."
18017+
}
18018+
],
18019+
"requestBody": {
18020+
"required": true,
18021+
"content": {
18022+
"application/json": {
18023+
"schema": {
18024+
"type": "object",
18025+
"required": [
18026+
"text"
18027+
],
18028+
"properties": {
18029+
"text": {
18030+
"type": "string",
18031+
"description": "The note's text content.",
18032+
"example": "Called the customer back, no answer."
18033+
}
18034+
}
18035+
}
18036+
}
18037+
}
18038+
},
18039+
"responses": {
18040+
"200": {
18041+
"description": "Case note created successfully.",
18042+
"content": {
18043+
"application/json": {
18044+
"schema": {
18045+
"$ref": "#/components/schemas/ContactManagerCaseNote"
18046+
}
18047+
}
18048+
}
18049+
},
18050+
"400": {
18051+
"$ref": "#/components/responses/BadRequest"
18052+
},
18053+
"401": {
18054+
"$ref": "#/components/responses/Unauthenticated"
18055+
},
18056+
"403": {
18057+
"$ref": "#/components/responses/PermissionDenied"
18058+
},
18059+
"404": {
18060+
"$ref": "#/components/responses/NotFound"
18061+
},
18062+
"500": {
18063+
"$ref": "#/components/responses/InternalError"
18064+
}
18065+
}
18066+
}
18067+
},
18068+
"/service_agents/contact_cases/{id}/notes/{note_id}": {
18069+
"delete": {
18070+
"summary": "Delete a case note",
18071+
"description": "Delete the note of the given note ID on the case of the given case\nID. An agent may only delete a note it authored itself; notes\nauthored by another agent or by the system cannot be deleted\nthrough this endpoint.\n",
18072+
"tags": [
18073+
"Service Agent"
18074+
],
18075+
"parameters": [
18076+
{
18077+
"name": "id",
18078+
"in": "path",
18079+
"required": true,
18080+
"schema": {
18081+
"type": "string",
18082+
"format": "uuid",
18083+
"example": "550e8400-e29b-41d4-a716-446655440000"
18084+
},
18085+
"description": "The unique identifier of the case. Returned from the `GET /service_agents/contact_cases` response."
18086+
},
18087+
{
18088+
"name": "note_id",
18089+
"in": "path",
18090+
"required": true,
18091+
"schema": {
18092+
"type": "string",
18093+
"format": "uuid",
18094+
"example": "33333333-0000-0000-0000-000000000003"
18095+
},
18096+
"description": "The unique identifier of the note. Returned from the `GET /service_agents/contact_cases/{id}/notes` response."
18097+
}
18098+
],
18099+
"responses": {
18100+
"200": {
18101+
"description": "Case note deleted successfully.",
18102+
"content": {
18103+
"application/json": {
18104+
"schema": {
18105+
"type": "object"
18106+
}
18107+
}
18108+
}
18109+
},
18110+
"400": {
18111+
"$ref": "#/components/responses/BadRequest"
18112+
},
18113+
"401": {
18114+
"$ref": "#/components/responses/Unauthenticated"
18115+
},
18116+
"403": {
18117+
"$ref": "#/components/responses/PermissionDenied"
18118+
},
18119+
"404": {
18120+
"$ref": "#/components/responses/NotFound"
18121+
},
18122+
"500": {
18123+
"$ref": "#/components/responses/InternalError"
18124+
}
18125+
}
18126+
}
18127+
},
1795218128
"/service_agents/contact_cases/{id}": {
1795318129
"get": {
1795418130
"summary": "Get detailed case info",
@@ -17996,6 +18172,73 @@
1799618172
"$ref": "#/components/responses/InternalError"
1799718173
}
1799818174
}
18175+
},
18176+
"put": {
18177+
"summary": "Attach or detach a case's contact",
18178+
"description": "Attaches the case to a specific existing Contact, or detaches it,\nvia a direct contact_id write. Send a non-empty contact_id to\nattach; send an empty string to detach. The target contact_id must\nbelong to the same customer as the case; a cross-tenant contact_id\nis rejected as not found. Every attach/detach is recorded as a\ncase_contact_attributed/case_contact_detached event, queryable via\nbin-timeline-manager's audit log.\n",
18179+
"tags": [
18180+
"Service Agent"
18181+
],
18182+
"parameters": [
18183+
{
18184+
"name": "id",
18185+
"in": "path",
18186+
"required": true,
18187+
"schema": {
18188+
"type": "string",
18189+
"format": "uuid",
18190+
"example": "550e8400-e29b-41d4-a716-446655440000"
18191+
},
18192+
"description": "The unique identifier of the case. Returned from the `GET /service_agents/contact_cases` response."
18193+
}
18194+
],
18195+
"requestBody": {
18196+
"required": true,
18197+
"content": {
18198+
"application/json": {
18199+
"schema": {
18200+
"type": "object",
18201+
"required": [
18202+
"contact_id"
18203+
],
18204+
"properties": {
18205+
"contact_id": {
18206+
"type": "string",
18207+
"description": "The contact to attach. Empty string detaches. Deliberately NOT format: uuid -- the internal RPC-layer uuid.UUID field cannot unmarshal a literal empty string, so this HTTP-layer field stays a plain string and the \"\" -> uuid.Nil conversion happens explicitly in the handler.",
18208+
"example": "660e8400-e29b-41d4-a716-446655440001"
18209+
}
18210+
}
18211+
}
18212+
}
18213+
}
18214+
},
18215+
"responses": {
18216+
"200": {
18217+
"description": "The case after the contact attach/detach.",
18218+
"content": {
18219+
"application/json": {
18220+
"schema": {
18221+
"$ref": "#/components/schemas/ContactManagerCase"
18222+
}
18223+
}
18224+
}
18225+
},
18226+
"400": {
18227+
"$ref": "#/components/responses/BadRequest"
18228+
},
18229+
"401": {
18230+
"$ref": "#/components/responses/Unauthenticated"
18231+
},
18232+
"403": {
18233+
"$ref": "#/components/responses/PermissionDenied"
18234+
},
18235+
"404": {
18236+
"$ref": "#/components/responses/NotFound"
18237+
},
18238+
"500": {
18239+
"$ref": "#/components/responses/InternalError"
18240+
}
18241+
}
1799918242
}
1800018243
},
1800118244
"/service_agents/contact_cases": {

0 commit comments

Comments
 (0)