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
52 changes: 49 additions & 3 deletions apps/customer-portal/backend-v2/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ responses for the frontend. This is a Go rewrite of the Ballerina backend at
`apps/customer-portal/backend`, modeled on `apps/csm-portal/backend`'s conventions — read that
backend's own CLAUDE.md too if something here is underspecified.

**Status: in progress.** 50 routes are wired up so far (`GET /health`, `GET`/`PATCH /users/me`,
**Status: in progress.** 60 routes are wired up so far (`GET /health`, `GET`/`PATCH /users/me`,
`POST /accounts/search`, `GET /accounts/{id}`, `POST /projects/search`, `GET /projects/{id}`,
`POST /cases/search`, `GET /cases/{id}`, `POST /cases`, `PATCH /cases/{id}`,
`POST /cases/{id}/comments`, `POST /cases/{id}/activities/search`, `POST /deployments/search`,
Expand All @@ -25,14 +25,18 @@ backend's own CLAUDE.md too if something here is underspecified.
`POST /projects/{projectId}/conversations/{conversationId}/messages`,
`GET /projects/{id}/conversations/{conversationId}/summary`, `GET /ws`,
`POST /projects/{projectId}/deployments/{deploymentId}/license`, `POST /deployment-usages`,
`GET /projects/{id}/filters`, `GET /projects/{id}/features`, `GET /projects/{id}/stats`,
`GET /projects/{id}/stats/cases`, `GET /projects/{id}/stats/conversations`,
`GET /projects/{id}/stats/support`, `GET /projects/{id}/stats/time-cards`,
`GET /projects/{id}/stats/change-requests`,
`GET /updates/product-update-levels`, `POST /updates/levels/search`) across five upstream
services: entity-service, the WSO2 Updates service, SCIM, the AI chat agent, and the
product-consumption service (see "The AI chat agent" and "The product-consumption service" below —
unlike the other three, neither is entity-service-backed at all). The Ballerina backend exposes
~100 routes across many more modules (registry tokens, escalations, incidents, problems, task
SLAs, tasks, groups/service-offerings/configuration-items, account/project contacts, project
update, generic user search, project/case/deployment/conversation/time-card stats, case feedback,
instance search/metrics, global search, etc.) — none of those are ported yet, several because they
update, generic user search, case feedback, instance search/metrics, global search, etc.) — none
of those are ported yet, several because they
have no genuine equivalent on `cs-tools/entity-service` (confirmed by grepping
`entity-service/internal/server/routes.go` for each — stats, feedback, instance metrics, escalations,
and global search all come up empty) or aren't actually customer-portal features at all (see "Which
Expand Down Expand Up @@ -175,6 +179,48 @@ It backs two routes:
The Go client base64-encodes the bytes before forwarding to the upstream service, matching its
JSON contract exactly (`{"email": "...", "zip": "<base64>"}`).

## Project metadata and stats — reshaped, not passed through

