diff --git a/apps/customer-portal/backend/Dependencies.toml b/apps/customer-portal/backend/Dependencies.toml index 838d0c5e7..1af3eb70a 100644 --- a/apps/customer-portal/backend/Dependencies.toml +++ b/apps/customer-portal/backend/Dependencies.toml @@ -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"} ] - diff --git a/apps/customer-portal/backend/modules/product_consumption_tracking/client.bal b/apps/customer-portal/backend/modules/product_consumption_tracking/client.bal new file mode 100644 index 000000000..08a8db4f0 --- /dev/null +++ b/apps/customer-portal/backend/modules/product_consumption_tracking/client.bal @@ -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, + 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 + ] + } +}); diff --git a/apps/customer-portal/backend/modules/product_consumption_tracking/constants.bal b/apps/customer-portal/backend/modules/product_consumption_tracking/constants.bal new file mode 100644 index 000000000..eff28a225 --- /dev/null +++ b/apps/customer-portal/backend/modules/product_consumption_tracking/constants.bal @@ -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; diff --git a/apps/customer-portal/backend/modules/product_consumption_tracking/product_consumption_tracking.bal b/apps/customer-portal/backend/modules/product_consumption_tracking/product_consumption_tracking.bal new file mode 100644 index 000000000..cf9440576 --- /dev/null +++ b/apps/customer-portal/backend/modules/product_consumption_tracking/product_consumption_tracking.bal @@ -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); + +} diff --git a/apps/customer-portal/backend/modules/product_consumption_tracking/types.bal b/apps/customer-portal/backend/modules/product_consumption_tracking/types.bal new file mode 100644 index 000000000..d1bfb8f77 --- /dev/null +++ b/apps/customer-portal/backend/modules/product_consumption_tracking/types.bal @@ -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; + # Scopes + string[] scopes = []; +|}; + +# Deployment usage import response. +public type DeploymentUsageImportResponse record {| + # Response message + string message?; + # Response result + json result?; + 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 {| + string email; + string zip; +|}; diff --git a/apps/customer-portal/backend/service.bal b/apps/customer-portal/backend/service.bal index 6a8f2e677..f743f8cfd 100644 --- a/apps/customer-portal/backend/service.bal +++ b/apps/customer-portal/backend/service.bal @@ -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; @@ -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 { + body: { + message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND + } + }; + } + + string? validationError = validateDeploymentUsageImportRequest(req); + if validationError is string { + log:printWarn(validationError); + return { + body: { + message: validationError + } + }; + } + + byte[]|http:ClientError zipFile = req.getBinaryPayload(); + if zipFile is http:ClientError { + string customError = "Failed to read deployment usage import zip file."; + log:printError(customError, zipFile); + return { + 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 { + body: { + message: "Invalid deployment usage import request." + } + }; + } + + string customError = "Failed to import deployment usage data."; + log:printError(customError, response); + return { + body: { + message: customError + } + }; + } + return response; + } + # Get usage stats for a specific project. # # + id - ID of the project diff --git a/apps/customer-portal/backend/utils.bal b/apps/customer-portal/backend/utils.bal index 2589a069d..27d4ff097 100644 --- a/apps/customer-portal/backend/utils.bal +++ b/apps/customer-portal/backend/utils.bal @@ -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" { + return "Request body must be a zip file."; + } + return; +} + # Search cases for a given project. # # + idToken - ID token for authorization