Skip to content
Updated Jul 4, 2026

Policy Admin

Policy Admin is the system of record for insurance product configuration: which products exist, how they are versioned, what underwriting rules apply at each version, and which market-segment profiles override behaviour. It also holds the party and account master (members, employers, brokers, providers) that other services reference by locator. Owned by the policy-admin service, schema policy_admin.

Field reference: full columns, types and nullability live in the catalog: glossary terms Party, PartyRole, Account, Product, ProductVersion, RuleSet, Rule, MarketProfile, MarketProfileVersion, FieldDefinition. This page is the narrative.

Rating and boundary subsystems are not in the running service

The rating engine (rating factors, rate tables, rate evaluations) and the boundary/PostGIS service (boundary levels, boundaries, spatial lookups) are built on unmerged feature branches and are not wired into the deployed service today. They are called out as Planned below. A reader checking out the active branch and running policy-admin gets none of that functionality: main.go wires only party, account, product, rule-set, config, market-profile and field-definition services.

What it owns

  • The party master (individuals, organisations, providers) and their role assignments.
  • Accounts that group parties for billing and relationship management, plus account contacts.
  • Versioned insurance products with an ACTIVE/INACTIVE lifecycle.
  • Rule sets and individual rules per product version, consumed by the Enrollment rule engine.
  • Config bundles: validate then deploy a packaged product release (product + versions + rule sets) in one operation.
  • Market profiles and their versioned, market-segment configurations.
  • Field definitions: configurable per-version product attributes without schema changes.

Planned (unmerged branch): per-version rating factors and rate tables for the pricing engine; hierarchy-agnostic boundary definitions with PostGIS spatial lookups. See Planned subsystems.

Data model

policy_admin schema, 13 migrations (0001-0013). No spatial or rating tables exist in the running schema.

TablePurpose
partiesPerson and organisation records. type is INDIVIDUAL/ORGANISATION/PROVIDER.
party_rolesLinks a party to an entity. entity_type is ACCOUNT/POLICY/QUOTE; role is ACCOUNT_HOLDER/INSURED/BENEFICIARY/DEPENDENT.
accountsAccount records grouping parties for billing and management.
productsProduct definitions. status is ACTIVE/INACTIVE.
product_versionsVersioned product configuration. status is DRAFT/PUBLISHED; published_at records the transition.
rule_setsNamed collections of rules per product version.
rulesIndividual rule conditions and outcomes.
rule_evaluationsAudit log of rule evaluation results.
market_profilesMarket segment header, with a current_version pointer.
market_profile_versionsVersioned market config (jurisdiction, regulatory regime, feature flags, billing rules, mandatory fields, document templates). Published state is published_at being non-null.
field_definitionsConfigurable field specifications per product version.

Statuses above are enforced by Postgres CHECK constraints on the listed columns.

API routes

JWT routes require a bearer token; internal routes are cluster-internal with no JWT. Every collection GET uses an explicit /list suffix.

Parties and accounts

MethodPathAuthDescription
POST/partiesJWTCreate a party
GET/parties/listJWTList parties (filter by type, search)
GET/parties/{locator}JWTGet party by locator
PATCH/parties/{locator}JWTUpdate party details
GET/parties/{locator}/roles/listJWTList roles for a party
POST/accountsJWTCreate an account
GET/accounts/listJWTList accounts
GET/accounts/{locator}JWTGet account by locator
PATCH/accounts/{locator}JWTUpdate account
PATCH/accounts/{locator}/billingLevelJWTSet billing level
POST/accounts/{locator}/contactsJWTAdd a contact
GET/accounts/{locator}/contacts/listJWTList contacts
PATCH/accounts/{locator}/contacts/{contactLocator}JWTUpdate a contact
DELETE/accounts/{locator}/contacts/{contactLocator}JWTRemove a contact
GET/accounts/{locator}/policies/listJWTList account policies (stub: returns empty)
GET/accounts/{locator}/quotes/listJWTList account quotes (stub: returns empty)
POST/accounts/{locator}/number/generateJWTGenerate account display number
POST/accounts/{locator}/number/setJWTSet account display number explicitly

