# CLAUDE.md

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

## What this service is

`myzrok-orchestration` is a NetFoundry Spring Boot service that manages billing for zrok (billing accounts,
billing providers, plans, subscriptions) and keeps a zrok user's entitlements (limit classes, "limitless" flag,
interstitial-grant skip, custom domains) in sync with their subscription status. It sits between a payment
provider (Chargebee), zrok's own database/API, and NetFoundry's internal auth/messaging platform.

Note: the Java package is `io.netfoundry.myzrok_orchestration` (underscore) because the Maven artifact id
`io.netfoundry.myzrok-orchestration` is not a valid Java package name — don't "fix" this to match the artifact id.

## Build, run, test

Requires JDK 21 (despite some comments in `run.sh`/`install.sh` referencing 17 — the POM enforces
`java.version=21` and compiles with `--enable-preview`).

- `./install.sh` (or `./mvnw clean install`) — full build, runs with `SPRING_PROFILES_ACTIVE=test`.
- `./run.sh` (or `./mvnw spring-boot:run`) — runs the app locally with `SPRING_PROFILES_ACTIVE=development`,
  reading DB/service config from `application-development.properties`. Needs the local MySQL from
  `docker-compose.yml` (`docker compose up`, exposes MySQL on `localhost:3334`, db `MyzrokOrchestration`).
- `./mvnw test` — run the full test suite.
- Single test class: `./mvnw test -Dtest=ClassName`; single method: `./mvnw test -Dtest=ClassName#methodName`.
  (Surefire only picks up `**/*Test.java`.)
- Tests are `@SpringBootTest` and use Testcontainers to start a MySQL container (app's own DB) and a
  PostgreSQL container (stand-in for zrok's DB) automatically — see `DataSourceTestConfiguration` /
  `ZrokDataSourceTestConfiguration`. Containers are started with `.withReuse(true)` and are skipped
  when the `pipeline-test` profile is active (CI is expected to supply real DBs in that case).
- Useful Maven profiles (`-P<name>`):
  - `dependency_check` — OWASP dependency-check report.
  - `docs` — delomboks sources and generates the Spring Auto REST Docs / Asciidoctor API docs into
    `static/v2/docs`.
  - `myzrokOrchestrationClient` — generates a Java client from the built OpenAPI spec
    (`target/classes/static/v1/docs/openapi-v3.yaml`); skip with `-Dcodegen.skip`.

## Architecture

### Two databases, two different roles

- **Primary datasource** (`DataSourceConfiguration`, bean `dataSource`/`transactionManager`) — MySQL, owned by
  this service. Holds `billing_account`, `billing_provider`, `plan`, `subscription` (Flyway migrations in
  `src/main/resources/db/migration`).
- **Zrok datasource** (`ZrokDataSourceConfiguration`, bean `zrokDataSource`/`zrokTransactionManager`,
  qualifier `"zrokDataSource"`) — PostgreSQL, **owned by the zrok service**, accessed directly by this
  service to read/write accounts, limit classes, applied limit classes, and skip-interstitial-grants
  (`external/zrok/domain/**`, `ZrokRepositoryService`). Flyway migration for this datasource
  (`classpath:db/zrokmigration`) is normally disabled in real environments
  (`io.netfoundry.zrok.datasource.flyway.enable=false`) since zrok owns that schema; it's only
  enabled for local/test convenience.

When touching zrok entitlement data, use `@Transactional("zrokTransactionManager")` (see `ZrokClient`), not the
default transaction manager.

### Domain package pattern

Each entity under `domain/<entity>/` (e.g. `billingaccount`, `billingprovider`, `plan`, `subscription`) follows
the same CQRS-ish shape:
- `<Entity>` — the persisted record.
- `<Entity>Repository` — built on `io.netfoundry:common-util-framework`'s `GeneralDatabaseRepository` (an
  external NetFoundry library, not in this repo).
- `<Entity>Service` — business logic + repository access.
- `Create<Entity>` / `Update<Entity>` / `Find<Entity>` (often with a `.BeanBuilder`) — request/query DTOs used
  by controllers and services; `Find*` types are also bound directly from query params via Spring MVC.

The same pattern is mirrored for zrok's own resources under `external/zrok/domain/**` (accounts, limitclasses,
appliedlimitclasses, skipinterstitialgrant), but those are read/written through `zrokDataSource`.

### Billing provider abstraction

`external/billingproviders/ProviderService` is the interface every payment provider integration implements
(create/cancel/pause/resume subscription, payment methods, invoices, webhook parsing). `ProviderType` identifies
the provider on a `BillingProvider` row. `ProviderManagementService` (base class of `BillingManagementService`
and `SubscriptionEventHandler`) is constructed with `Set<ProviderService>` and indexes it into a
`Map<ProviderType, ProviderService>`, so adding a new provider means implementing `ProviderService` and
registering it as a Spring bean — no other wiring changes needed. Chargebee is currently the only
implementation (`external/billingproviders/chargebee/`).

