Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
08c8be2
Terminology: replace a cached code group's codes from an uploaded CSV
MikeAtPinnacle Aug 4, 2026
9a6dedb
LEGLINK-889: Add cached ValueSet member lookup and harden CSV status …
MikeAtPinnacle Aug 12, 2026
21ae0ca
Merge branch 'users/mtherien/terminology-csv-upload' into users/mther…
MikeAtPinnacle Aug 12, 2026
95827f7
TECH_DEBT: Refresh the Mock DMRP API package lock file
MikeAtPinnacle Aug 12, 2026
1bba019
Merge branch 'dev' into users/mtherien/leglink-889-fix-test-endpoint
MikeAtPinnacle Aug 12, 2026
8fa5b00
Merge branch 'dev' into users/mtherien/leglink-889-fix-test-endpoint
MikeAtPinnacle Aug 12, 2026
be496be
Merge branch 'dev' into users/mtherien/leglink-889-fix-test-endpoint
MikeAtPinnacle Aug 12, 2026
94b088c
LEGLINK-889: Address CodeRabbit review findings on the terminology co…
MikeAtPinnacle Aug 12, 2026
b3c7bb1
Merge branch 'users/mtherien/leglink-889-fix-test-endpoint' of https:…
MikeAtPinnacle Aug 12, 2026
25b429f
LEGLINK-889: Stop the multi-system ValueSet lookup test depending on …
MikeAtPinnacle Aug 12, 2026
7a22bdd
LEGLINK-889: Make the cache-key accumulation test actually check for …
MikeAtPinnacle Aug 12, 2026
242ba5c
Merge remote-tracking branch 'origin/dev' into users/mtherien/leglink…
MikeAtPinnacle Aug 12, 2026
105e615
LEGLINK-889: Require the admin policy on the terminology code upload …
MikeAtPinnacle Aug 12, 2026
eda709c
LEGLINK-889: Truncate the values in the unrecognized-status warning
MikeAtPinnacle Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace LantanaGroup.Link.Terminology.Application.Exceptions;

/// <summary>
/// Thrown when an operation names a code group that is not present in the cache.
/// </summary>
/// <remarks>
/// Distinct from a bare <see cref="KeyNotFoundException"/> so that callers can map "the caller asked for
/// something that is not cached" (a 404) without also catching the dictionary lookups that fail inside the
/// cache's own loading code (a defect, which must surface as a 500 with a traceId rather than being
/// disguised as a not-found answer). It derives from <see cref="KeyNotFoundException"/> so the narrower
/// contract stays source-compatible with any caller still catching the base type.
/// </remarks>
public class CodeGroupNotFoundException : KeyNotFoundException
{
/// <summary>
/// Creates the exception with a message describing the code group that could not be found.
/// </summary>
public CodeGroupNotFoundException(string message) : base(message)
{
}

/// <summary>
/// Creates the exception with a message and the underlying cause.
/// </summary>
public CodeGroupNotFoundException(string message, Exception innerException) : base(message, innerException)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ public interface ICodeGroupCacheService
/// </summary>
List<CodeGroup> GetAllCodeGroups(CodeGroup.CodeGroupTypes type);

/// <summary>
/// Replaces the codes of an already-cached code group with the contents of a CSV, preserving the
/// FHIR resource metadata (url, version, id, name, identifiers) loaded from disk.
/// </summary>
/// <remarks>
/// Affects this instance's in-memory cache only: nothing is written to the configured terminology
/// path, and <see cref="LoadCache"/> restores the on-disk state. The replacement is all-or-nothing β€”
/// a CSV that fails to parse leaves the previously cached codes untouched.
/// </remarks>
/// <param name="type">The kind of code group to replace.</param>
/// <param name="id">The resource id of the code group.</param>
/// <param name="version">The version to replace, or null for the latest cached version.</param>
/// <param name="csvContent">
/// The CSV content, including a header row. A CodeSystem CSV has 2 or 3 columns (code, display and
/// optionally status); a ValueSet CSV has 3 or 4 (system, code, display and optionally status).
/// </param>
/// <param name="cancellationToken">Token to indicate if the operation should be cancelled.</param>
/// <returns>The replaced code group, carrying the codes just loaded.</returns>
/// <exception cref="Exceptions.CodeGroupNotFoundException">No such code group is cached.</exception>
/// <exception cref="InvalidOperationException">The CSV does not have a supported number of columns.</exception>
CodeGroup ReplaceCodesFromCsv(CodeGroup.CodeGroupTypes type, string id, string? version, string csvContent, CancellationToken cancellationToken = default);

/// <summary>
/// Clears all cached code groups.
/// </summary>
Expand All @@ -31,5 +53,5 @@ public interface ICodeGroupCacheService
/// <summary>
/// Loads (or reloads) the cache from the configured terminology source.
/// </summary>
Task LoadCache();
Task LoadCache(CancellationToken cancellationToken = default);
}
13 changes: 9 additions & 4 deletions DotNet/Terminology/Application/Models/CsvCodeSystemRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ public class CsvCodeSystemRecord
public required string Display { get; set; }

