# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this service does

`entitlement-management` is a NetFoundry Spring Boot service that tracks and reports **usage of network components** (edge routers, endpoints/identities) and **data usage** for billing/entitlement purposes. On an hourly schedule it walks billing subscriptions → billable resources → networks → their edge routers/endpoints, computes counts, and pushes usage documents to Kinesis (which lands in Elasticsearch). It also exposes REST endpoints for querying aggregated usage that's already in Elasticsearch.

## Build, run, test

- Build: `./mvnw clean install` (or `./install.sh`, which additionally clears AWS env vars and pins `SPRING_PROFILES_ACTIVE=test`). Requires Java 21.
- Run locally: `./run.sh` — runs `mvn spring-boot:run` with `SPRING_PROFILES_ACTIVE=development`. App listens on port `8079` locally (`8092` is the "official" port in other environments). Local run expects a MySQL instance (see `docker-compose.yml`, DB `EntitlementManagement`, user `root`/`nfpass`) and a local Redis on `6379`; upstream service URLs default to `localhost` ports (core v2 `9300/core/v2`, core v3 `9300/core/v3`, billing `8094`, auth `8095`) per `application-development.properties`.
- Run a single test class: `./mvnw test -Dtest=NetworkComponentUsageServiceTest`
- Run a single test method: `./mvnw test -Dtest=NetworkComponentUsageServiceTest#testFindAverageUtilization`
- Surefire is configured to only pick up `**/*Test.java`.
- Sonar analysis: `./sonar.sh` (requires `SONAR_TOKEN`/`SONAR_ACCESS_KEY`).
- OWASP dependency check: activate the `dependency_check` Maven profile.

## Architecture

### Two parallel data paths

1. **Write path (scheduled ingestion)** — `SchedulingConfiguration` (active only in `sandbox`/`staging`/`production` profiles) runs 4 hourly, ShedLock-guarded jobs, one per billing plan family (Enterprise, Pay-As-You-Go, Freemium, Trial). Each calls `NetworkComponentUsageProcessor.find{SelfService,Enterprise}PlanComponentUsage(planId, startTime, endTime)`, then `NetworkComponentUsageService.save(...)` pushes the resulting `NetworkComponentUsage` documents to a Kinesis stream (`io.netfoundry.entitlement.usage.data.stream`).
2. **Read path (REST API)** — `UsageController` exposes `/network-component-usages` and `/network-data-usages`, both authorized via `AuthUtilService` (NetFoundry `authorization` client v2, domain `io.netfoundry.billing`). These query Elasticsearch directly via `NetworkComponentUsageService`/`NetworkDataUsageService` (index patterns `nf_component_usage-*` and `ncutilization*`).

### NetworkComponentUsageProcessor — the core orchestration

This is the most complex class in the service (`processor/usage/NetworkComponentUsageProcessor.java`). For a given billing plan id and time window it:

1. Pages through subscriptions for the plan (`RestServiceClient` calls to the billing service).
2. Resolves each subscription to `BillableResource`s (network groups or individual networks), auto-creating billable-resource mappings in the billing service when core reports network groups that aren't yet tracked (`compareAndSync`). Trial plans resolve billable resources by billing account id instead of subscription id.
3. Expands billable resources into `Network`s. **Networks come in two API generations**: "v7" networks live under core API v2 (`coreSvcBaseUrl`) and "v8"/newer networks live under core API v3 (`coreSvcBaseUrlv3`). `Network.getProductVersion()` starting with `"7"` decides which code path (`isV7Network`) is used for fetching edge routers/endpoints. When looking up by network group or by id, v2 results are preferred and v3 ("V8") results are only used as a fallback/supplement when v2 returns nothing — this v7/v8 duality is a recurring theme across recent history (see git log) and any change to network lookup needs to preserve that fallback behavior.
4. For v7 networks: edge routers/endpoints come from core's REST API directly (`getEdgeRouter`, `getEndpoint`), and router "customer-hosted vs NF-hosted" (`NetworkComponentType.CUHER`/`NFHER`) is derived from the associated `Host.provider`.
5. For v8 networks: edge router/identity data comes from the network's own Ziti controller via `ZitiReaderService` (not core), parsed by `EdgeRouterParser`/`IdentityParser` into the same `EdgeRouter`/`Endpoint` shapes, then cross-referenced against core's edge router records (`getV8EdgeRouter`) to determine host provider/registration status.
6. Builds a `NetworkComponentUsage` document (endpoint/router counts, registered counts, and per-component detail) per provisioned network.

All outbound calls to other NetFoundry services go through `RestServiceClient` + `InterServiceTokenProvider`, wrapped in `ClientUtils.tryWithCapture`, which retries once on I/O failure and once more on a 401/403 (in case the auth token needed a refresh), throwing `IllegalStateException` if still unauthorized after retry.

### Talking to per-network Ziti controllers

`EntitlementZitiApiClientFactory` builds a `ZitiApiClient` scoped to a specific network's Ziti controller. It calls core (`NetworkService.getZitiAuthInitialization`) to learn the controller URL, Ziti version, and a session token, caches that (`CacheManager`, backed by `RedissonCacheManager`/Redis with a 10s expiry grace period) keyed by network id, and re-fetches on `SessionExpiredException`. `ZitiReaderService` is the thin façade used by the processor to fetch edge routers/identities for a network.

### Elasticsearch aggregation services

`NetworkComponentUsageService.findAverageUtilization` and `NetworkDataUsageService.findSumUtilization` both build hand-rolled Elasticsearch `BoolQueryBuilder`/`AggregationBuilder` queries (terms-aggregate by network id, with avg/sum sub-aggregations) rather than going through a repository abstraction. Tests mock these by feeding raw ES JSON responses through `ElasticMockTestBase.getSearchResponseFromJson`, which manually registers the `NamedXContentRegistry` entries needed to deserialize `SearchResponse` (the old Elasticsearch 7.x high-level REST client requires this — there's no lightweight test helper for it upstream).

### Persistence

MySQL (via `spring-boot-starter-data-jdbc` + c3p0 pooling in `DataSourceConfiguration`, connection details from `spring.datasource.*`) is used only for Flyway migrations (`src/main/resources/db/migration`) and ShedLock's `SchedulerLock` table — it is not used to store usage data (that goes to Kinesis/Elasticsearch). `DataSourceConfiguration.sqlDataSource()` intentionally forces a connection on startup (`dataSource.getConnection()`), so the app **will not start** without a reachable MySQL instance, even outside the scheduling profiles.

### Auth

Two independent NetFoundry auth mechanisms are in play:
- **Inbound**: `AuthorizationConfiguration` validates JWTs against `io.netfoundry.auth` via the `authorization` client v2 library, gated per-endpoint by `AuthUtilService` checks against domain `io.netfoundry.billing` / resource type `network-component-usage`. Token validation can be disabled with `io.netfoundry.entitlement.auth.tokenvalidationdisabled=true` (dev-only escape hatch, logged loudly).
- **Outbound**: `InterServiceTokenProvider` (from `authorization` client) mints inter-service tokens per target audience (`io.netfoundry.billing`, `io.netfoundry.core`, `io.netfoundry.network`) for calls this service makes to other NetFoundry services.

## Configuration notes

- Profiles: `development` (local), `sandbox`, `staging`, `production` — set via `SPRING_PROFILES_ACTIVE`. Only `sandbox`/`staging`/`production` run the scheduled usage-ingestion jobs.
- `application-development.properties` contains real-looking sandbox credentials for local dev convenience (Elasticsearch, Auth0) — check with the team before assuming any of these are still valid, and never propagate similar secrets into `application-production.properties` or other profiles.