### Core orchestration flow

- `BillingManagementService` — signup (create billing account + provider account + payment method +
  subscription, with best-effort provider-side cleanup on failure), plan changes/subscribe, cancel, invoices,
  account deletion. Plan-change logic (`subscribe(...)`) intentionally treats "downgrade to a free plan"
  differently from a normal plan change (whether prorated credits are issued, whether the old or new
  subscription drives the zrok limit change, cancel-at-end-of-term vs immediately) — read the inline comment
  there before changing this method.
- `SubscriptionEventHandler` — consumes billing-provider webhook events (via `BillingHooksController`),
  normalizes them to a `SubscriptionEvent`/`SubscriptionEventType` (CREATED/CANCELED/DELETED/PAYMENT_FAILED/
  PAYMENT_SUCCEEDED), resolves the local `BillingAccount`/`Subscription` by external ID, and updates zrok
  entitlements via `ZrokClient` accordingly (pausing on payment failure, resuming on payment success, clearing
  limits on cancel/delete, handling custom-domain cleanup when downgrading).
- `ZrokClient` — the only thing allowed to mutate zrok's entitlement state. Wraps `ZrokRepositoryService`
  (direct Postgres access) and `ZrokApiClient` (zrok's grants API).

### Two independent auth mechanisms

This service sits behind NetFoundry's gateway (see `@Server(url = "https://gateway.production.netfoundry.io")`
in `MyzrokOrchestrationApplication`). The gateway validates the caller's NetFoundry bearer JWT and only then
routes the request here — this service does not itself validate an inbound `Authorization: Bearer` header
(confirmed: the imported `AuthorizationStrategy.AutoConfig` registers no filter/interceptor for that).

1. **Internal NetFoundry auth** (`AuthorizationConfiguration`, JWT via `Auth0JwtValidator`/`JwtConsumer`,
   `AuthorizationContext`/`AuthEntityResolver`, path-based resource authorization). Most beans here are
   `@Profile("!test")`. This is the plumbing this service would use to layer its own finer-grained,
   resource-level authorization *on top of* the gateway's identity check (e.g. "is this already-authenticated
   caller allowed to read *this specific* billing account/plan"), via `@AuthRead`/`@AuthWrite`-style annotations
   and `WhereClauseResolver` scoping "find" queries by caller permission. It is currently dormant: every
   `@AuthRead(...)` annotation is commented out in the controllers, and `WhereClauseResolver` just sorts path
   IDs and then throws `IllegalStateException("Unsupported resource type: ...")` for every resource. Treat this
   as "wired but not switched on" rather than "in progress" — don't assume it's actively protecting anything.
2. **Zrok token auth** — the mechanism actually enforced today, and orthogonal to the gateway's JWT check (the
   gateway has no notion of zrok's own token system). Callers send an `x-token` header;
   `ZrokAuthenticationInterceptor` exchanges it for a zrok `Account` via `ZrokClient`/`ZrokRepositoryService`
   and confirms the account owns the `billingAccountId` in the path (protects `/billing-accounts/**` on every
   method, and `/billing-providers/*/plans/**` on GET only). `ZrokAccountInjectionFilter` runs before that,
   on `GET /billing-accounts`, and force-overwrites any caller-supplied `zrokId` query param with the ID
   resolved from the token, so a client can't list another zrok user's billing accounts.

### HTTP / serialization conventions

- Responses default to `hal+json` (Spring HATEOAS `EntityModel`/`PagedModel`); controllers build HAL links via
  `HalUtil` + `WebMvcLinkBuilder.methodOn(...)`. `application/x-yaml` and `application/problem+json` are also
  supported content types (`WebConfig`).
  - Enum and UUID Spring converters are replaced: enum binding is case-insensitive, and UUID binding accepts
    the literal string `"null"` as `null` (see `WebConfig#addFormatters`).
- OpenAPI/Swagger annotations on controllers are the source of truth for the published API spec
  (`static/docs/*.yaml` is filtered with the Maven project version at build time via
  `myzrok.orchestration.openapi.version`).

## Gotchas worth knowing before changing config

- `PostConfiguration` (`@Profile("!test")`) registers `ResourceIdPathConverter`/`ResourceIdPath` deserialization
  manually and wires `StandardResourceResolver` type resolutions for `BillingProvider`/`Plan` — this exists as a
  documented workaround for a Spring Boot 3 circular-dependency issue, not incidental setup.
- Many auth/service-discovery beans across `AuthorizationConfiguration`, `ServerConfiguration`, and
  `PostConfiguration` are `@Profile("!test")` and have direct test replacements under
  `src/test/java/.../config/*TestConfiguration.java` — when adding a new bean that depends on live
  auth/service infrastructure, check whether it needs the same test-profile split.