/// <summary>
/// Indicates the status of the code item, Active or Inactive
/// The raw status cell, expected to read "Active" or "Inactive" in any casing. Empty in a
/// two-column file, which has no status column at all.
/// </summary>
/// <remarks>
/// Deliberately a string rather than a <see cref="CodeStatus"/>: CsvHelper's enum converter throws a
/// <c>TypeConverterException</c> on any other value, and because the records are enumerated lazily that
/// throw escapes the read loop and costs the entire code system, not the one bad row. Interpreting the
/// cell is left to the loader, which defaults it and logs what it saw.
/// </remarks>
[Index(2)]
[Default(CodeStatus.Active)]
[EnumIgnoreCase]
public CodeStatus Status { get; set; } = CodeStatus.Active;
public string? Status { get; set; }
}
40 changes: 40 additions & 0 deletions DotNet/Terminology/Application/Models/ReplaceCodesResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
namespace LantanaGroup.Link.Terminology.Application.Models;

/// <summary>
/// Summarises the codes loaded into a code group by a CSV upload, so the caller can confirm what the
/// running instance now holds without a follow-up expand.
/// </summary>
public class ReplaceCodesResponse
{
/// <summary>The kind of code group that was replaced, "CodeSystem" or "ValueSet".</summary>
public required string Type { get; init; }

/// <summary>The resource id of the replaced code group.</summary>
public required string Id { get; init; }

/// <summary>The version of the replaced code group, as loaded from its FHIR resource.</summary>
public string? Version { get; init; }

/// <summary>The total number of codes loaded from the CSV.</summary>
public required int CodeCount { get; init; }

/// <summary>
/// The number of distinct code systems the codes belong to. Always 1 for a CodeSystem; a ValueSet
/// spans as many systems as its CSV's first column names.
/// </summary>
public required int SystemCount { get; init; }

/// <summary>
/// How many of the loaded codes carry an Inactive status.
/// </summary>
/// <remarks>
/// A ValueSet CSV without the optional fourth column loads members with no membership status at all,
/// which reports zero here and leaves each code's status to be resolved from its code system. If a
/// test expects inactive members, a non-zero count here is the confirmation that the status column
/// was read.
/// </remarks>
public required int InactiveCodeCount { get; init; }

/// <summary>The name of the uploaded file, echoed back to confirm which CSV was applied.</summary>
public string? FileName { get; init; }
}
46 changes: 46 additions & 0 deletions DotNet/Terminology/Application/Models/ValueSetCodeLookupResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace LantanaGroup.Link.Terminology.Application.Models;

/// <summary>
/// The response of the cached ValueSet code lookup: one member of a cached value set, reported with both
/// the status the value set itself declares and the status that will actually be applied.
/// </summary>
/// <remarks>
/// The two statuses are separate because they answer different questions, and conflating them is what
/// LEGLINK-889 was raised over. A value set may declare its own membership status (the optional fourth CSV
/// column added by LEGLINK-639), in which case it overrides the code system; a value set with no status
/// column declares nothing and its members inherit whatever the CodeSystem says. Reporting only one number
/// leaves a caller unable to tell "this value set says inactive" from "this value set is silent and the
/// code system says inactive" β€” and unable to tell either from a code system edit that never took effect.
/// </remarks>
public class ValueSetCodeLookupResult
{
/// <summary>
/// The code system URI the member was found under. A value set groups its members by system, so this
/// names which of them matched β€” it is the system the effective status was rejoined from.
/// </summary>
public required string System { get; set; }

/// <summary>
/// The code value.
/// </summary>
public required string Value { get; set; }

/// <summary>
/// The human-readable display text the value set carries for the code.
/// </summary>
public required string Display { get; set; }

/// <summary>
/// The status the value set declares for this member, or <c>null</c> when the value set's CSV has no
/// status column and therefore declares nothing. Null is not the same as Active: it means the question
/// is deferred to the code system.
/// </summary>
public CodeStatus? MembershipStatus { get; set; }

/// <summary>
/// The status that applies β€” the declared membership status when there is one, otherwise the status
/// rejoined from the cached CodeSystem for <see cref="System"/>. This is the value
/// <c>ValueSet/$validate-code</c> acts on, so the two always agree.
/// </summary>
public required CodeStatus EffectiveStatus { get; set; }
}
11 changes: 11 additions & 0 deletions DotNet/Terminology/Application/Settings/TerminologyConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,15 @@ public class TerminologyConfig
/// The path where all terminology artifacts are loaded from the server's local file system.
/// </summary>
public required string Path { get; init; }

/// <summary>
/// Enables the endpoints that replace a cached code group's codes from an uploaded CSV.
/// Intended for testing only and must remain false in production.
/// </summary>
/// <remarks>
/// Deliberately not <c>required</c> and deliberately unvalidated at startup: a missing key leaves
/// this false, so the feature fails closed in any environment whose configuration store never got
/// the row. The endpoints report themselves as not found while it is false.
/// </remarks>
public bool EnableCodeUploadEndpoint { get; init; }
}
Loading
Loading