The policies/list and quotes/list handlers currently return empty arrays. They do not read enrollment data, so they do not create a runtime dependency on Enrollment.

Products, versions and rules

MethodPathAuthDescription
POST/productsJWTCreate a product
GET/products/listJWTList products (filter by status)
GET/products/{locator}JWTGet product
PATCH/products/{locator}JWTUpdate product
PATCH/products/{locator}/activateJWTActivate a product
PATCH/products/{locator}/deactivateJWTDeactivate a product
POST/products/{locator}/versionsJWTCreate a product version
GET/products/{locator}/versions/listJWTList product versions
GET/products/{locator}/versions/{version}JWTGet a specific version
PATCH/products/{locator}/versions/{version}JWTUpdate a version (409 if published)
PATCH/products/{locator}/versions/{version}/publishJWTPublish a version
POST/products/{locator}/versions/{version}/rulesets/JWTCreate a rule set
GET/products/{locator}/versions/{version}/rulesets/listJWTList rule sets for a version
GET/products/{locator}/versions/{version}/rulesets/{name}JWTGet a named rule set with its rules
PUT/products/{locator}/versions/{version}/rulesets/{name}/rulesJWTReplace all rules in a rule set
PATCH/products/{locator}/versions/{version}/rulesets/{name}/rules/{ruleLocator}JWTUpdate a single rule
POST/products/{productLocator}/versions/{version}/fieldsJWTCreate a field definition
GET/products/{productLocator}/versions/{version}/fieldsJWTList field definitions
DELETE/products/{productLocator}/versions/{version}/fields/{fieldLocator}JWTDelete a field definition

Rule sets are mounted under .../rulesets (one word). There is no top-level /rules/{locator} route: single-rule updates are nested under the owning rule set.

Config bundles

MethodPathAuthDescription
POST/config/validateJWTValidate a config bundle (422 with failures if invalid)
POST/config/deployJWTDeploy a config bundle, returning the new version
GET/config/{locator}/downloadJWTDownload the latest published bundle
GET/config/{locator}/download/{version}JWTDownload a specific version's bundle

Market profiles

MethodPathAuthDescription
POST/market-profilesJWTCreate a market profile
GET/market-profiles/listJWTList market profiles
GET/market-profiles/{code}JWTGet market profile by code
GET/market-profiles/{code}/currentJWTGet the current published version
POST/market-profiles/{code}/versionsJWTCreate a profile version
GET/market-profiles/{code}/versions/listJWTList versions
GET/market-profiles/{code}/versions/{version}JWTGet a specific version
PATCH/market-profiles/{code}/versions/{version}JWTUpdate a version (409 if published)
PATCH/market-profiles/{code}/versions/{version}/publishJWTPublish a version

Internal routes

No JWT; cluster-internal callers only.

MethodPathDescription
GET/internal/products/{productLocator}/versions/{version}/rulesets/{name}Fetch a rule set with rules for Enrollment
GET/internal/parties/{locator}Fetch a party (e.g. for Notifications)
GET/internal/market-profiles/{code}/currentFetch the current market profile version
GET/internal/product-versions/{id}/fieldsFetch field definitions by product-version UUID

Events

Policy Admin publishes directly to Kafka, with no outbox: internal/kafka/producer.go builds the canonical platform envelope (eventId, eventType, occurredAt, payload, optional state; see the Kafka Event Catalog) and writes it to policy-admin.events. Every emit is fire-and-forget, so a publish failure never blocks the REST write it describes. The producer stamps no correlationId or session lineage yet; the OTel trace context travels only in the Kafka message headers. Each event freezes its complete subject entities at emit time under the envelope's state key, built by the stateOf* helpers in internal/service/state.go, because a locator in the payload points at a mutable row.

eventTypeEmitted whenState subjects
member.party_createda party row is first created (public POST /parties or the internal idempotent create; reuse by email does not re-emit)party
party.updateda party's mutable details (names, email, phone) changeparty
account.createdan employer/broker account record is first createdaccount
product.publisheda product version goes live (immutable from then on)product, productVersion

