Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apps/customer-portal/backend/modules/entity/constants.bal
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ const DEFAULT_LIMIT = 20;
public const PENDING_ON_WSO2 = 2;
public const CANCELED = 6;

# Valid case state values
const WAITING_ON_WSO2 = 1003;
const CLOSED = 3;
const REOPENED = 1006;

# Valid conversation state values
public const ACTIVE = 2;
public const RESOLVED = 3;
Expand Down
41 changes: 35 additions & 6 deletions apps/customer-portal/backend/modules/entity/types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,9 @@ public type ProjectCaseStatsResponse record {|

# Project conversation statistics response.
public type ProjectConversationStatsResponse record {|
# Active chat count
int activeCount;
# Session count
int sessionCount;
# Resolved count
int resolvedCount;
# Count of conversations by state
ChoiceListItem[] stateCount;
json...;
|};

# Project deployment statistics response.
Expand Down Expand Up @@ -1293,3 +1290,35 @@ public type ProjectTimeCardStatsResponse record {|
decimal nonBillableHours;
json...;
|};

# case state IDs.
public type CaseStateIds record {|
# Open state ID
int open;
# Close state ID
int closed;
# Solution proposed ID
int solutionProposed;
# Work in progress state ID
int workInProgress;
# Awaiting info ID
int awaitingInfo;
# Waiting on WSO2 ID
int waitingOnWso2;
# Reopened state ID
int reopened;
|};

# Conversation state IDs.
public type ConverstaionStateIds record {|
# Open state ID
int open;
# Active state ID
int active;
# Converted state ID
int converted;
# Resolved state ID
int resolved;
# Abandoned state ID
int abandonded;
|};
15 changes: 14 additions & 1 deletion apps/customer-portal/backend/modules/entity/utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
// under the License.
import ballerina/time;

public configurable CaseStateIds & readonly caseStateIds = {
open: 1,
closed: 3,
waitingOnWso2: 1003,
reopened: 1006,
awaitingInfo: 18,
solutionProposed: 6,
workInProgress: 10
};
public configurable ConverstaionStateIds & readonly conversationStateIds =
{open: 1, active: 2, resolved: 3, converted: 4, abandonded: 5};

# Generate authorization headers.
#
# + token - ID token for authorization
Expand Down Expand Up @@ -142,7 +154,8 @@ public isolated function validateDeploymentUpdatePayload(DeploymentUpdatePayload
# + payload - Case update payload
# + return - Error message if validation fails, nil otherwise
public isolated function validateCaseUpdatePayload(CaseUpdatePayload payload) returns string? {
if payload.stateKey != CLOSED && payload.stateKey != REOPENED && payload.stateKey != WAITING_ON_WSO2 {
if payload.stateKey != caseStateIds.closed && payload.stateKey != caseStateIds.reopened &&
payload.stateKey != caseStateIds.waitingOnWso2 {
Comment thread
Rashmika998 marked this conversation as resolved.
return "Invalid status. Allowed values are Waiting on WSO2, Closed, or Reopened.";
}
return;
Expand Down
32 changes: 30 additions & 2 deletions apps/customer-portal/backend/modules/types/types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ public type ProjectCaseStats record {|

# Project support statistics.
public type ProjectSupportStats record {|
# Total cases count
int totalCases?;
# Ongoing cases count
int? ongoingCases?;
# Active chats count
int activeChats?;
# Session chats count
Expand Down Expand Up @@ -993,3 +993,31 @@ public type ConversationResponse record {|
string updatedBy;
json...;
|};

# Overall conversation statistics for a project.
public type OverallConversationStats record {|
# Chat sessions count
int sessionCount?;
# Resolved chats count
int resolvedCount?;
# Open chats count
int openCount?;
# Abandoned chats count
int abandonedCount?;
# Total messages count
int totalCount?;
# Active chats count
int activeCount?;
# Converted chats count
int convertedCount?;
|};

# Conversation statistics by state for a project.
public type ConversationStats record {|
# Resolved chats count
int resolvedCount?;
# Open chats count
int openCount?;
# Abandoned chats count
int abandonedCount?;
|};
83 changes: 62 additions & 21 deletions apps/customer-portal/backend/service.bal
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
log:printError(ERR_MSG_CONVERSATION_STATISTICS, conversationStats);
// To return other stats even if conversation stats retrieval fails, error will not be returned.
}
types:OverallConversationStats mappedConversationStats = getConversationStats(conversationStats);

// Fetch deployment stats
entity:ProjectDeploymentStatsResponse|error deploymentStats =
Expand All @@ -627,8 +628,7 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
projectStats: {
openCases: caseStats is entity:ProjectCaseStatsResponse ?
getOpenCasesCountFromProjectCasesStats(caseStats) : (),
activeChats: conversationStats is entity:ProjectConversationStatsResponse ?
conversationStats.activeCount : (),
activeChats: mappedConversationStats.activeCount,
deployments: deploymentStats is entity:ProjectDeploymentStatsResponse ? deploymentStats.totalCount : (),
slaStatus: projectActivityStats is entity:ProjectStatsResponse ? projectActivityStats.slaStatus : ()
},
Expand Down Expand Up @@ -673,6 +673,57 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
return mapCaseStats(caseStats);
}

# Get conversation statistics for a project by ID.
#
# + id - ID of the project
# + return - Conversation statistics overview or error response
resource function get projects/[entity:IdString id]/stats/conversations(http:RequestContext ctx)
returns types:ConversationStats|http:Unauthorized|http:Forbidden|http:InternalServerError {

authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO);
if userInfo is error {
return <http:InternalServerError>{
body: {
message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND
}
};
}

entity:ProjectConversationStatsResponse|error conversationStats =
entity:getConversationStatsForProject(userInfo.idToken, id);
if conversationStats is error {
if getStatusCode(conversationStats) == http:STATUS_UNAUTHORIZED {
log:printWarn(string `User: ${userInfo.userId} is not authorized to access the customer portal!`);
return <http:Unauthorized>{
body: {
message: ERR_MSG_UNAUTHORIZED_ACCESS
}
};
}

if getStatusCode(conversationStats) == http:STATUS_FORBIDDEN {
log:printWarn(string `Access to conversation statistics is forbidden for user: ${userInfo.userId}`);
return <http:Forbidden>{
body: {
message: "Access to conversation statistics is forbidden!"
}
};
}
string customError = "Failed to retrieve conversation statistics for the project.";
log:printError(customError, conversationStats);
return <http:InternalServerError>{
body: {
message: customError
}
};
}

types:OverallConversationStats { openCount, resolvedCount, abandonedCount} =
getConversationStats(conversationStats);

return { openCount, resolvedCount, abandonedCount };
}

# Get project support statistics by ID.
#
# + id - ID of the project
Expand Down Expand Up @@ -704,15 +755,15 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
log:printError(ERR_MSG_CONVERSATION_STATISTICS, conversationStats);
// To return other stats even if conversation stats retrieval fails, error will not be returned.
}
types:OverallConversationStats mappedConversationStats = getConversationStats(conversationStats);

int? ongoingCases = getOngoingCasesCount(caseStats);

return {
totalCases: caseStats is entity:ProjectCaseStatsResponse ? caseStats.totalCount : (),
activeChats: conversationStats is entity:ProjectConversationStatsResponse ?
conversationStats.activeCount : (),
sessionChats: conversationStats is entity:ProjectConversationStatsResponse ?
conversationStats.sessionCount : (),
resolvedChats: conversationStats is entity:ProjectConversationStatsResponse ?
conversationStats.resolvedCount : ()
ongoingCases: ongoingCases is int ? ongoingCases : (),
activeChats: mappedConversationStats.activeCount,
sessionChats: mappedConversationStats.sessionCount,
resolvedChats: mappedConversationStats.resolvedCount
};
}

Expand Down Expand Up @@ -1409,16 +1460,6 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
}
};
}
if getStatusCode(commentsResponse) == http:STATUS_FORBIDDEN {
log:printWarn(string `User: ${userInfo.userId} is forbidden to access comments for case with ID: ${
id}!`);
return <http:Forbidden>{
body: {
message: "You're not authorized to access the comments for the requested case. " +
"Please check your access permissions or contact support."
}
};
}
string customError = "Failed to retrieve comments.";
log:printError(customError, commentsResponse);
return <http:InternalServerError>{
Expand Down Expand Up @@ -2723,7 +2764,7 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
# + id - ID of the project
# + payload - Time card search payload containing filters and pagination info
# + return - List of time cards matching the criteria or an error
resource function post projects/[string id]/time\-cards/search(http:RequestContext ctx,
resource function post projects/[entity:IdString id]/time\-cards/search(http:RequestContext ctx,
types:TimeCardSearchPayload payload)
returns http:Ok|http:BadRequest|http:Unauthorized|http:Forbidden|http:InternalServerError {

Expand Down Expand Up @@ -2791,7 +2832,7 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
# + startDate - Start date for the statistics (optional)
# + endDate - End date for the statistics (optional)
# + return - Time card statistics for the project or an error
resource function get projects/[string id]/time\-cards/stats(http:RequestContext ctx, entity:Date? startDate,
resource function get projects/[string id]/stats/time\-cards(http:RequestContext ctx, entity:Date? startDate,
Comment thread
shayanmalinda marked this conversation as resolved.
entity:Date? endDate)
returns entity:ProjectTimeCardStatsResponse|http:Unauthorized|http:Forbidden|http:InternalServerError {
Comment thread
Rashmika998 marked this conversation as resolved.
Comment thread
Rashmika998 marked this conversation as resolved.

Expand Down
63 changes: 63 additions & 0 deletions apps/customer-portal/backend/utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,66 @@ public isolated function mapConversationResponse(entity:ConversationResponse res
updatedOn: response.updatedOn
};
}

# Get ongoing cases count from project case stats response.
#
# + response - Project case stats response from the entity service
# + return - Count of ongoing cases, or 0 if not available
public isolated function getOngoingCasesCount(entity:ProjectCaseStatsResponse|error response) returns int? {
Comment thread
shayanmalinda marked this conversation as resolved.
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
select {id: item.id.toString(), label: item.label, count: item.count};
if stateCount.length() > 0 {
int ongoingCasesCount = 0;
foreach types:ReferenceItem stat in stateCount {
int statCount = stat.count ?: 0;
ongoingCasesCount += statCount;
}
return ongoingCasesCount;
}
return 0;
}
return;
}

# Get conversation stats from project conversation stats response.
#
# + response - Project conversation stats response from the entity service
# + return - Conversation stats with counts for each conversation state
public isolated function getConversationStats(entity:ProjectConversationStatsResponse|error response)
Comment thread
shayanmalinda marked this conversation as resolved.
returns types:OverallConversationStats {

if response is entity:ProjectConversationStatsResponse {
types:ReferenceItem[] mappedConversationStats = from entity:ChoiceListItem item in response.stateCount
select {id: item.id.toString(), label: item.label, count: item.count};

types:ReferenceItem[] openCases = mappedConversationStats.filter(stat =>
stat.id == entity:conversationStateIds.open.toString());
types:ReferenceItem[] activeCases = mappedConversationStats.filter(stat =>
stat.id == entity:conversationStateIds.active.toString());
types:ReferenceItem[] resolvedCases = mappedConversationStats.filter(stat =>
stat.id == entity:conversationStateIds.resolved.toString());
types:ReferenceItem[] convertedCases = mappedConversationStats.filter(stat =>
stat.id == entity:conversationStateIds.converted.toString());
types:ReferenceItem[] abandondedCases = mappedConversationStats.filter(stat =>
stat.id == entity:conversationStateIds.abandonded.toString());
// TODO: Add session chats after entity service supports it

return {
openCount: openCases.length() > 0 ? openCases[0].count : (),
activeCount: activeCases.length() > 0 ? activeCases[0].count : (),
resolvedCount: resolvedCases.length() > 0 ? resolvedCases[0].count : (),
convertedCount: convertedCases.length() > 0 ? convertedCases[0].count : (),
abandonedCount: abandondedCases.length() > 0 ? abandondedCases[0].count : ()
};
Comment thread
Rashmika998 marked this conversation as resolved.
}
return {
openCount: (),
activeCount: (),
resolvedCount: (),
convertedCount: (),
abandonedCount: (),
sessionCount: ()
};
}