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
28 changes: 5 additions & 23 deletions apps/customer-portal/backend-v2/internal/dto/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ type DeploymentSummary struct {
Project *IDLabelRef `json:"project,omitempty"`
CreatedOn time.Time `json:"createdOn"`
UpdatedOn time.Time `json:"updatedOn"`
URL *string `json:"url,omitempty"`
// ProductCount is named productCount, NOT deployedProductCount: the
// frontend's ProjectDeploymentItem reads productCount, and the Usage
// Metrics page filters deployments on `(dep.productCount ?? 0) > 0`. An
// absent value silently filters every deployment out, leaving that page
// blank with no error and no network calls. The Ballerina backend does the
// same rename (utils.bal: `productCount: deployment.deployedProductCount`).
ProductCount *int `json:"productCount,omitempty"`
InstanceCount *int `json:"instanceCount,omitempty"`
ProductCount int `json:"productCount"`
}

// SearchDeploymentsResponse is the portal's response for
Expand Down Expand Up @@ -72,6 +72,9 @@ func MapSearchDeployments(r entity.SearchDeploymentsResponse) SearchDeploymentsR
Project: entityRefToIDLabel(&d.Project),
CreatedOn: d.CreatedOn,
UpdatedOn: d.UpdatedOn,

URL: d.URL,
ProductCount: d.DeployedProductCount,
})
}
return SearchDeploymentsResponse{
Expand All @@ -83,27 +86,6 @@ func MapSearchDeployments(r entity.SearchDeploymentsResponse) SearchDeploymentsR
}
}

// WithDeploymentCounts fills ProductCount/InstanceCount on each deployment from
// per-deployment tallies keyed by deployment ID.
//
// entity-service's DeploymentView carries no counts (neither does the
// ServiceNow payload behind it), so the handler derives them and passes them
// here — see DeploymentHandler.SearchDeployments. A deployment missing from a
// map is left nil rather than set to 0, so "not counted" stays distinguishable
// from "counted zero"; the frontend treats both as 0 via `?? 0`.
func WithDeploymentCounts(resp SearchDeploymentsResponse, productCounts, instanceCounts map[string]int) SearchDeploymentsResponse {
for i := range resp.Deployments {
id := resp.Deployments[i].ID
if n, ok := productCounts[id]; ok {
resp.Deployments[i].ProductCount = &n
}
if n, ok := instanceCounts[id]; ok {
resp.Deployments[i].InstanceCount = &n
}
}
return resp
}

// DeploymentCreateRequest is the portal's request body for
// POST /projects/{id}/deployments — DeploymentTypeKey is the ServiceNow
// numeric choice-list key the frontend sends (CreateDeploymentRequest.
Expand Down
100 changes: 0 additions & 100 deletions apps/customer-portal/backend-v2/internal/dto/deployment_counts_test.go

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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.

package dto

import (
"encoding/json"
"testing"

"github.com/wso2-open-operations/cs-tools/apps/customer-portal/backend-v2/internal/entity"
)

// TestMapSearchDeployments_EmitsProductCount is the regression guard for the
// Usage & Metrics page rendering blank.
//
// That page filters deployments on `(dep.productCount ?? 0) > 0`. The key is
// productCount — NOT deployedProductCount, which is what entity-service calls
// it upstream. When the key is missing every deployment is filtered out, no
// deployment tab is selected, and every downstream metrics query is disabled by
// its own `enabled` guard — so the page renders empty with no console error and
// no network request at all. The Ballerina backend performs the same rename.
func TestMapSearchDeployments_EmitsProductCount(t *testing.T) {
got := MapSearchDeployments(entity.SearchDeploymentsResponse{
Deployments: []entity.DeploymentView{
{ID: "dep-1", Name: "Primary Production", DeployedProductCount: 3},
{ID: "dep-2", Name: "Empty", DeployedProductCount: 0},
},
Total: 2,
})

raw, err := json.Marshal(got.Deployments)
if err != nil {
t.Fatalf("marshal returned error: %v", err)
}
var items []map[string]any
if err := json.Unmarshal(raw, &items); err != nil {
t.Fatalf("result is not valid JSON: %v", err)
}

if items[0]["productCount"] != float64(3) {
t.Errorf(`dep-1 productCount = %v, want 3 — the page filters on this exact key`, items[0]["productCount"])
}
// Present-and-zero, not omitted: a deployment with no products is a real
// answer from the upstream count, not missing data.
if items[1]["productCount"] != float64(0) {
t.Errorf("dep-2 productCount = %v, want 0", items[1]["productCount"])
}
for i := range items {
if _, wrong := items[i]["deployedProductCount"]; wrong {
t.Errorf(`item %d emitted "deployedProductCount"; the frontend reads "productCount"`, i)
}
}
}

// TestMapSearchDeployments_EmitsURL covers the other field entity-service was
// dropping: the frontend's ProjectDeploymentItem declares url, and the
// Ballerina entity-service's Deployment record carries it.
func TestMapSearchDeployments_EmitsURL(t *testing.T) {
url := "https://deployment.example.com"
got := MapSearchDeployments(entity.SearchDeploymentsResponse{
Deployments: []entity.DeploymentView{{ID: "dep-1", URL: &url}, {ID: "dep-2"}},
})

if got.Deployments[0].URL == nil || *got.Deployments[0].URL != url {
t.Errorf("dep-1 URL = %v, want %q", got.Deployments[0].URL, url)
}
if got.Deployments[1].URL != nil {
t.Errorf("dep-2 URL = %v, want nil when absent upstream", *got.Deployments[1].URL)
}
}
5 changes: 5 additions & 0 deletions apps/customer-portal/backend-v2/internal/entity/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -758,10 +758,15 @@ type DeploymentView struct {
Name string `json:"name"`
Type string `json:"type"`
Description *string `json:"description"`
URL *string `json:"url"`
CreatedBy *EntityRef `json:"createdBy"`
Project EntityRef `json:"project"`
CreatedOn time.Time `json:"createdOn"`
UpdatedOn time.Time `json:"updatedOn"`
// DeployedProductCount comes straight from entity-service, which decodes it
// from the upstream deployment payload. Exposed to the frontend as
// productCount — see dto.DeploymentSummary.
DeployedProductCount int `json:"deployedProductCount"`
}

// SearchDeploymentsResponse is entity-service's response for POST /deployments/search.
Expand Down
Loading