Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/customer-portal/backend/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,10 @@ modules = [
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.authorization"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.entity"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.product_consumption_subscription"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.product_consumption_tracking"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.registry"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.scim"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.types"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.updates"},
{org = "wso2", packageName = "customer_portal", moduleName = "customer_portal.user_management"}
]

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/http;

configurable string productConsumptionTrackingBaseUrl = ?;
configurable ClientCredentialsOauth2Config clientCredentialsOauth2Config = ?;

@display {
label: "Product Consumption Tracking",
id: "product-consumption-tracking"
}
final http:Client productConsumptionTrackingClient = check new (productConsumptionTrackingBaseUrl, {
auth: {...clientCredentialsOauth2Config},
httpVersion: http:HTTP_1_1,
http1Settings: {keepAlive: http:KEEPALIVE_NEVER},
timeout: 300.0,
Comment thread
agzaiyenth marked this conversation as resolved.
retryConfig: {
count: RETRY_COUNT,
interval: RETRY_INTERVAL,
statusCodes: [
http:STATUS_INTERNAL_SERVER_ERROR,
http:STATUS_REQUEST_TIMEOUT,
http:STATUS_BAD_GATEWAY,
http:STATUS_SERVICE_UNAVAILABLE,
http:STATUS_GATEWAY_TIMEOUT
]
}
Comment thread
agzaiyenth marked this conversation as resolved.
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

const CONTENT_TYPE_APPLICATION_ZIP = "application/zip";
const RETRY_COUNT = 3;
const RETRY_INTERVAL = 2.0d;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

# Import deployment usage data from a zip file.
#
# + email - Email address of the importing user
# + zipFile - Zip file content as bytes
# + return - Deployment usage import response or error
public isolated function importDeploymentUsage(string email, byte[] zipFile)
returns DeploymentUsageImportResponse|error {

ImportRequest payload = {
email: email,
zip: zipFile.toBase64()
};

return productConsumptionTrackingClient->/deployment\-usages.post(payload);

Comment thread
agzaiyenth marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

# [Configurable] Client credentials grant type oauth2 configuration.
public type ClientCredentialsOauth2Config record {|
# Token URL
string tokenUrl;
# Client ID
string clientId;
# Client Secret
string clientSecret;
Comment thread
agzaiyenth marked this conversation as resolved.
# Scopes
string[] scopes = [];
|};

# Deployment usage import response.
public type DeploymentUsageImportResponse record {|
# Response message
string message?;
# Response result
json result?;
Comment thread
agzaiyenth marked this conversation as resolved.
json...;
|};

# Import Request payload.
#
# + email - email address of the user performing the import operation
# + zip - zip file content as base64 string
public type ImportRequest record {
Comment thread
agzaiyenth marked this conversation as resolved.
string email;
string zip;
};
60 changes: 60 additions & 0 deletions apps/customer-portal/backend/service.bal
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import customer_portal.ai_chat_agent;
import customer_portal.authorization;
import customer_portal.entity;
import customer_portal.product_consumption_subscription;
import customer_portal.product_consumption_tracking;
import customer_portal.registry;
import customer_portal.scim;
import customer_portal.types;
Expand Down Expand Up @@ -5108,6 +5109,65 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) {
return licenseResponse;
}

# Import product consumption usage from a zip file.
#
# + req - Request containing zip file binary body
# + return - Deployment usage import response or error
isolated resource function post deployment\-usages(http:RequestContext ctx, http:Request req)
returns product_consumption_tracking:DeploymentUsageImportResponse|http:BadRequest|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
}
};
}

string? validationError = validateDeploymentUsageImportRequest(req);
if validationError is string {
log:printWarn(validationError);
return <http:BadRequest>{
body: {
message: validationError
}
};
}

byte[]|http:ClientError zipFile = req.getBinaryPayload();
Comment thread
agzaiyenth marked this conversation as resolved.
if zipFile is http:ClientError {
Comment thread
agzaiyenth marked this conversation as resolved.
string customError = "Failed to read deployment usage import zip file.";
log:printError(customError, zipFile);
return <http:BadRequest>{
body: {
message: customError
}
};
}

product_consumption_tracking:DeploymentUsageImportResponse|error response =
product_consumption_tracking:importDeploymentUsage(userInfo.email, zipFile);
if response is error {
if getStatusCode(response) == http:STATUS_BAD_REQUEST {
return <http:BadRequest>{
body: {
message: "Invalid deployment usage import request."
Comment thread
agzaiyenth marked this conversation as resolved.
}
};
}

string customError = "Failed to import deployment usage data.";
log:printError(customError, response);
return <http:InternalServerError>{
body: {
message: customError
}
};
}
return response;
}

# Get usage stats for a specific project.
#
# + id - ID of the project
Expand Down
17 changes: 17 additions & 0 deletions apps/customer-portal/backend/utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ configurable types:FeatureFlags featureFlags = {
};
configurable string[] restrictedChangeRequestStateIds = ["-3", "-4", "-5"];

# Validate deployment usage import request.
#
# + req - Request containing zip file binary body
# + return - Error message if validation fails, nil otherwise
isolated function validateDeploymentUsageImportRequest(http:Request req) returns string? {
string contentType = req.getContentType();
if contentType.trim().length() == 0 {
return "Content-Type header is required.";
}

string baseType = re `;`.split(contentType.trim().toLowerAscii())[0].trim();
if baseType != "application/zip" && baseType != "application/x-zip-compressed" {
Comment thread
agzaiyenth marked this conversation as resolved.
Comment thread
agzaiyenth marked this conversation as resolved.
return "Request body must be a zip file.";
}
return;
}

# Search cases for a given project.
#
# + idToken - ID token for authorization
Expand Down