`GET /projects/{id}/filters`, `/features`, `/stats`, `/stats/cases`, `/stats/conversations`,
`/stats/support`, `/stats/time-cards`, and `/stats/change-requests` all read from seven
entity-service endpoints (`GetProjectMetadata`, `GetProjectCaseStats`,
`GetProjectConversationStats`, `GetProjectDeploymentStats`, `GetProjectStats`,
`GetProjectTimeCardStats`, and `GetProjectChangeRequestStats`) — the Ballerina backend fans a
handful of raw entity-service responses out into eight differently-shaped, purpose-built views
rather than exposing them 1:1, and `internal/dto/project_stats.go`
replicates that fan-out exactly (ported from the Ballerina backend's `getProjectFilters`,
`mapProjectFeatures`, `mapCaseStats`, `getConversationStats`, and
`mapProjectChangeRequestStatsResponse` in `utils.bal`):
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- **`/filters` and `/features` both call `GetProjectMetadata`** — there is no `GET /projects/{id}/metadata`
passthrough endpoint in this backend at all, because the Ballerina backend never exposed one
either; it only ever exposes the metadata response split into these two narrower views.
`ChoiceListItem`/`ReferenceTableItem` (entity-service's two "list of valid options" shapes) both
collapse into one `dto.ReferenceItem{id, label, count?}` for the frontend, matching the Ballerina
backend's own `ReferenceItem` type. `/filters`' `changeRequestStates` additionally drops three
internal ServiceNow workflow state IDs (`dto.restrictedChangeRequestStateIDs`) that were never
meant to be a customer-facing filter option.
- **`/stats` and `/stats/support` are composite, graceful-degradation endpoints** — each combines
multiple independent entity-service calls (`/stats` combines case/conversation/deployment/activity
stats; `/stats/support` combines case/conversation stats) and returns `200` even if every one of
them fails, simply omitting that source's fields from the response (`dto.BuildProjectDashboardStats`/
`dto.BuildProjectSupportStats` take `*entity.XxxResponse`, nil meaning "this source failed to
load"). This exactly mirrors the Ballerina backend's own behavior — it logs each failure and moves
on rather than failing the whole request. `/stats/cases`, `/stats/conversations`,
`/stats/time-cards`, and `/stats/change-requests`, by contrast, are **not** graceful — each is a
single entity-service call and a failure there is a hard failure (`mapUpstreamError`), matching
the Ballerina backend's per-endpoint behavior exactly (verified individually, not assumed from the
composite endpoints' pattern).
- **State-ID-based derived counts are hardcoded, not configurable.** `dto.caseStateIDOpen` and the
`conversationStateID*` constants pick specific counts out of a state-count breakdown (e.g. "how
many cases are in the *open* state") using the same default ServiceNow state IDs the Ballerina
backend's own `stateIdOpen`/`conversationStateIds` configuration defaults to. If cs-tools'
ServiceNow instance uses different state IDs for these, these constants need to become
configurable here too — they are not currently, since the Ballerina backend's own configurability
was never exercised away from its defaults as far as this rewrite could confirm.
- `/stats/cases` also, in the Ballerina backend, fetches change-request stats and never uses the
result (dead code, presumably a leftover) — this backend does not replicate that no-op call.

## Middleware chain

`SecurityHeaders → CorrelationID → Auth → Logger → Mux`
Expand Down
28 changes: 27 additions & 1 deletion apps/customer-portal/backend-v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Go rewrite of the Ballerina backend at `apps/customer-portal/backend`. It is a b
[`entity-service`](../../../entity-service) (this repo's `cs-tools/entity-service`, not the
`digiops-cs/entity-service` the Ballerina backend targets), and shapes the responses for the frontend.

This is a work in progress — only the 52 routes listed below are implemented so far, across
This is a work in progress — only the 60 routes listed below are implemented so far, across
entity-service, the WSO2 Updates service, SCIM, the AI chat agent, and the product-consumption
service (two more separate services — see [CLAUDE.md](./CLAUDE.md#the-ai-chat-agent) and
[CLAUDE.md](./CLAUDE.md#the-product-consumption-service)). Everything else the Ballerina backend
Expand Down Expand Up @@ -195,6 +195,7 @@ backend-v2/
│ │ ├── user.go
│ │ ├── account.go
│ │ ├── project.go
│ │ ├── project_stats.go
│ │ ├── case.go
│ │ ├── deployment.go
│ │ ├── deployed_product.go
Expand All @@ -218,6 +219,7 @@ backend-v2/
│ ├── users.go # GET/PATCH /users/me
│ ├── accounts.go # POST /accounts/search, GET /accounts/{id}
│ ├── projects.go # POST /projects/search, GET /projects/{id}
│ ├── project_stats.go # project filters/features/dashboard-stats (composite, some graceful-degradation)
│ ├── cases.go # cases search/get/create/update/comment/activities
│ ├── deployments.go # POST /deployments/search, POST /deployments, PATCH /deployments/{id}
│ ├── deployed_products.go # deployed-product search/create/update
Expand Down Expand Up @@ -249,6 +251,14 @@ backend-v2/
- `GET /accounts/{id}` — get account by ID (same normalization)
- `POST /projects/search` — search projects
- `GET /projects/{id}` — get project by ID
- `GET /projects/{id}/filters` — get filter-dropdown options for a project (case states, severities, issue types, etc.)
- `GET /projects/{id}/features` — get a project's feature-access flags
- `GET /projects/{id}/stats` — get a project's dashboard statistics (combines case/conversation/deployment/activity stats; partial failures are tolerated)
- `GET /projects/{id}/stats/cases` — get a project's case statistics, optionally filtered by `caseTypes`/`createdBy` query params
- `GET /projects/{id}/stats/conversations` — get a project's conversation statistics, optionally filtered by `createdBy`
- `GET /projects/{id}/stats/support` — get a project's combined support statistics (case + conversation; partial failures are tolerated)
- `GET /projects/{id}/stats/time-cards` — get a project's time-card statistics, optionally filtered by `startDate`/`endDate`
- `GET /projects/{id}/stats/change-requests` — get a project's change-request statistics
- `POST /cases/search` — search cases
- `GET /cases/{id}` — get case by ID
- `POST /cases` — create a case
Expand Down Expand Up @@ -334,6 +344,22 @@ curl -X POST http://localhost:8080/projects/search \

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>/filters

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>/features

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>/stats

curl -H "x-jwt-assertion: $JWT" "http://localhost:8080/projects/<project-id>/stats/cases?caseTypes=default_case&createdBy=<user-id>"

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>/stats/conversations

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>/stats/support

curl -H "x-jwt-assertion: $JWT" "http://localhost:8080/projects/<project-id>/stats/time-cards?startDate=2026-07-01&endDate=2026-07-31"

curl -H "x-jwt-assertion: $JWT" http://localhost:8080/projects/<project-id>/stats/change-requests

curl -X POST http://localhost:8080/cases/search \
-H "x-jwt-assertion: $JWT" -H "Content-Type: application/json" \
-d '{"pagination":{"limit":10,"offset":0},"filters":{"searchQuery":"login error"}}'
Expand Down
9 changes: 9 additions & 0 deletions apps/customer-portal/backend-v2/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func main() {

userHandler := handler.NewUserHandler(entityClient, scimClient)
projectHandler := handler.NewProjectHandler(entityClient)
projectStatsHandler := handler.NewProjectStatsHandler(entityClient)
caseHandler := handler.NewCaseHandler(entityClient)
deploymentHandler := handler.NewDeploymentHandler(entityClient)
deployedProductHandler := handler.NewDeployedProductHandler(entityClient)
Expand Down Expand Up @@ -153,6 +154,14 @@ func main() {

mux.HandleFunc("POST /projects/search", projectHandler.SearchProjects)
mux.HandleFunc("GET /projects/{id}", projectHandler.GetProject)
mux.HandleFunc("GET /projects/{id}/filters", projectStatsHandler.GetProjectFilters)
mux.HandleFunc("GET /projects/{id}/features", projectStatsHandler.GetProjectFeatures)
mux.HandleFunc("GET /projects/{id}/stats", projectStatsHandler.GetProjectDashboardStats)
mux.HandleFunc("GET /projects/{id}/stats/cases", projectStatsHandler.GetProjectCaseStats)
mux.HandleFunc("GET /projects/{id}/stats/conversations", projectStatsHandler.GetProjectConversationStats)
mux.HandleFunc("GET /projects/{id}/stats/support", projectStatsHandler.GetProjectSupportStats)
mux.HandleFunc("GET /projects/{id}/stats/time-cards", projectStatsHandler.GetProjectTimeCardStats)
mux.HandleFunc("GET /projects/{id}/stats/change-requests", projectStatsHandler.GetProjectChangeRequestStats)

mux.HandleFunc("POST /cases/search", caseHandler.SearchCases)
mux.HandleFunc("GET /cases/{id}", caseHandler.GetCase)
Expand Down
Loading