diff --git a/apps/csm-portal/backend/openapi.yaml b/apps/csm-portal/backend/openapi.yaml index b7b34519bc..860df20bba 100644 --- a/apps/csm-portal/backend/openapi.yaml +++ b/apps/csm-portal/backend/openapi.yaml @@ -3384,21 +3384,30 @@ components: type: string description: Root-cause category for a closed or solution-proposed case. enum: - - USER_MISUNDERSTANDING_CONCEPTS - - USER_MISUNDERSTANDING_DOCUMENTATION - - USER_NOT_FOLLOWING_DOCUMENTATION - - USER_MISTAKE - - SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE - - SOLUTION_PROBLEMATIC_CODE - - APPLICATION_BUG - - APPLICATION_MISLEADING_UX_UI - - APPLICATION_LIMITATION - - APPLICATION_MISSING_FEATURE - - APPLICATION_DOCUMENTATION_GAP - - APPLICATION_DOCUMENTATION_ERROR - - INFRASTRUCTURE_CUSTOMERS_SIDE - - INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH - - INFRASTRUCTURE_SAAS_SIDE_OTHER + - SOLUTION_ARCHITECTURE + - DEPLOYMENT_ARCHITECTURE + - USER_ERROR_CONFIGURATION + - USER_ERROR_PRODUCT_CONCEPT + - USER_ERROR_RUNTIME + - USER_ERROR_RECOMMENDATION_BEST_PRACTICES + - CUSTOMIZATION_LIMITATION + - CUSTOMIZATION_BUG + - DOCUMENTATION_GAP + - DOCUMENTATION_ERROR + - PRODUCT_LIMITATION + - PRODUCT_BUG + - PRODUCT_REGRESSION + - PRODUCT_MIGRATION + - INFRASTRUCTURE_DATABASE + - INFRASTRUCTURE_OS + - INFRASTRUCTURE_NETWORK + - INFRASTRUCTURE_JDK + - INFRASTRUCTURE_LDAP + - INFRASTRUCTURE_LOAD_BALANCER + - INFRASTRUCTURE_IAAS + - INFRASTRUCTURE_EXTERNAL_PRODUCT + - INFRASTRUCTURE_PROXY + - INFRASTRUCTURE_OTHER - UNKNOWN CaseLabelRef: diff --git a/apps/csm-portal/webapp/src/api/backend/types.ts b/apps/csm-portal/webapp/src/api/backend/types.ts index d1ccda94d1..952b6fbed5 100644 --- a/apps/csm-portal/webapp/src/api/backend/types.ts +++ b/apps/csm-portal/webapp/src/api/backend/types.ts @@ -114,21 +114,30 @@ export type BeCaseResolutionCode = /** Root-cause category for a closed or solution-proposed case. Same gating as {@link BeCaseResolutionCode}. */ export type BeCaseCause = - | "USER_MISUNDERSTANDING_CONCEPTS" - | "USER_MISUNDERSTANDING_DOCUMENTATION" - | "USER_NOT_FOLLOWING_DOCUMENTATION" - | "USER_MISTAKE" - | "SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE" - | "SOLUTION_PROBLEMATIC_CODE" - | "APPLICATION_BUG" - | "APPLICATION_MISLEADING_UX_UI" - | "APPLICATION_LIMITATION" - | "APPLICATION_MISSING_FEATURE" - | "APPLICATION_DOCUMENTATION_GAP" - | "APPLICATION_DOCUMENTATION_ERROR" - | "INFRASTRUCTURE_CUSTOMERS_SIDE" - | "INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH" - | "INFRASTRUCTURE_SAAS_SIDE_OTHER" + | "SOLUTION_ARCHITECTURE" + | "DEPLOYMENT_ARCHITECTURE" + | "USER_ERROR_CONFIGURATION" + | "USER_ERROR_PRODUCT_CONCEPT" + | "USER_ERROR_RUNTIME" + | "USER_ERROR_RECOMMENDATION_BEST_PRACTICES" + | "CUSTOMIZATION_LIMITATION" + | "CUSTOMIZATION_BUG" + | "DOCUMENTATION_GAP" + | "DOCUMENTATION_ERROR" + | "PRODUCT_LIMITATION" + | "PRODUCT_BUG" + | "PRODUCT_REGRESSION" + | "PRODUCT_MIGRATION" + | "INFRASTRUCTURE_DATABASE" + | "INFRASTRUCTURE_OS" + | "INFRASTRUCTURE_NETWORK" + | "INFRASTRUCTURE_JDK" + | "INFRASTRUCTURE_LDAP" + | "INFRASTRUCTURE_LOAD_BALANCER" + | "INFRASTRUCTURE_IAAS" + | "INFRASTRUCTURE_EXTERNAL_PRODUCT" + | "INFRASTRUCTURE_PROXY" + | "INFRASTRUCTURE_OTHER" | "UNKNOWN"; export type BeCaseSortField = "createdOn" | "updatedOn" | "severity" | "state"; @@ -255,6 +264,14 @@ export interface BeCaseView { createdOn?: string; updatedOn?: string; closedOn?: string | null; + /** Timestamp when the case was resolved. Populated for resolved/closed cases; null otherwise. */ + resolvedOn?: string | null; + /** Resolution code from a prior close/propose-solution. Populated for resolved/closed cases; null otherwise. */ + resolutionCode?: BeCaseResolutionCode | null; + /** Root-cause category from a prior close/propose-solution. Populated for resolved/closed cases; null otherwise. */ + cause?: BeCaseCause | null; + /** Free-text resolution/close notes from a prior close/propose-solution. Populated for resolved/closed cases; null otherwise. */ + resolutionNotes?: string | null; } export interface BeCaseCreatePayload { diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts index 2251f7f4ba..706a2fa6fa 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts @@ -113,6 +113,14 @@ function detailFromBeCase( audit: [], attachments: [], isWatching: false, + resolution: + c.resolutionCode || c.cause || c.resolutionNotes + ? { + resolutionCode: c.resolutionCode ?? undefined, + cause: c.cause ?? undefined, + notes: c.resolutionNotes ?? undefined, + } + : undefined, }; } diff --git a/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.test.tsx b/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.test.tsx index 0964e0b07f..c70ce71d68 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.test.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.test.tsx @@ -38,10 +38,10 @@ describe("ResolutionDialog", () => { ); expect(screen.getByRole("button", { name: /close case/i })).toBeDisabled(); - chooseOption("resolution-code-label", /solved fixed by support guidance provided/i); + chooseOption("resolution-code-label", /solved.*fixed by support\/guidance provided/i); expect(screen.getByRole("button", { name: /close case/i })).toBeDisabled(); - chooseOption("case-cause-label", /application bug/i); + chooseOption("case-cause-label", /product\/bug/i); expect(screen.getByRole("button", { name: /close case/i })).not.toBeDisabled(); }); @@ -68,6 +68,36 @@ describe("ResolutionDialog", () => { }); }); + it("prefills fields from a prior resolution and submits unchanged", () => { + const onSubmit = vi.fn(); + render( + {}} + onSubmit={onSubmit} + />, + ); + expect( + screen.getByRole("button", { name: /propose solution/i }), + ).not.toBeDisabled(); + expect(screen.getByLabelText(/close notes/i)).toHaveValue( + "Root-caused and verified with the customer.", + ); + + fireEvent.click(screen.getByRole("button", { name: /propose solution/i })); + expect(onSubmit).toHaveBeenCalledWith({ + resolutionCode: "SOLVED_BY_CUSTOMER", + cause: "UNKNOWN", + closeNotes: "Root-caused and verified with the customer.", + }); + }); + it("calls onClose when cancelled", () => { const onClose = vi.fn(); render( diff --git a/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.tsx b/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.tsx index 8088a073ae..bdd867d496 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/components/ResolutionDialog.tsx @@ -31,14 +31,25 @@ import { useState, type JSX } from "react"; import type { BeCaseCause, BeCaseResolutionCode } from "@api/backend/types"; import { CASE_CAUSES, + CASE_CAUSE_LABELS, RESOLUTION_CODES, - humanizeResolutionEnum, + RESOLUTION_CODE_LABELS, } from "@features/csm-cases/utils/caseResolution"; interface ResolutionDialogProps { /** Which lifecycle transition this dialog is confirming. */ kind: "close" | "propose_solution"; isSubmitting: boolean; + /** + * Values from a prior close/propose-solution on this case, when the + * backend has them (e.g. a case reopened after a previous resolution). + * Prefills the fields instead of opening blank. + */ + initial?: { + resolutionCode?: BeCaseResolutionCode; + cause?: BeCaseCause; + closeNotes?: string; + }; onClose: () => void; onSubmit: (fields: { resolutionCode: BeCaseResolutionCode; @@ -75,12 +86,15 @@ const COPY: Record< export default function ResolutionDialog({ kind, isSubmitting, + initial, onClose, onSubmit, }: ResolutionDialogProps): JSX.Element { - const [resolutionCode, setResolutionCode] = useState(""); - const [cause, setCause] = useState(""); - const [closeNotes, setCloseNotes] = useState(""); + const [resolutionCode, setResolutionCode] = useState( + initial?.resolutionCode ?? "", + ); + const [cause, setCause] = useState(initial?.cause ?? ""); + const [closeNotes, setCloseNotes] = useState(initial?.closeNotes ?? ""); const copy = COPY[kind]; const canSubmit = !!resolutionCode && !!cause && !isSubmitting; @@ -107,7 +121,7 @@ export default function ResolutionDialog({ > {RESOLUTION_CODES.map((code) => ( - {humanizeResolutionEnum(code)} + {RESOLUTION_CODE_LABELS[code]} ))} @@ -123,7 +137,7 @@ export default function ResolutionDialog({ > {CASE_CAUSES.map((c) => ( - {humanizeResolutionEnum(c)} + {CASE_CAUSE_LABELS[c]} ))} diff --git a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx index dc432c0ebd..5dd536ccbd 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx @@ -1563,6 +1563,15 @@ export default function CsmCaseDetailPage(): JSX.Element { setResolutionDialog(null)} onSubmit={onResolutionSubmit} /> diff --git a/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts b/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts index d2824399d0..029e6a3a9c 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts @@ -20,7 +20,12 @@ import type { Severity, SlaClockType, } from "@features/csm-dashboard/types/abtDashboard"; -import type { BeCaseIssueType, BeCaseType } from "@api/backend/types"; +import type { + BeCaseCause, + BeCaseIssueType, + BeCaseResolutionCode, + BeCaseType, +} from "@api/backend/types"; export interface CsmCaseRow { /** @@ -413,4 +418,13 @@ export interface CsmCaseDetail extends CsmCaseRow { attachments: CaseAttachment[]; /** Whether the current user is watching this case (controls Watch toggle). */ isWatching: boolean; + /** + * Post Resolution Activity from a prior close/propose-solution, when set — + * used to prefill {@link ResolutionDialog} instead of reopening it blank. + */ + resolution?: { + resolutionCode?: BeCaseResolutionCode; + cause?: BeCaseCause; + notes?: string; + }; } diff --git a/apps/csm-portal/webapp/src/features/csm-cases/utils/caseResolution.ts b/apps/csm-portal/webapp/src/features/csm-cases/utils/caseResolution.ts index 9b409421c4..3ffc831ae9 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/utils/caseResolution.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/utils/caseResolution.ts @@ -40,33 +40,99 @@ export const RESOLUTION_CODES: BeCaseResolutionCode[] = [ /** All backend root-cause categories, in the order declared in openapi.yaml. */ export const CASE_CAUSES: BeCaseCause[] = [ - "USER_MISUNDERSTANDING_CONCEPTS", - "USER_MISUNDERSTANDING_DOCUMENTATION", - "USER_NOT_FOLLOWING_DOCUMENTATION", - "USER_MISTAKE", - "SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE", - "SOLUTION_PROBLEMATIC_CODE", - "APPLICATION_BUG", - "APPLICATION_MISLEADING_UX_UI", - "APPLICATION_LIMITATION", - "APPLICATION_MISSING_FEATURE", - "APPLICATION_DOCUMENTATION_GAP", - "APPLICATION_DOCUMENTATION_ERROR", - "INFRASTRUCTURE_CUSTOMERS_SIDE", - "INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH", - "INFRASTRUCTURE_SAAS_SIDE_OTHER", + "SOLUTION_ARCHITECTURE", + "DEPLOYMENT_ARCHITECTURE", + "USER_ERROR_CONFIGURATION", + "USER_ERROR_PRODUCT_CONCEPT", + "USER_ERROR_RUNTIME", + "USER_ERROR_RECOMMENDATION_BEST_PRACTICES", + "CUSTOMIZATION_LIMITATION", + "CUSTOMIZATION_BUG", + "DOCUMENTATION_GAP", + "DOCUMENTATION_ERROR", + "PRODUCT_LIMITATION", + "PRODUCT_BUG", + "PRODUCT_REGRESSION", + "PRODUCT_MIGRATION", + "INFRASTRUCTURE_DATABASE", + "INFRASTRUCTURE_OS", + "INFRASTRUCTURE_NETWORK", + "INFRASTRUCTURE_JDK", + "INFRASTRUCTURE_LDAP", + "INFRASTRUCTURE_LOAD_BALANCER", + "INFRASTRUCTURE_IAAS", + "INFRASTRUCTURE_EXTERNAL_PRODUCT", + "INFRASTRUCTURE_PROXY", + "INFRASTRUCTURE_OTHER", "UNKNOWN", ]; /** - * Title-cases an UPPER_SNAKE_CASE backend enum value for display, e.g. - * "SOLVED_BY_CUSTOMER" -> "Solved by customer". Mirrors `humanizeState` - * (abtDashboard.ts) so an enum value added on the backend still renders - * readably with no frontend change required. + * Exact display text for each resolution code, matching the backing data + * source's picklist verbatim (punctuation, capitalization, and duplicate + * wording included — e.g. `CONSIDERED_FOR_ROADMAP` and + * `CONSIDERED_FOR_ROADMAP_ALT` both read "Considered for roadmap" because + * that's what the source shows for both underlying choice-list entries). + * Mechanical humanization can't reproduce this text, so it's listed + * explicitly rather than derived from the enum token. */ -export function humanizeResolutionEnum(value: string): string { - const words = value.toLowerCase().split("_").filter(Boolean); - return words - .map((w, i) => (i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w)) - .join(" "); -} +export const RESOLUTION_CODE_LABELS: Record = { + SOLVED_FIXED_BY_SUPPORT_GUIDANCE_PROVIDED: + "Solved – Fixed by support/guidance provided", + SOLVED_FIXED_BY_CLOSING_RELATED_INCIDENT: + "Solved – Fixed by closing related Incident", + SOLVED_FIXED_BY_CLOSING_RELATED_RD_TICKET: + "Solved - Fixed by closing related R&D ticket", + SOLVED_WORKAROUND_PROVIDED: "Solved – Workaround provided", + SOLVED_BY_CUSTOMER: "Solved by customer", + CONSIDERED_FOR_ROADMAP: "Considered for roadmap", + INCONCLUSIVE_OUT_OF_SCOPE: "Inconclusive – Out of scope", + INCONCLUSIVE_CANNOT_REPRODUCE: "Inconclusive – Cannot reproduce", + INCONCLUSIVE_NO_WORKAROUND: "Inconclusive – No workaround", + DUPLICATE_ISSUE: "Duplicate issue", + VOIDED_CANCELED: "Voided/Canceled", + ON_HOLD: "On Hold", + CONSIDERED_FOR_ROADMAP_ALT: "Considered for roadmap", + SOLVED_FIXED_THE_ISSUE: "Solved - Fixed the issue", + SOLVED_WORKAROUND_PROVIDED_ALT: "Solved - Workaround provided", + SOLVED_BY_CONTRIBUTOR: "Solved by Contributor", + SOLVED_BY_NOVERA: "Solved : By Novera", + ABRUPTLY_CLOSED_DUE_TO_NON_RESPONSIVENESS: + "Abruptly closed due to non responsiveness through auto closure", +}; + +/** + * Exact display text for each root-cause category, matching the backing + * data source's "cause" picklist verbatim. Listed explicitly rather than + * derived from the enum token — mechanical humanization can't reproduce the + * source's `/`-separated wording or acronym casing (`IAAS`, `JDK`, `LDAP`, + * `OS`). + */ +export const CASE_CAUSE_LABELS: Record = { + SOLUTION_ARCHITECTURE: "Solution Architecture", + DEPLOYMENT_ARCHITECTURE: "Deployment Architecture", + USER_ERROR_CONFIGURATION: "User Error/Configuration", + USER_ERROR_PRODUCT_CONCEPT: "User Error/Product Concept", + USER_ERROR_RUNTIME: "User Error/Runtime", + USER_ERROR_RECOMMENDATION_BEST_PRACTICES: + "User Error/Recommandation/Best practices", + CUSTOMIZATION_LIMITATION: "Customization/Limitation", + CUSTOMIZATION_BUG: "Customization/Bug", + DOCUMENTATION_GAP: "Documentation/Gap", + DOCUMENTATION_ERROR: "Documentation/Error", + PRODUCT_LIMITATION: "Product/Limitation", + PRODUCT_BUG: "Product/Bug", + PRODUCT_REGRESSION: "Product/Regression", + PRODUCT_MIGRATION: "Product/Migration", + INFRASTRUCTURE_DATABASE: "Infrastructure/Database", + INFRASTRUCTURE_OS: "Infrastructure/OS", + INFRASTRUCTURE_NETWORK: "Infrastructure/Network", + INFRASTRUCTURE_JDK: "Infrastructure/JDK", + INFRASTRUCTURE_LDAP: "Infrastructure/LDAP", + INFRASTRUCTURE_LOAD_BALANCER: "Infrastructure/Load Balancer", + INFRASTRUCTURE_IAAS: "Infrastructure/IAAS", + INFRASTRUCTURE_EXTERNAL_PRODUCT: "Infrastructure/External Product", + INFRASTRUCTURE_PROXY: "Infrastructure/Proxy", + INFRASTRUCTURE_OTHER: "Infrastructure/Other", + UNKNOWN: "Unknown", +}; diff --git a/entity-service/internal/domain/entity.go b/entity-service/internal/domain/entity.go index e1b4216b83..757eae3843 100644 --- a/entity-service/internal/domain/entity.go +++ b/entity-service/internal/domain/entity.go @@ -773,26 +773,38 @@ const ( CaseResolutionCodeAbruptlyClosedDueToNonResponsiveness CaseResolutionCode = "ABRUPTLY_CLOSED_DUE_TO_NON_RESPONSIVENESS" ) -// CaseCause enumerates the root-cause categories for a resolved case. +// CaseCause enumerates the root-cause categories for a resolved case, one per +// entry in the backing data source's real "cause" choice list (verified +// against the live picklist; the previous 16-value set was a fabricated +// taxonomy that didn't correspond to any real choice in that list). type CaseCause string const ( - CaseCauseUserMisunderstandingConcepts CaseCause = "USER_MISUNDERSTANDING_CONCEPTS" - CaseCauseUserMisunderstandingDocumentation CaseCause = "USER_MISUNDERSTANDING_DOCUMENTATION" - CaseCauseUserNotFollowingDocumentation CaseCause = "USER_NOT_FOLLOWING_DOCUMENTATION" - CaseCauseUserMistake CaseCause = "USER_MISTAKE" - CaseCauseSolutionProblematicArchitecture CaseCause = "SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE" - CaseCauseSolutionProblematicCode CaseCause = "SOLUTION_PROBLEMATIC_CODE" - CaseCauseApplicationBug CaseCause = "APPLICATION_BUG" - CaseCauseApplicationMisleadingUXUI CaseCause = "APPLICATION_MISLEADING_UX_UI" - CaseCauseApplicationLimitation CaseCause = "APPLICATION_LIMITATION" - CaseCauseApplicationMissingFeature CaseCause = "APPLICATION_MISSING_FEATURE" - CaseCauseApplicationDocumentationGap CaseCause = "APPLICATION_DOCUMENTATION_GAP" - CaseCauseApplicationDocumentationError CaseCause = "APPLICATION_DOCUMENTATION_ERROR" - CaseCauseInfrastructureCustomerSide CaseCause = "INFRASTRUCTURE_CUSTOMERS_SIDE" - CaseCauseInfrastructureSaaSNotEnough CaseCause = "INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH" - CaseCauseInfrastructureSaaSother CaseCause = "INFRASTRUCTURE_SAAS_SIDE_OTHER" - CaseCauseUnknown CaseCause = "UNKNOWN" + CaseCauseSolutionArchitecture CaseCause = "SOLUTION_ARCHITECTURE" + CaseCauseDeploymentArchitecture CaseCause = "DEPLOYMENT_ARCHITECTURE" + CaseCauseUserErrorConfiguration CaseCause = "USER_ERROR_CONFIGURATION" + CaseCauseUserErrorProductConcept CaseCause = "USER_ERROR_PRODUCT_CONCEPT" + CaseCauseUserErrorRuntime CaseCause = "USER_ERROR_RUNTIME" + CaseCauseUserErrorRecommendation CaseCause = "USER_ERROR_RECOMMENDATION_BEST_PRACTICES" + CaseCauseCustomizationLimitation CaseCause = "CUSTOMIZATION_LIMITATION" + CaseCauseCustomizationBug CaseCause = "CUSTOMIZATION_BUG" + CaseCauseDocumentationGap CaseCause = "DOCUMENTATION_GAP" + CaseCauseDocumentationError CaseCause = "DOCUMENTATION_ERROR" + CaseCauseProductLimitation CaseCause = "PRODUCT_LIMITATION" + CaseCauseProductBug CaseCause = "PRODUCT_BUG" + CaseCauseProductRegression CaseCause = "PRODUCT_REGRESSION" + CaseCauseProductMigration CaseCause = "PRODUCT_MIGRATION" + CaseCauseInfrastructureDatabase CaseCause = "INFRASTRUCTURE_DATABASE" + CaseCauseInfrastructureOS CaseCause = "INFRASTRUCTURE_OS" + CaseCauseInfrastructureNetwork CaseCause = "INFRASTRUCTURE_NETWORK" + CaseCauseInfrastructureJDK CaseCause = "INFRASTRUCTURE_JDK" + CaseCauseInfrastructureLDAP CaseCause = "INFRASTRUCTURE_LDAP" + CaseCauseInfrastructureLoadBalancer CaseCause = "INFRASTRUCTURE_LOAD_BALANCER" + CaseCauseInfrastructureIAAS CaseCause = "INFRASTRUCTURE_IAAS" + CaseCauseInfrastructureExternalProduct CaseCause = "INFRASTRUCTURE_EXTERNAL_PRODUCT" + CaseCauseInfrastructureProxy CaseCause = "INFRASTRUCTURE_PROXY" + CaseCauseInfrastructureOther CaseCause = "INFRASTRUCTURE_OTHER" + CaseCauseUnknown CaseCause = "UNKNOWN" ) // EngagementType classifies the type of an engagement case. diff --git a/entity-service/internal/service/sn_case_service.go b/entity-service/internal/service/sn_case_service.go index e3966fb595..5c5055f22e 100644 --- a/entity-service/internal/service/sn_case_service.go +++ b/entity-service/internal/service/sn_case_service.go @@ -21,6 +21,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "strconv" "strings" "time" @@ -583,7 +584,7 @@ func (s *snCaseService) GetCaseByID(ctx context.Context, id string) (domain.Case } } if c.Cause != nil { - if cause, ok := snCauseLabelToEnum[c.Cause.Label]; ok { + if cause, ok := snCauseByID[c.Cause.ID]; ok { cv.Cause = &cause } } @@ -832,35 +833,53 @@ var snResolutionCodeByID = func() map[string]domain.CaseResolutionCode { return m }() -// snCauseLabelToEnum maps ServiceNow cause label strings to domain CaseCause enums. -var snCauseLabelToEnum map[string]domain.CaseCause - -// snCauseLabel maps domain CaseCause enums to the ServiceNow cause label strings. -var snCauseLabel = map[domain.CaseCause]string{ - domain.CaseCauseUserMisunderstandingConcepts: "User - Misunderstanding concepts", - domain.CaseCauseUserMisunderstandingDocumentation: "User - Misunderstanding documentation", - domain.CaseCauseUserNotFollowingDocumentation: "User - Not following documentation", - domain.CaseCauseUserMistake: "User - Mistake", - domain.CaseCauseSolutionProblematicArchitecture: "Solution - Problematic solution architecture", - domain.CaseCauseSolutionProblematicCode: "Solution - Problematic code", - domain.CaseCauseApplicationBug: "Application - Bug", - domain.CaseCauseApplicationMisleadingUXUI: "Application - Misleading UX / UI", - domain.CaseCauseApplicationLimitation: "Application - Limitation", - domain.CaseCauseApplicationMissingFeature: "Application - Missing feature", - domain.CaseCauseApplicationDocumentationGap: "Application - Documentation gap", - domain.CaseCauseApplicationDocumentationError: "Application - Documentation error", - domain.CaseCauseInfrastructureCustomerSide: "Infrastructure - Customer's side", - domain.CaseCauseInfrastructureSaaSNotEnough: "Infrastructure - SaaS side - Not enough ...", - domain.CaseCauseInfrastructureSaaSother: "Infrastructure - SaaS side - Other", - domain.CaseCauseUnknown: "Unknown", -} - -func init() { - snCauseLabelToEnum = make(map[string]domain.CaseCause, len(snCauseLabel)) - for k, v := range snCauseLabel { - snCauseLabelToEnum[v] = k +// snCauseKey maps domain CaseCause enums to the ServiceNow integer choice +// values for sn_customerservice_case.cause on the PROD tenant (wso2), +// verified via sys_choice. Unlike resolution codes' scattered keys, prod's +// cause choice list happens to number sequentially 1-25 in picklist order. +// +// The DEV tenant (wso2sndev) configures this same field with the label text +// as its stored value instead of an integer — a real cross-tenant +// inconsistency, not a bug in this mapping. This map targets prod, the only +// tenant live customer traffic reaches; DEV-tenant testing of the cause +// field will not round-trip correctly against this mapping. +var snCauseKey = map[domain.CaseCause]int{ + domain.CaseCauseSolutionArchitecture: 1, + domain.CaseCauseDeploymentArchitecture: 2, + domain.CaseCauseUserErrorConfiguration: 3, + domain.CaseCauseUserErrorProductConcept: 4, + domain.CaseCauseUserErrorRuntime: 5, + domain.CaseCauseUserErrorRecommendation: 6, + domain.CaseCauseCustomizationLimitation: 7, + domain.CaseCauseCustomizationBug: 8, + domain.CaseCauseDocumentationGap: 9, + domain.CaseCauseDocumentationError: 10, + domain.CaseCauseProductLimitation: 11, + domain.CaseCauseProductBug: 12, + domain.CaseCauseProductRegression: 13, + domain.CaseCauseProductMigration: 14, + domain.CaseCauseInfrastructureDatabase: 15, + domain.CaseCauseInfrastructureOS: 16, + domain.CaseCauseInfrastructureNetwork: 17, + domain.CaseCauseInfrastructureJDK: 18, + domain.CaseCauseInfrastructureLDAP: 19, + domain.CaseCauseInfrastructureLoadBalancer: 20, + domain.CaseCauseInfrastructureIAAS: 21, + domain.CaseCauseInfrastructureExternalProduct: 22, + domain.CaseCauseInfrastructureProxy: 23, + domain.CaseCauseInfrastructureOther: 24, + domain.CaseCauseUnknown: 25, +} + +// snCauseByID maps ServiceNow cause choice-value strings (the SN "cause" +// field's id, e.g. "12") to domain CaseCause enums. +var snCauseByID = func() map[string]domain.CaseCause { + m := make(map[string]domain.CaseCause, len(snCauseKey)) + for k, v := range snCauseKey { + m[strconv.Itoa(v)] = k } -} + return m +}() // snWorkStateIDMap maps domain CaseWorkState enums to SN numeric work state IDs. var snWorkStateIDMap = map[domain.CaseWorkState]int{ @@ -959,11 +978,12 @@ func (s *snCaseService) UpdateCase(ctx context.Context, req domain.UpdateCaseReq payload.ResolutionCode = &key } if req.Cause != nil { - label, ok := snCauseLabel[*req.Cause] + key, ok := snCauseKey[*req.Cause] if !ok { return domain.UpdateCaseResponse{}, &apierror.ValidationError{Msg: "cause contains invalid value: " + string(*req.Cause)} } - payload.Cause = &label + val := strconv.Itoa(key) + payload.Cause = &val } payload.CloseNotes = req.CloseNotes } @@ -1052,7 +1072,7 @@ func (s *snCaseService) UpdateCase(ctx context.Context, req domain.UpdateCaseReq } } if snResp.Case.Cause != nil { - if c, ok := snCauseLabelToEnum[snResp.Case.Cause.Label]; ok { + if c, ok := snCauseByID[snResp.Case.Cause.ID]; ok { resp.Case.Cause = &c } } diff --git a/entity-service/openapi.yaml b/entity-service/openapi.yaml index 53b64997b6..95dcb6ea23 100644 --- a/entity-service/openapi.yaml +++ b/entity-service/openapi.yaml @@ -3074,21 +3074,30 @@ components: cause: type: string enum: - - USER_MISUNDERSTANDING_CONCEPTS - - USER_MISUNDERSTANDING_DOCUMENTATION - - USER_NOT_FOLLOWING_DOCUMENTATION - - USER_MISTAKE - - SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE - - SOLUTION_PROBLEMATIC_CODE - - APPLICATION_BUG - - APPLICATION_MISLEADING_UX_UI - - APPLICATION_LIMITATION - - APPLICATION_MISSING_FEATURE - - APPLICATION_DOCUMENTATION_GAP - - APPLICATION_DOCUMENTATION_ERROR - - INFRASTRUCTURE_CUSTOMERS_SIDE - - INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH - - INFRASTRUCTURE_SAAS_SIDE_OTHER + - SOLUTION_ARCHITECTURE + - DEPLOYMENT_ARCHITECTURE + - USER_ERROR_CONFIGURATION + - USER_ERROR_PRODUCT_CONCEPT + - USER_ERROR_RUNTIME + - USER_ERROR_RECOMMENDATION_BEST_PRACTICES + - CUSTOMIZATION_LIMITATION + - CUSTOMIZATION_BUG + - DOCUMENTATION_GAP + - DOCUMENTATION_ERROR + - PRODUCT_LIMITATION + - PRODUCT_BUG + - PRODUCT_REGRESSION + - PRODUCT_MIGRATION + - INFRASTRUCTURE_DATABASE + - INFRASTRUCTURE_OS + - INFRASTRUCTURE_NETWORK + - INFRASTRUCTURE_JDK + - INFRASTRUCTURE_LDAP + - INFRASTRUCTURE_LOAD_BALANCER + - INFRASTRUCTURE_IAAS + - INFRASTRUCTURE_EXTERNAL_PRODUCT + - INFRASTRUCTURE_PROXY + - INFRASTRUCTURE_OTHER - UNKNOWN description: Root-cause category. Only allowed when state is closed or solution_proposed. closeNotes: @@ -3162,21 +3171,30 @@ components: type: string nullable: true enum: - - USER_MISUNDERSTANDING_CONCEPTS - - USER_MISUNDERSTANDING_DOCUMENTATION - - USER_NOT_FOLLOWING_DOCUMENTATION - - USER_MISTAKE - - SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE - - SOLUTION_PROBLEMATIC_CODE - - APPLICATION_BUG - - APPLICATION_MISLEADING_UX_UI - - APPLICATION_LIMITATION - - APPLICATION_MISSING_FEATURE - - APPLICATION_DOCUMENTATION_GAP - - APPLICATION_DOCUMENTATION_ERROR - - INFRASTRUCTURE_CUSTOMERS_SIDE - - INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH - - INFRASTRUCTURE_SAAS_SIDE_OTHER + - SOLUTION_ARCHITECTURE + - DEPLOYMENT_ARCHITECTURE + - USER_ERROR_CONFIGURATION + - USER_ERROR_PRODUCT_CONCEPT + - USER_ERROR_RUNTIME + - USER_ERROR_RECOMMENDATION_BEST_PRACTICES + - CUSTOMIZATION_LIMITATION + - CUSTOMIZATION_BUG + - DOCUMENTATION_GAP + - DOCUMENTATION_ERROR + - PRODUCT_LIMITATION + - PRODUCT_BUG + - PRODUCT_REGRESSION + - PRODUCT_MIGRATION + - INFRASTRUCTURE_DATABASE + - INFRASTRUCTURE_OS + - INFRASTRUCTURE_NETWORK + - INFRASTRUCTURE_JDK + - INFRASTRUCTURE_LDAP + - INFRASTRUCTURE_LOAD_BALANCER + - INFRASTRUCTURE_IAAS + - INFRASTRUCTURE_EXTERNAL_PRODUCT + - INFRASTRUCTURE_PROXY + - INFRASTRUCTURE_OTHER - UNKNOWN description: Root-cause category, present when the case was closed or solution proposed. closeNotes: @@ -3647,7 +3665,7 @@ components: cause: type: string nullable: true - enum: [USER_MISUNDERSTANDING_CONCEPTS, USER_MISUNDERSTANDING_DOCUMENTATION, USER_NOT_FOLLOWING_DOCUMENTATION, USER_MISTAKE, SOLUTION_PROBLEMATIC_SOLUTION_ARCHITECTURE, SOLUTION_PROBLEMATIC_CODE, APPLICATION_BUG, APPLICATION_MISLEADING_UX_UI, APPLICATION_LIMITATION, APPLICATION_MISSING_FEATURE, APPLICATION_DOCUMENTATION_GAP, APPLICATION_DOCUMENTATION_ERROR, INFRASTRUCTURE_CUSTOMERS_SIDE, INFRASTRUCTURE_SAAS_SIDE_NOT_ENOUGH, INFRASTRUCTURE_SAAS_SIDE_OTHER, UNKNOWN] + enum: [SOLUTION_ARCHITECTURE, DEPLOYMENT_ARCHITECTURE, USER_ERROR_CONFIGURATION, USER_ERROR_PRODUCT_CONCEPT, USER_ERROR_RUNTIME, USER_ERROR_RECOMMENDATION_BEST_PRACTICES, CUSTOMIZATION_LIMITATION, CUSTOMIZATION_BUG, DOCUMENTATION_GAP, DOCUMENTATION_ERROR, PRODUCT_LIMITATION, PRODUCT_BUG, PRODUCT_REGRESSION, PRODUCT_MIGRATION, INFRASTRUCTURE_DATABASE, INFRASTRUCTURE_OS, INFRASTRUCTURE_NETWORK, INFRASTRUCTURE_JDK, INFRASTRUCTURE_LDAP, INFRASTRUCTURE_LOAD_BALANCER, INFRASTRUCTURE_IAAS, INFRASTRUCTURE_EXTERNAL_PRODUCT, INFRASTRUCTURE_PROXY, INFRASTRUCTURE_OTHER, UNKNOWN] description: Root-cause category. Populated for resolved/closed ServiceNow cases; null otherwise. resolutionNotes: type: string