From e001be8a057557e35253d9ee07d7ea0fe960d4bc Mon Sep 17 00:00:00 2001 From: Rashmika998 Date: Mon, 2 Mar 2026 07:23:10 +0530 Subject: [PATCH 1/3] Add change request details endpoint and support soft delete in deployed product update --- .../backend/modules/entity/entity.bal | 9 +++ .../backend/modules/entity/types.bal | 34 +++++++++- .../backend/modules/entity/utils.bal | 21 ++++++ .../backend/modules/types/types.bal | 31 ++++++++- apps/customer-portal/backend/service.bal | 66 +++++++++++++++++-- apps/customer-portal/backend/utils.bal | 55 +++++++++++++++- 6 files changed, 204 insertions(+), 12 deletions(-) diff --git a/apps/customer-portal/backend/modules/entity/entity.bal b/apps/customer-portal/backend/modules/entity/entity.bal index 5007fcccac..ee3aa63f2a 100644 --- a/apps/customer-portal/backend/modules/entity/entity.bal +++ b/apps/customer-portal/backend/modules/entity/entity.bal @@ -411,6 +411,15 @@ public isolated function getProjectTimeCardStats(string idToken, string projectI return csEntityClient->/projects/[projectId]/time\-cards/stats.get(generateHeaders(idToken)); } +# Get change request by ID. +# +# + idToken - ID token for authorization +# + changeRequestId - Unique ID of the change request to be retrieved +# + return - Change request response containing details of the retrieved change request or error +public isolated function getChangeRequestDetails(string idToken, string changeRequestId) returns ChangeRequestResponse|error { + return csEntityClient->/change\-requests/[changeRequestId].get(generateHeaders(idToken)); +} + # Search change requests of a project. # # + idToken - ID token for authorization diff --git a/apps/customer-portal/backend/modules/entity/types.bal b/apps/customer-portal/backend/modules/entity/types.bal index 00999b2824..da8daf439a 100644 --- a/apps/customer-portal/backend/modules/entity/types.bal +++ b/apps/customer-portal/backend/modules/entity/types.bal @@ -688,6 +688,8 @@ public type DeployedProductUpdatePayload record {| int? cores?; # TPS allocated for the product decimal? tps?; + # Active status (can only be set to false to deactivate deployed product) + boolean active?; |}; # Response from updating a deployed product. @@ -1411,10 +1413,12 @@ public type ChangeRequest record {| ReferenceTableItem? deployment; # Deployed product information ReferenceTableItem? deployedProduct; + # Product information + ReferenceTableItem? product; # Planned start date and time - string? startDate; + Date? plannedStartOn; # Planned end date and time - string? endDate; + Date? plannedEndOn; # Duration string? duration; # Indicates if the change request has a service outage @@ -1487,3 +1491,29 @@ public type CatalogItemVariablesResponse record {| # List of catalog item variables CatalogItemVariable[] variables; |}; + +# Change request details information. +public type ChangeRequestResponse record {| + *ChangeRequest; + # Change request description + string? description; + # User who created the change request + string createdBy; + # Justification for the change request + string? justification; + # Impact description + string? impactDescription; + # Service outage details + string? serviceOutage; + # Communication plan + string? communicationPlan; + # Rollback plan + string? rollbackPlan; + # Test plan + string? testPlan; + # Indicates if the customer has approved + boolean hasCustomerApproved; + # Indicates if the customer has reviewed + boolean hasCustomerReviewed; + json...; +|}; diff --git a/apps/customer-portal/backend/modules/entity/utils.bal b/apps/customer-portal/backend/modules/entity/utils.bal index d39f6eabf9..9eb47c1349 100644 --- a/apps/customer-portal/backend/modules/entity/utils.bal +++ b/apps/customer-portal/backend/modules/entity/utils.bal @@ -225,3 +225,24 @@ public isolated function validateCaseCreatePayload(CaseCreatePayload payload) re } return; } + +# Validate deployed product update payload. +# +# + payload - Deployed product update payload +# + return - Error message if validation fails, () otherwise +isolated function validateDeployedProductUpdatePayload(DeployedProductUpdatePayload payload) returns string? { + boolean? active = payload.active; + int? cores = payload?.cores; + decimal? tps = payload?.tps; + if active is boolean { + if active { + return "Invalid value for active field. When updating cores or tps, active field should be set to false."; + } + if cores !is () || tps !is () { + return "When deactivating, cores and tps fields should not be provided."; + } + } else if cores is () && tps is () { + return "At least one of cores or tps should be provided when updating deployed product details."; + } + return; +} diff --git a/apps/customer-portal/backend/modules/types/types.bal b/apps/customer-portal/backend/modules/types/types.bal index 7fc6f50ab6..10f660b789 100644 --- a/apps/customer-portal/backend/modules/types/types.bal +++ b/apps/customer-portal/backend/modules/types/types.bal @@ -1052,10 +1052,12 @@ public type ChangeRequest record {| ReferenceItem? deployment; # Deployed product information ReferenceItem? deployedProduct; + # Product information + ReferenceItem? product; # Planned start date and time - string? startDate; + entity:Date? startDate; # Planned end date and time - string? endDate; + entity:Date? endDate; # Duration string? duration; # Indicates if the change request has a service outage @@ -1122,3 +1124,28 @@ public type CatalogSearchPayload record {| # Pagination details entity:Pagination pagination?; |}; + +# Change request details information. +public type ChangeRequestResponse record {| + *ChangeRequest; + # Change request description + string? description; + # User who created the change request + string createdBy; + # Justification for the change request + string? justification; + # Impact description + string? impactDescription; + # Service outage details + string? serviceOutage; + # Communication plan + string? communicationPlan; + # Rollback plan + string? rollbackPlan; + # Test plan + string? testPlan; + # Indicates if the customer has approved + boolean hasCustomerApproved; + # Indicates if the customer has reviewed + boolean hasCustomerReviewed; +|}; diff --git a/apps/customer-portal/backend/service.bal b/apps/customer-portal/backend/service.bal index 5a7a14ac0c..22d6fd09c2 100644 --- a/apps/customer-portal/backend/service.bal +++ b/apps/customer-portal/backend/service.bal @@ -718,10 +718,10 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { }; } - types:OverallConversationStats { openCount, resolvedCount, activeCount, abandonedCount} = + types:OverallConversationStats {openCount, resolvedCount, activeCount, abandonedCount} = getConversationStats(conversationStats); - return { openCount, resolvedCount, activeCount, abandonedCount }; + return {openCount, resolvedCount, activeCount, abandonedCount}; } # Get project support statistics by ID. @@ -1869,7 +1869,7 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { } # Search catalogs for a specific deployed product with filters and pagination. - # + # # + id - ID of the deployed product # + payload - Catalog search request body # + return - Paginated catalogs or error @@ -1894,7 +1894,7 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { if searchResponse is error { if getStatusCode(searchResponse) == http:STATUS_FORBIDDEN { log:printWarn(string `User: ${ - userInfo.userId} is forbidden to search catalogs for deployed product with ID: ${id}!`); + userInfo.userId} is forbidden to search catalogs for deployed product with ID: ${id}!`); return { body: { message: "You're not authorized to search catalogs for the deployed product. " + @@ -1922,11 +1922,11 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { } # Get catalog item variables by catalog ID and item ID. - # + # # + catalogId - ID of the catalog # + itemId - ID of the catalog item # + return - Catalog item details or error - resource function get catalogs/[entity:IdString catalogId]/items/[entity:IdString itemId](http:RequestContext ctx) + resource function get catalogs/[entity:IdString catalogId]/items/[entity:IdString itemId](http:RequestContext ctx) returns http:Ok|http:Unauthorized|http:Forbidden|http:InternalServerError { authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO); @@ -3074,4 +3074,58 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { body: mapChangeRequestSearchResponse(response) }; } + + # Get change request details by change request ID. + # + # + id - ID of the change request + # + return - Change request details or an error + resource function get change\-requests/[entity:IdString id](http:RequestContext ctx) + returns types:ChangeRequestResponse|http:Unauthorized|http:Forbidden|http:NotFound|http:InternalServerError { + + authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO); + if userInfo is error { + return { + body: { + message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND + } + }; + } + + entity:ChangeRequestResponse|error response = entity:getChangeRequestDetails(userInfo.idToken, id); + if response is error { + if getStatusCode(response) == http:STATUS_UNAUTHORIZED { + log:printWarn(string `User: ${ + userInfo.userId} is not authorized to access change request information!`); + return { + body: { + message: ERR_MSG_UNAUTHORIZED_ACCESS + } + }; + } + if getStatusCode(response) == http:STATUS_FORBIDDEN { + log:printWarn(string `Access to change request information is forbidden for user: ${userInfo.userId}`); + return { + body: { + message: "Access to change request information is forbidden for the user!" + } + }; + } + if getStatusCode(response) == http:STATUS_NOT_FOUND { + return { + body: { + message: "The requested change request is not found!" + } + }; + } + + string customError = "Failed to retrieve change request details."; + log:printError(customError, response); + return { + body: { + message: customError + } + }; + } + return mapChangeRequestResponse(response); + } } diff --git a/apps/customer-portal/backend/utils.bal b/apps/customer-portal/backend/utils.bal index f3635feac8..3a407c6921 100644 --- a/apps/customer-portal/backend/utils.bal +++ b/apps/customer-portal/backend/utils.bal @@ -653,6 +653,7 @@ public isolated function mapChangeRequestSearchResponse(entity:ChangeRequestSear let entity:ReferenceTableItem? case = changeRequest.case let entity:ReferenceTableItem? deployment = changeRequest.deployment let entity:ReferenceTableItem? deployedProduct = changeRequest.deployedProduct + let entity:ReferenceTableItem? product = changeRequest.product let entity:ChoiceListItem? state = changeRequest.state let entity:ChoiceListItem? impact = changeRequest.impact let entity:ChoiceListItem? 'type = changeRequest.'type @@ -660,8 +661,8 @@ public isolated function mapChangeRequestSearchResponse(entity:ChangeRequestSear id: changeRequest.id, number: changeRequest.number, title: changeRequest.title, - startDate: changeRequest.startDate, - endDate: changeRequest.endDate, + startDate: changeRequest.plannedStartOn, + endDate: changeRequest.plannedEndOn, duration: changeRequest.duration, hasServiceOutage: changeRequest.hasServiceOutage, createdOn: changeRequest.createdOn, @@ -671,6 +672,8 @@ public isolated function mapChangeRequestSearchResponse(entity:ChangeRequestSear deployment: deployment != () ? {id: deployment.id, label: deployment.name, number: deployment?.number} : (), deployedProduct: deployedProduct != () ? {id: deployedProduct.id, label: deployedProduct.name, number: deployedProduct?.number} : (), + product: product != () ? + {id: product.id, label: product.name, number: product?.number} : (), state: state != () ? {id: state.id.toString(), label: state.label} : (), impact: impact != () ? {id: impact.id.toString(), label: impact.label} : (), 'type: 'type != () ? {id: 'type.id.toString(), label: 'type.label} : () @@ -702,3 +705,51 @@ public isolated function mapCatalogSearchResponse(entity:CatalogSearchResponse r return {catalogs, totalRecords: response.totalRecords, 'limit: response.'limit, offset: response.offset}; } + +# Map change request response to the desired structure. +# +# + response - Change request response from the entity service +# + return - Mapped change request response +public isolated function mapChangeRequestResponse(entity:ChangeRequestResponse response) + returns types:ChangeRequestResponse { + + entity:ReferenceTableItem? project = response.project; + entity:ReferenceTableItem? case = response.case; + entity:ReferenceTableItem? deployment = response.deployment; + entity:ReferenceTableItem? deployedProduct = response.deployedProduct; + entity:ReferenceTableItem? product = response.product; + entity:ChoiceListItem? state = response.state; + entity:ChoiceListItem? impact = response.impact; + entity:ChoiceListItem? 'type = response.'type; + return { + id: response.id, + number: response.number, + title: response.title, + startDate: response.plannedStartOn, + endDate: response.plannedEndOn, + duration: response.duration, + hasServiceOutage: response.hasServiceOutage, + createdOn: response.createdOn, + updatedOn: response.updatedOn, + project: project != () ? {id: project.id, label: project.name, number: project?.number} : (), + case: case != () ? {id: case.id, label: case.name, number: case?.number} : (), + deployment: deployment != () ? {id: deployment.id, label: deployment.name, number: deployment?.number} : (), + deployedProduct: deployedProduct != () ? + {id: deployedProduct.id, label: deployedProduct.name, number: deployedProduct?.number} : (), + product: product != () ? + {id: product.id, label: product.name, number: product?.number} : (), + state: state != () ? {id: state.id.toString(), label: state.label} : (), + impact: impact != () ? {id: impact.id.toString(), label: impact.label} : (), + 'type: 'type != () ? {id: 'type.id.toString(), label: 'type.label} : (), + description: response.description, + createdBy: response.createdBy, + justification: response.justification, + impactDescription: response.impactDescription, + serviceOutage: response.serviceOutage, + communicationPlan: response.communicationPlan, + rollbackPlan: response.rollbackPlan, + testPlan: response.testPlan, + hasCustomerApproved: response.hasCustomerApproved, + hasCustomerReviewed: response.hasCustomerReviewed + }; +} From eed9049e21b248a54f690aad248d5ff5eff2fcbd Mon Sep 17 00:00:00 2001 From: Rashmika998 Date: Mon, 2 Mar 2026 08:12:20 +0530 Subject: [PATCH 2/3] Refactor validations and response types --- apps/customer-portal/backend/modules/entity/types.bal | 2 +- apps/customer-portal/backend/modules/entity/utils.bal | 2 +- apps/customer-portal/backend/modules/types/types.bal | 2 ++ apps/customer-portal/backend/service.bal | 9 +++++++++ apps/customer-portal/backend/utils.bal | 3 ++- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/customer-portal/backend/modules/entity/types.bal b/apps/customer-portal/backend/modules/entity/types.bal index da8daf439a..1c2477bfeb 100644 --- a/apps/customer-portal/backend/modules/entity/types.bal +++ b/apps/customer-portal/backend/modules/entity/types.bal @@ -266,7 +266,7 @@ public type Case record {| # Choice list item information. public type ChoiceListItem record {| # Choice list item value - int id; + int|string id; # Choice list item label string label; # Count diff --git a/apps/customer-portal/backend/modules/entity/utils.bal b/apps/customer-portal/backend/modules/entity/utils.bal index 9eb47c1349..bdea879949 100644 --- a/apps/customer-portal/backend/modules/entity/utils.bal +++ b/apps/customer-portal/backend/modules/entity/utils.bal @@ -230,7 +230,7 @@ public isolated function validateCaseCreatePayload(CaseCreatePayload payload) re # # + payload - Deployed product update payload # + return - Error message if validation fails, () otherwise -isolated function validateDeployedProductUpdatePayload(DeployedProductUpdatePayload payload) returns string? { +public isolated function validateDeployedProductUpdatePayload(DeployedProductUpdatePayload payload) returns string? { boolean? active = payload.active; int? cores = payload?.cores; decimal? tps = payload?.tps; diff --git a/apps/customer-portal/backend/modules/types/types.bal b/apps/customer-portal/backend/modules/types/types.bal index 10f660b789..e65349493b 100644 --- a/apps/customer-portal/backend/modules/types/types.bal +++ b/apps/customer-portal/backend/modules/types/types.bal @@ -45,6 +45,8 @@ public type CaseSearchFilters record {| int severityId?; # Deployment ID string deploymentId?; + # Case created by the logged in user + boolean createdByMe?; |}; # Payload for case search. diff --git a/apps/customer-portal/backend/service.bal b/apps/customer-portal/backend/service.bal index 22d6fd09c2..597c53f886 100644 --- a/apps/customer-portal/backend/service.bal +++ b/apps/customer-portal/backend/service.bal @@ -1824,6 +1824,15 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { }; } + string? validateDeployedProductUpdatePayload = entity:validateDeployedProductUpdatePayload(payload); + if validateDeployedProductUpdatePayload is string { + return { + body: { + message: validateDeployedProductUpdatePayload + } + }; + } + entity:DeployedProductUpdateResponse|error response = entity:updateDeployedProduct(userInfo.idToken, productId, payload); if response is error { diff --git a/apps/customer-portal/backend/utils.bal b/apps/customer-portal/backend/utils.bal index 3a407c6921..1fcc6bfcc1 100644 --- a/apps/customer-portal/backend/utils.bal +++ b/apps/customer-portal/backend/utils.bal @@ -39,7 +39,8 @@ public isolated function searchCases(string idToken, string projectId, types:Cas severityKey: payload.filters?.severityId, caseTypes: payload.filters?.caseTypes, stateKeys: payload.filters?.statusIds, - deploymentId: payload.filters?.deploymentId + deploymentId: payload.filters?.deploymentId, + createdByMe: payload.filters?.createdByMe }, pagination: payload.pagination, sortBy: payload.sortBy From 682b89e7fc212e229841f7ee919060287a8be7e4 Mon Sep 17 00:00:00 2001 From: Rashmika998 Date: Mon, 2 Mar 2026 08:23:36 +0530 Subject: [PATCH 3/3] Normalize the comparison --- apps/customer-portal/backend/utils.bal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/customer-portal/backend/utils.bal b/apps/customer-portal/backend/utils.bal index 1fcc6bfcc1..b429c8c669 100644 --- a/apps/customer-portal/backend/utils.bal +++ b/apps/customer-portal/backend/utils.bal @@ -586,7 +586,8 @@ public isolated function mapConversationResponse(entity:ConversationResponse res public isolated function getOngoingCasesCount(entity:ProjectCaseStatsResponse|error response) returns int? { if response is entity:ProjectCaseStatsResponse { types:ReferenceItem[] stateCount = from entity:ChoiceListItem item in response.stateCount - where item.id != entity:caseStateIds.closed && item.id != entity:caseStateIds.solutionProposed + where item.id.toString() != entity:caseStateIds.closed.toString() + && item.id.toString() != entity:caseStateIds.solutionProposed.toString() select {id: item.id.toString(), label: item.label, count: item.count}; if stateCount.length() > 0 { int ongoingCasesCount = 0;