Publisert - 28.09.2026

Changelog

All notable changes to the Nompd API will be documented in this file.

The format is based on Keep a Changelog.

[Unreleased]

Breaking Changes

  • New product structure — The products entries in both treatment-group and reimbursement-group responses (v1 and v2) have a new shape. The reference, type, and display fields are removed, and the single identifier object is replaced by an identifiers list:

    {
      "identifiers": [
        { "system": "FEST/LegemiddelPakning/Varenummer", "value": "448992" },
        { "system": "FEST/LegemiddelPakning/Id", "value": "ADDED_WHEN_FEST_FILE_IS_READ" },
        { "system": "FEST/LegemiddelMerkevare/Id", "value": "ADDED_WHEN_FEST_FILE_IS_READ" },
        { "system": "FEST/LegemiddelVirkestoff/Id", "value": "ADDED_WHEN_FEST_FILE_IS_READ" }
      ]
    }
    

    The identifier system is renamed from FEST/Varenummer to FEST/LegemiddelPakning/Varenummer (the previous type value is folded into the system string). The FEST/*/Id identifiers carry the placeholder value ADDED_WHEN_FEST_FILE_IS_READ until FEST file ingestion populates the real values. products are empty between the deployment of this change and the next sync.

  • Renamed items to entries in the response envelope — The list endpoints (treatment-group and reimbursement-group, v1 and v2) now return { "currentVersion": ..., "entries": [...] } instead of { "currentVersion": ..., "items": [...] }. The entry shape (data, version, lastChanged, isDeleted) is unchanged. No backwards-compatible parallel field — consumers must migrate.

Added

  • FEST validity filtering (IsInFest) — Products (ReimbursementGroupProduct/ TreatmentAlternativeProduct) now carry an internal IsInFest flag, computed whenever their FEST identifiers are (re)resolved, from whether a FEST/LegemiddelPakning/Id identifier with a non-"Missing" value could be found. By default (ignore-fest-validation=false), the flag is omitted from the JSON response shape. GET /api/v1/treatment-group, GET /api/v1/reimbursement-group, their /{id} and v2 counterparts now drop products without a resolved FEST match from a group's response, and omit the group entirely (404 for /{id}) only if it had at least one product before filtering but is left with none afterward — a group with no products to begin with (a pre-existing, documented state) is unaffected. Incremental since-version responses emit a response-only isDeleted: true tombstone when a changed active group loses its final FEST-valid product, allowing clients to remove an earlier visible copy; a later FEST match reactivates it with isDeleted: false. Pass ignore-fest-validation=true to restore the previous unfiltered behavior and include isInFest: true|false on every returned product. An IsInFest-only change increments the owning group's version even though the canonical content hash remains unchanged. See API Reference.

  • GET /api/internal/fest/missing-products — New internal-only endpoint listing every persisted product whose article number currently has no resolved FEST match, for data-quality monitoring of products excluded by the filtering above. See API Reference.

  • --festsync CLI mode — Running the API binary with --festsync (alongside the existing --sync/--migrate modes) triggers a single FEST/M30 sync (FestSyncService.ExecuteAsync().

  • FEST sync log + automatic full/delta sync mode — New FestSyncLog table (see Data model (Postgres)) records one row per FestSyncService.ExecuteAsync run (started/completed timestamps, success, mode, the incrementalDate actually used, duration, per-category stats, refreshed product count, and any error message). The sync now automatically chooses between two modes: a full fetch (incrementalDate: null, deactivates existing rows missing from the fetch) when forced or when no previous run succeeded, and a delta fetch (incrementalDate = last successful run's start time minus one hour) otherwise, which leaves rows missing from the (small) delta response untouched instead of deactivating them. A failed run is still logged (Success: false) so it isn't mistaken for a successful baseline. FestSyncResponse gains mode, force, and incrementalDateUsed fields reporting which path a given run took. See the related breaking change to POST /api/internal/fest/sync's query parameters above.

  • GET /api/internal/sync/logs — New read-only, paginated endpoint returning a combined, normalized list of past sync runs from both FestSyncLog (FEST/M30) and SyncHistory (SHI), sorted by completion/start time descending. Same X-API-KEY authentication and per-API-key rate limiting as the other /api/internal/* endpoints; hidden from Swagger. Supports an optional source filter (fest/shi, case-insensitive; invalid value → 400) and take (default 50, clamped 1-200) / skip (default 0; negative → 400) paging parameters. Does not trigger a sync itself — see API reference for the full response shape. Known limitation: SyncHistory has no persisted failure/error state, so success is always true for source: "Shi" entries and a failed SHI run leaves no row at all (invisible to this endpoint); FEST entries track failures explicitly via FestSyncLog. Not a breaking change.

  • ConversionService now populates real FEST product identifiers — during a FHIR sync (POST /api/internal/sync), TreatmentAlternativeProduct/ReimbursementGroupProduct identifiers (FEST/LegemiddelPakning/Id, FEST/LegemiddelMerkevare/Id, FEST/LegemiddelVirkestoff/Id) are resolved from the synced Fest* tables by varenummer (via IMedialProductLookupService/ProductIdentifierService), instead of the ADDED_WHEN_FEST_FILE_IS_READ placeholder. Falls back to a Missing sentinel value when a varenummer has no FEST match. The identifier-building logic was extracted out of ConversionService into a new IProductIdentifierService/ProductIdentifierService, shared with the FEST-sync refresh described below. Not a breaking change.

  • Persisted product identifiers now refresh automatically after a FEST sync — when POST /api/internal/fest/sync adds or updates a Legemiddelpakning row (or a LegemiddelVirkestoff row linked back to one via RefPakning), every already-persisted TreatmentAlternativeProduct/ReimbursementGroupProduct referencing that varenummer has its FEST identifiers rebuilt via the new IProductIdentifierRefreshService. FestSyncResponse gains a new refreshedProductCount field reporting how many products were refreshed. LegemiddelMerkevare-only changes are not tracked since they don't affect a pakning's own identifiers. Superseded below — Version/ContentHash are now also recomputed and bumped when this changes a group (see "FEST sync now versions changed TreatmentGroup/ ReimbursementGroup rows" below). Not a breaking change (additive field on FestSyncResponse).

  • FEST sync now versions changed TreatmentGroup/ReimbursementGroup rows and records them in SyncHistory — Previously, a FEST-triggered product identifier refresh (see above) bumped only the owning group's UpdatedAt, leaving Version/ContentHash stale; API consumers polling ?since-version= never saw FEST-only identifier changes, and there was no audit trail for these updates. Now, after IProductIdentifierRefreshService.RefreshAsync rebuilds identifiers, it recomputes each touched group's ContentHash (using the same entity→response mapping the API already uses, now extracted into shared Api.Mapping.TreatmentGroupMapper/ ReimbursementGroupMapper classes) and, only for groups whose hash actually changed, applies a single shared Version bump (from the existing SyncMetadata.CurrentVersion counter, incremented once per run — the same mechanism the SHI sync uses) alongside ContentHash/ UpdatedAt. RefreshAsync now returns a ProductIdentifierRefreshResult (RefreshedProductCount, UpdatedTreatmentGroupKeys, UpdatedReimbursementGroupKeys) instead of a plain int. FestSyncResponse gains updatedTreatmentGroupCount/ updatedReimbursementGroupCount fields. When a run changes at least one group, FestSyncService now also writes a new SyncHistory row (Source: Fest) with the applied version and the changed groups' BusinessKeys in treatmentGroups/reimbursementGroups — visible through GET /api/internal/sync/logs. A run that changes nothing writes no SyncHistory row. SyncHistory gains a new Source (Shi/Fest) column (migration AddSyncHistorySource; existing rows backfill to Shi) to distinguish which pipeline wrote a given row — see Data model (Postgres). GET /api/internal/sync/logs's ?source=fest filter now also returns these SyncHistory rows (previously only FestSyncLog rows) and ?source=shi is now correctly scoped to Source: Shi rows only (previously it returned every SyncHistory row regardless of source, since the column didn't exist yet). Publication is coordinated through a database transaction/lock shared by SHI and FEST: product/group changes, the atomic global version increment, and the corresponding SyncHistory row commit or roll back together, including when sync jobs overlap across API replicas. Not a breaking change (additive fields; existing SyncHistory rows keep working via the Shi default).

  • FestClient project — New backend/src/FestClient class library for consuming the FEST (Statens legemiddelverk) SOAP/WCF medicinal products service via a generated dotnet-svcutil proxy and a thin IFestServiceClient/FestServiceClient wrapper. Not yet wired into the Api (no endpoint, background job, or persistence) — see FestClient for details. Not a breaking change.

  • FEST/M30 persistence + sync endpoint — The full FEST/M30 dataset is now persisted into ~130 Fest*-prefixed Postgres tables (see the "FEST/M30 Entities" section of Data model (Postgres) and Fest data model conventions). A new FestSyncService maps and upserts (by ExternalId) all 15 FEST categories, deriving Active from FEST's own Status.V == A. POST /api/internal/fest/sync triggers a sync and returns only a status summary (FestSyncResponse: added/updated/unchanged/deactivated counts per category, duration, timestamp) — it never returns the raw FEST payload. The existing GET /api/vi/fest raw-payload endpoint is kept for now (marked with a // TODO for future removal) but is deprecated in favor of reading from the synced Fest* tables. Not a breaking change.

  • HelseID (DPoP) v2 endpoints — The data endpoints are now also served under /api/v2/, authenticated with a HelseID access token using DPoP (RFC 9449) instead of the X-API-KEY header:

    • GET /api/v2/treatment-group, GET /api/v2/treatment-group/{id}
    • GET /api/v2/reimbursement-group, GET /api/v2/reimbursement-group/{id}
    • Same data, query parameters, and response shapes as the v1 endpoints.
    • Requests send Authorization: DPoP <access-token> plus a per-request DPoP proof header. The access token must be issued by HelseID with audience nhn:legemiddelgrunndata and the nhn:legemiddelgrunndata/api scope. Bearer tokens are not accepted on v2 endpoints (they return 401).
    • New status code: 403 Forbidden when authenticated via HelseID but the access token is missing the nhn:legemiddelgrunndata/api scope.
    • v2 endpoints are not rate limited.
    • Non-breaking — v1 endpoints and API key authentication are unchanged.
  • Rate limiting — The data endpoints (/api/v1/treatment-group, /api/v1/reimbursement-group, /api/internal/sync) are now rate limited to 120 requests per minute per API key (sliding window, configurable via RateLimiting__* environment variables). Exceeding the limit returns 429 Too Many Requests with a Retry-After header (set to the rate-limit window length) and body { "error": "Rate limit exceeded. Try again later." }. Requests without a valid API key (missing or unknown) share an anonymous bucket, keeping the number of limiter partitions bounded. Limiting can be disabled with RateLimiting__Enabled=false. The health endpoint and Swagger UI are not rate limited. Not a breaking change — only new 429 responses under load.

  • Global exception handling — Unhandled exceptions now return 500 Internal Server Error with a JSON body containing a correlationId. In the Development environment, the response also includes message and stackTrace. The correlation ID is reused from an incoming X-Correlation-Id header when present, otherwise generated, and is echoed in the X-Correlation-Id response header. Exceptions are logged with their correlation ID.

  • X-Correlation-Id on all responses — Every response (not only 500 errors) now includes an X-Correlation-Id response header, so any request can be traced in the server logs. A client-provided X-Correlation-Id request header is still reused when present. Non-breaking — only adds a response header.

Removed

  • MongoDB deployment and helper scripts — Removed the MongoDB Helm chart values (manifests/apps/mongodb), the mongodb entries from the ArgoCD app-of-apps values, and the helper scripts create-mongodb-secret.sh, forward-db.sh, and run-exporer.sh. PostgreSQL is the sole datastore. No API behavior or data contract changes.

Fixed

  • Stale product identifiers when a Legemiddelpakning's Varenr changes — If FEST changed the Varenr of an existing Legemiddelpakning (same ExternalId) between syncs, ProductIdentifierRefreshService.RefreshAsync matched persisted TreatmentAlternativeProduct/ ReimbursementGroupProduct rows only by their currently-stored (old) Varenummer identifier, which no longer appeared in the sync's "affected article numbers" set — so those products were silently never refreshed again by any later sync. FestSyncService's Legemiddelpakning sync step now also tracks old→new Varenr renames for the run, and RefreshAsync matches on both the old and new value and resolves to the new Varenr before rebuilding identifiers.
  • force=true FEST sync now also force-rebuilds every product's identifiers — Previously, force=true (POST /api/internal/fest/sync?force=true) only forced a full FEST fetch and deletion reconciliation; identifier refresh was still scoped to the diff-based "affected article numbers" computed for that run. If the forced fetch pulled back data identical to the last sync, the affected set was empty and no product identifiers were refreshed at all, even though the caller asked for a forced sync. IProductIdentifierRefreshService.RefreshAsync gained a refreshAll parameter that, when set, rebuilds identifiers for every persisted TreatmentAlternativeProduct/ReimbursementGroupProduct regardless of the diff; force=true now always passes refreshAll: true, so a forced sync unconditionally rebuilds all identifiers. The refresh resolves each product's current Varenummer through its stable FEST/LegemiddelPakning/Id, allowing it to repair Varenummer values that were already stale before the forced sync. If the stable package id no longer resolves to active FEST data, the stored Varenummer is retained and dependent identifiers use the existing Missing sentinel.
  • FEST consumer filter is now hardcoded to Rekvirent — Both endpoints, and FestSyncService.ExecuteAsync, now always use FilterEnum.Rekvirent internally
  • FEST/LegemiddelVirkestoff/Id resolved from the wrong FEST category — The active-substance identifier was previously derived from an unrelated FEST category (KatLegemiddelVirkestoff / LegemiddelVirkestoff.RefPakning), a standalone aggregation category that only links a subset of packages and produced missing or incorrect data for others (e.g. varenr 017234, 491230). The correct chain, confirmed against the FEST/WCF source schema, is Legemiddelpakning.Pakningsinfo[].RefLegemiddelMerkevare → LegemiddelMerkevare → (SortertVirkestoffMedStyrke[].RefVirkestoffMedStyrke → VirkestoffMedStyrke.RefVirkestoff) or (SortertVirkestoffUtenStyrke[].RefVirkestoff, direct) → Virkestoff.Id. The old FestLegemiddelVirkestoff/FestLegemiddelVirkestoffPakning tables and sync logic have been removed entirely and replaced with FestLegemiddelMerkevare (+ two child ref tables) and FestVirkestoffMedStyrke, synced from KatLegemiddelMerkevare and the VirkestoffMedStyrke members of KatVirkestoff. MedialProductLookupService.GetActiveSubstanceIdsAsync and the FEST sync's product-identifier auto-refresh (a substance-only change now correctly reverse-resolves to the affected varenummer through the full chain) have been updated accordingly. See Fest data model conventions. Operational note: this is a new category with no previously-stored data, so it requires a full FEST re-sync (not a SQL fix) to populate FestLegemiddelMerkevare/FestVirkestoffMedStyrke before FEST/LegemiddelVirkestoff/Id starts resolving correctly.
  • FEST sync Active flag was always false — FestSyncService derived FestEntityBase.Active by comparing the wrapper's Status.V against the literal string "AKTIV", but FEST's real status code for an active listing is the short code "A" (Status.DN displays as "Aktiv oppføring"). Every synced Fest* row was therefore (incorrectly) marked inactive regardless of its actual FEST status. Active is now derived from Status.V == "A". Operational note: a plain re-sync will not fix already-persisted rows, since unchanged rows (SourceUpdatedAt == Tidspunkt) are skipped without recomputing Active. Since the already-stored StatusCode was always correct (only the derived Active bool was wrong), existing rows can be repaired directly with a one-time UPDATE "FestLegemiddelpaknings" SET "Active" = ("StatusCode" = 'A') (and the equivalent for "FestLegemiddelVirkestoffs") instead of forcing a full re-sync.
  • FEST sync ExternalId used the wrong node's Id — For 9 of the 15 synced FEST categories (Legemiddelpakning, Legemiddeldose, LegemiddelVirkestoff, LegemiddelMerkevare, Diagnose, Virkestoff/VirkestoffMedStyrke, Interaksjon, Vilkar, Byttegruppe, OrdineringVirkestoff), FestSyncService derived ExternalId from the outer "Oppf..." wrapper node's Id instead of the nested domain entity's own Id. Both carry independent, valid Id values, but other categories' Ref{Target} (IDREF) fields reference the nested entity's Id — using the wrapper's Id broke that cross-reference semantics. ExternalId is now correctly derived from the nested entity's own Id for these categories (see Fest data model conventions for the full per-category rule). Operational note: after deploying this fix, run a full FEST re-sync — rows previously stored under the old (wrapper-based) ExternalId will no longer match on the next sync and will be deactivated by the existing "missing" handling, while new rows are inserted with the corrected ExternalId. This is expected and self-healing via the normal sync job; stale deactivated duplicates are not automatically deleted.
  • Access token typ validation on v2 (HelseID) endpoints — Access tokens whose typ header is not at+jwt are now rejected with 401 Unauthorized, as required by the HelseID security profile. Previously the typ header was not validated, so such tokens were accepted and the request was processed.
  • DPoP htu validation behind TLS-terminating gateway — The forwarded-headers middleware now forwards X-Forwarded-Proto and trusts only the source networks configured via FORWARDED_TRUSTED_NETWORKS (comma-separated CIDR list) instead of all proxies. This ensures request.Scheme is https for DPoP htu comparison while preventing header spoofing from untrusted sources.
  • Swallowed unhandled exceptions — The request logging middleware previously swallowed unhandled exceptions, which resulted in empty 200 OK responses with no error log. Exceptions now propagate to the exception handler.
  • Client aborts — A client aborting a request no longer produces a 500 response or an error log.

Breaking Changes

  • Removed /filter sub-route — Filter parameters are now query params on the main endpoint.
    • GET /api/v1/treatment-group/filter?... → GET /api/v1/treatment-group?...
    • GET /api/v1/reimbursement-group/filter?... → GET /api/v1/reimbursement-group?...
    • No backwards-compatible parallel endpoint — consumers must migrate.

[4.0.0] - 2026-06-17

Breaking Changes

  • Renamed medicines to products on both TreatmentAlternative and ReimbursementGroup, and renamed the referenced type MedicinalReference to ProductReference.
    • JSON field: medicines → products (affects GET /api/v1/treatmentGroup, GET /api/v1/reimbursementGroup, GET /api/v1/reimbursementGroup/{id}, and GET /api/v1/reimbursementGroup/filter)
    • The element shape (reference, type, identifier, display) is unchanged — only the array field name and the C# type name changed.
    • No backwards-compatible parallel field — consumers must migrate.

[3.1.0] - 2026-05-07

Added

  • medicines on ReimbursementGroup — exposes the medicines covered by a reimbursement authorization. Sourced from RegulatedAuthorization.ArticleNumber; emitted as a MedicinalReference[] (FEST/Varenummer). Empty list when upstream provides no article number. Non-breaking additive change.

[3.0.0] - 2026-04-30

Breaking Changes

  • Renamed Behandlingsgruppe model to TreatmentGroup — endpoint, model, and database table all renamed for consistency with ReimbursementGroup.
    • Endpoint: GET /api/v1/behandlingsgrupper → GET /api/v1/treatmentGroup
    • Table: Behandlingsgrupper → TreatmentGroups
    • Model shape unchanged (same fields: id, name, description, diseaseGroup, jurisdictions, validityPeriod, reviewDate, indications, treatmentAlternatives)
    • No backwards-compatible parallel endpoint — consumers must migrate.

Added

  • Per-document version metadata in response wrapper — VersionedItem<T> now exposes version (the sync version when this item last changed) and lastChanged (ISO-8601 timestamp). Applies to both treatmentGroup and reimbursementGroup endpoints.

[2.0.0] - 2026-04-28

Breaking Changes

  • Replaced Reimbursement model with ReimbursementGroup — the endpoint, model shape, and database table have all been renamed and restructured.

    • Endpoint: GET /api/v1/reimbursements → GET /api/v1/reimbursementGroup
    • Table: Reimbursements → ReimbursementGroups
    • Model is reduced to three fields: id, legalBasis (CodeableConcept), indications (CodeableConcept[])
    • The previous medicinalReferences array is removed pending a redesign
    • legalBasis now correctly carries the hjemmel (e.g., H-resept §950) sourced from the upstream reimbursementRegulation extension, rather than the first coding from Basis
    • indications carries the diagnoses (e.g., ICD-10 codes) sourced from Basis.Coding, with one CodeableConcept per coding

    Consumers using the old endpoint must migrate. There is no backwards-compatible parallel endpoint.

[1.1.0] - 2026-04-28

Added

  • Raw FHIR persistence — Upstream FHIR resources are now stored in Postgres alongside the converted output, sharing the same version number per sync.

    • New versioned collections: PlanDefinitions, ActivityDefinitions, RegulatedAuthorizations
    • Each wrapped in VersionedDocument<T> with the same upsert + soft-delete semantics as the output collections
    • Enables re-conversion if mapping logic changes, debugging via raw-vs-converted comparison, and full audit traceability
  • Sync history — Every sync run writes one entry to the new SyncHistory collection with:

    • Version, SyncedAt, DurationMs
    • Per-collection statistics (AddedCount, UpdatedCount, UnchangedCount, DeletedCount)
    • Business keys of every added, updated, and deleted record (AddedKeys, UpdatedKeys, DeletedKeys)
  • unchanged field in SyncResult — The POST /api/internal/sync response now includes a count of records that were detected as unchanged.

Changed

  • Content-hash-based change detection — VersionedDocument<T> has a new ContentHash field (SHA256 of BSON). Records only receive a new Version if their hash actually changed (or their deletion state flipped). Previously every sync bumped the version on every record, which made ?sinceVersion=N return everything after each sync. Now diff queries return only the records that genuinely changed.

    • Migration impact: The first sync after deploy bumps the version on all existing records once (because they have no hash to compare against), then subsequent syncs only touch genuinely changed records.
  • Sync log line — Now includes unchanged count in the structured log message.

Internal

  • PlanDefinition no longer has a [BsonId] attribute — it is now embedded inside VersionedDocument<PlanDefinition>, and the wrapper holds the database primary key.
  • UpsertCollection<T> in SyncService now returns SyncCollectionStats (counts plus business keys) instead of a counts-only tuple.

[1.0.0] - 2026-04-13

Added

  • Behandlingsgrupper endpoint (GET /api/v1/behandlingsgrupper)

    • Returns treatment groups with ranked treatment alternatives
    • Includes jurisdictions, indications, validity periods, and medicine references
  • Reimbursements endpoint (GET /api/v1/reimbursements)

    • Returns reimbursement authorizations with regulation basis and indication codes
    • Includes medicine references (FEST and NOMPD)
  • Version-based diff support

    • All endpoints accept ?since-version=N to return only changes since a given version
    • Responses include currentVersion for clients to track their sync state
    • Deleted items are included in diff responses with isDeleted: true
  • API key authentication

    • All endpoints require X-API-KEY header
    • Keys are configured server-side and issued manually
  • Sync endpoint (POST /api/internal/sync)

    • Triggers a full data sync from upstream FHIR APIs
    • Intended to be called by a Kubernetes CronJob
    • Returns sync statistics (added, updated, deleted, duration)

Søk i Utviklerportalen

Søket er fullført!