It consumes nothing: no Kafka consumer runs, and other state changes (deactivate, market-profile publish, config deploy) emit no events today. Contracts (payload schema, required state subjects, lineage, producers/consumers as code refs, golden examples) live in the event registry, one directory per type under packages/go/domain/eventregistry/registry/.

Dependencies

Policy Admin has no runtime service dependencies. It is a dependency of Enrollment, Billing, Claims, Provider, Notifications, Broker API, and Member Portal API, each of which holds a POLICY_ADMIN_URL client and reads parties, products, rule sets, market profiles or field definitions over the internal routes above.

Invariants

  • A published product_version is immutable: updating its rules, fields, or attributes returns 409 Conflict. Enrollment references a specific version, so in-force policies are always adjudicated against the rules in effect when underwritten.
  • Publishing a market profile version is one-way; re-publishing or editing a published version returns 409.
  • Config deploy is atomic over product + versions + rule sets; validate is the dry-run that returns the same failure set without writing.
  • parties.type, products.status, and product_versions.status are CHECK-constrained enums, not free text (unlike the billing domain).

Key design decisions

Config bundle deployment. Rather than editing rules through individual calls, an operator assembles a full bundle (product, versions, rule sets), validates it with POST /config/validate, then deploys it atomically with POST /config/deploy. Product releases stay reproducible and auditable.

Versioned, immutable products. Configuration freezes on publish. This is what lets Enrollment pin a policy to the exact ruleset it was underwritten under.

Planned subsystems

These are built on unmerged feature branches (feat-rating-engine, feat-boundary-service) and are not registered in the running service. They are documented here so the roadmap is not lost; do not treat them as live.

Rating engine (Planned, not implemented). Adds per-product-version rating factors and rate tables so the pricing path can compute premiums from configuration. Planned tables: rating_factors, rate_tables, rate_evaluations. Planned routes, all under /products/{locator}/versions/{version}:

MethodPathDescription
POST/rating-factorsCreate a rating factor
GET/rating-factorsList rating factors (no /list suffix on this branch)
DELETE/rating-factors/{locator}Delete a rating factor
PUT/rate-tables/baseSet base rates
GET/rate-tables/baseGet base rates
PUT/rate-tables/{factorLocator}Replace rate table entries for a factor
GET/rate-tables/{factorLocator}List rate table entries
GET/rate-evaluations/{entityType}/{entityId}Rating audit trail

Rate table rows are loaded by the PUT replace endpoints. There is no bulk rate-tables/import route.

Boundary service / PostGIS (Planned, not implemented). A hierarchy-agnostic boundary model: expanding to a new country means defining boundary levels and importing GeoJSON rather than changing code. Planned tables: boundary_levels, boundaries, boundary_groups, boundary_group_members. The boundaries.boundary_type CHECK enum is ADMINISTRATIVE, SERVICE_AREA, RATING_ZONE, SCHEME_REGION, EXCLUSION_ZONE. Planned routes:

MethodPathDescription
POST/boundary-levelsDefine hierarchy levels for a market
GET/boundary-levels/{marketCode}List levels for a market
POST/boundariesCreate a boundary
POST/boundaries/importBulk import GeoJSON boundaries
GET/boundaries/listList boundaries (filter by market, level, type)
GET/boundaries/{locator}Get boundary metadata
GET/boundaries/{locator}/geometryGet geometry as GeoJSON
GET/boundaries/{locator}/childrenList child boundaries
GET/boundaries/{locator}/ancestorsList ancestor boundaries
DELETE/boundaries/{locator}Soft-delete a boundary
POST/boundary-groupsCreate a boundary group
GET/boundary-groups/listList boundary groups
GET/boundary-groups/{code}Get a group
PUT/boundary-groups/{code}/membersSet group members
GET/boundary-groups/{code}/membersList group members
POST/spatial/containsCheck if a point is in a boundary
POST/spatial/lookupFind all boundaries containing a point
POST/spatial/serviceabilityServiceability check (in a service area, not excluded)

Olly Health Insurance Platform