# 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).

### Authentication

This service sits behind NetFoundry's gateway (see `@Server(url = "https://gateway.production.netfoundry.io")`
in `MyzrokOrchestrationApplication`). It does not use NetFoundry's `io.netfoundry:authorization` library — no
Auth0 service identity, no NetFoundry JWT validation, no resource-level `@AuthRead`/`@AuthUpdate` checks.

The only auth enforced in this service is the **zrok session**: `POST /session/login` checks zrok credentials and
sets a `MYZROK_SESSION` cookie (sessions live in `SessionService`'s in-memory cache). `ZrokAuthenticationInterceptor`
requires that cookie on `/billing-accounts/**` (every method; non-GET also needs `X-Requested-With: myzrok-extension`,
and the caller must own the `billingAccountId` in the path) and on `GET /billing-providers/*/plans/**`.
`ZrokAccountInjectionFilter` force-overwrites any caller-supplied `zrokId` on `GET /billing-accounts` with the
session's zrok ID. Everything else — `/billing-providers` CRUD, plan create/update/delete, `/billing/{id}/events`
(Chargebee webhook, verified in `SubscriptionEventHandler`) and `/session/**` — has no check in this service.

### 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

- Many service-discovery beans in `ServerConfiguration` are `@Profile("!test")` and have direct test replacements under
  `src/test/java/.../config/*TestConfiguration.java` — when adding a new bean that depends on live
  service infrastructure, check whether it needs the same test-profile split.
