From 29459f5345108ae2371cc74bb28423ce4db51a5c Mon Sep 17 00:00:00 2001 From: George Rios Date: Tue, 23 Jun 2026 15:58:32 -0400 Subject: [PATCH] feat: add resolveCarrier() and listCarrierGroups() to match TS SDK Mirror the new TypeScript SDK carrier surface: - ListCarrierGroupsAsync() -> GET /carriers/groups - ResolveCarrierAsync(groupId, file) -> POST /carriers/resolve (multipart) Adds CarrierGroup, CarrierResolveResult, CarrierResolveCandidate models and a tolerant ProductLine enum. Bumps package version to 1.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 25 +++++++ src/CommissionSight/CommissionSight.csproj | 2 +- src/CommissionSight/CommissionSightClient.cs | 19 +++++ src/CommissionSight/Models/CoreModels.cs | 43 +++++++++++ src/CommissionSight/Models/Enums.cs | 9 +++ test/CommissionSight.Tests/ClientTests.cs | 79 ++++++++++++++++++++ 6 files changed, 176 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 178ed4e..c861767 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,31 @@ while (true) } ``` +## Carriers + +List carriers, or list carrier **groups** (brands like "UHC" that fan out to a concrete carrier per +product line). When you have a statement but don't know which member carrier it's from, `ResolveCarrierAsync` +matches a brand + a sample file to ranked candidates: + +```csharp +var groups = await client.ListCarrierGroupsAsync(); +var uhc = groups.Data.First(g => g.Slug == "uhc"); + +using var sample = FileUpload.FromFile("mystery-statement.csv"); +var resolved = await client.ResolveCarrierAsync(uhc.Id, sample); + +if (!resolved.Ambiguous && resolved.Best is { } best) +{ + Console.WriteLine($"Looks like {best.Name} ({best.ProductLine}) — {best.Confidence:P0} confident"); + await client.UploadFileAsync(sample, best.CarrierId, 2026, 4); +} +else +{ + foreach (var c in resolved.Ranked) + Console.WriteLine($" candidate {c.Name} ({c.ProductLine}): {c.Confidence:P0} — {c.Reason}"); +} +``` + ## Reports & analytics ```csharp diff --git a/src/CommissionSight/CommissionSight.csproj b/src/CommissionSight/CommissionSight.csproj index 1869b0a..e7a727d 100644 --- a/src/CommissionSight/CommissionSight.csproj +++ b/src/CommissionSight/CommissionSight.csproj @@ -19,7 +19,7 @@ CommissionSight - 1.1.0 + 1.2.0 CommissionSight CommissionSight CommissionSight .NET SDK diff --git a/src/CommissionSight/CommissionSightClient.cs b/src/CommissionSight/CommissionSightClient.cs index 41ab04f..0689feb 100644 --- a/src/CommissionSight/CommissionSightClient.cs +++ b/src/CommissionSight/CommissionSightClient.cs @@ -227,6 +227,25 @@ public Task> ListCarriersAsync(bool withConfig = false, Can public Task GetCarrierAsync(string carrierId, CancellationToken cancellationToken = default) => SendAsync(HttpMethod.Get, $"/carriers/{carrierId}", cancellationToken: cancellationToken); + /// List carrier groups (brands) and their per-product member carriers. + public Task> ListCarrierGroupsAsync(CancellationToken cancellationToken = default) => + SendAsync>(HttpMethod.Get, "/carriers/groups", cancellationToken: cancellationToken); + + /// + /// Resolve a carrier brand + a sample statement file to the concrete member carrier + /// it belongs to. Returns ranked candidates; check Ambiguous before trusting + /// Best. + /// + public Task ResolveCarrierAsync(string groupId, FileUpload file, CancellationToken cancellationToken = default) + { + var form = new MultipartFormDataContent + { + { new StringContent(groupId), "groupId" }, + { FileContent(file), "file", file.FileName }, + }; + return SendAsync(HttpMethod.Post, "/carriers/resolve", form, cancellationToken: cancellationToken); + } + /// List a carrier's config versions (global + your overrides). public Task> ListConfigsAsync(string carrierId, CancellationToken cancellationToken = default) => SendAsync>(HttpMethod.Get, $"/carriers/{carrierId}/configs", cancellationToken: cancellationToken); diff --git a/src/CommissionSight/Models/CoreModels.cs b/src/CommissionSight/Models/CoreModels.cs index eefafaf..3c800b5 100644 --- a/src/CommissionSight/Models/CoreModels.cs +++ b/src/CommissionSight/Models/CoreModels.cs @@ -118,6 +118,49 @@ public sealed record JobResultsPage // ─── Carriers & configs ─────────────────────────────────────────────────────── +/// A carrier brand (e.g. "UHC") and its per-product member carriers. +public sealed record CarrierGroup +{ + public required string Id { get; init; } + public required string Name { get; init; } + public required string Slug { get; init; } + + /// The concrete carriers (one per product line) that belong to this brand. + public IReadOnlyList Members { get; init; } = []; +} + +/// One ranked candidate from ResolveCarrierAsync. +public sealed record CarrierResolveCandidate +{ + public required string CarrierId { get; init; } + public string? Name { get; init; } + public string? Slug { get; init; } + public ProductLine ProductLine { get; init; } + + /// Match confidence, 0..1. + public double Confidence { get; init; } + + /// Why this candidate matched (human-readable). + public required string Reason { get; init; } +} + +/// +/// Result of resolving a brand + sample file to a concrete carrier. When +/// is true, multiple candidates scored closely — inspect +/// rather than trusting alone. +/// +public sealed record CarrierResolveResult +{ + public required string GroupId { get; init; } + public bool Ambiguous { get; init; } + + /// The top candidate, or null when nothing matched. + public CarrierResolveCandidate? Best { get; init; } + + /// All candidates, most confident first. + public IReadOnlyList Ranked { get; init; } = []; +} + /// Result of creating a carrier config. public sealed record CreateConfigResult { diff --git a/src/CommissionSight/Models/Enums.cs b/src/CommissionSight/Models/Enums.cs index 351f262..47d82f6 100644 --- a/src/CommissionSight/Models/Enums.cs +++ b/src/CommissionSight/Models/Enums.cs @@ -75,6 +75,15 @@ public enum WebhookEvent [JsonStringEnumMemberName("job.failed")] JobFailed, } +/// The product line a carrier writes business in. +public enum ProductLine +{ + Unknown = 0, + [JsonStringEnumMemberName("major_medical")] MajorMedical, + [JsonStringEnumMemberName("medicare")] Medicare, + [JsonStringEnumMemberName("ancillary")] Ancillary, +} + /// Account lifecycle status. public enum AccountStatus { diff --git a/test/CommissionSight.Tests/ClientTests.cs b/test/CommissionSight.Tests/ClientTests.cs index fa8247d..7f917f2 100644 --- a/test/CommissionSight.Tests/ClientTests.cs +++ b/test/CommissionSight.Tests/ClientTests.cs @@ -252,6 +252,85 @@ public async Task CreateWorkspace_posts_name_and_parses_workspace() Assert.False(ws.IsDefault); } + [Fact] + public async Task ListCarrierGroups_parses_brand_and_members() + { + var (client, handler) = Make(_ => StubHttpMessageHandler.Json( + """ + { + "data": [{ + "id": "grp_uhc", "name": "UHC", "slug": "uhc", + "members": [ + { "id": "car_uhc_med", "name": "UHC Medicare", "slug": "uhc-medicare" }, + { "id": "car_uhc_anc", "name": "UHC Ancillary", "slug": "uhc-ancillary" } + ] + }], + "pagination": { "limit": 50, "offset": 0, "hasMore": false } + } + """)); + + var page = await client.ListCarrierGroupsAsync(); + + Assert.Equal($"{BaseUrl}/carriers/groups", handler.LastRequest!.RequestUri!.ToString()); + var group = Assert.Single(page.Data); + Assert.Equal("UHC", group.Name); + Assert.Equal(2, group.Members.Count); + Assert.Equal("UHC Medicare", group.Members[0].Name); + } + + [Fact] + public async Task ResolveCarrier_posts_multipart_and_parses_ranked_candidates() + { + var (client, handler) = Make(_ => StubHttpMessageHandler.Json( + """ + { + "groupId": "grp_uhc", + "ambiguous": false, + "best": { "carrierId": "car_uhc_med", "name": "UHC Medicare", "slug": "uhc-medicare", + "productLine": "medicare", "confidence": 0.92, "reason": "matched plan-code column" }, + "ranked": [ + { "carrierId": "car_uhc_med", "name": "UHC Medicare", "slug": "uhc-medicare", + "productLine": "medicare", "confidence": 0.92, "reason": "matched plan-code column" }, + { "carrierId": "car_uhc_anc", "name": "UHC Ancillary", "slug": "uhc-ancillary", + "productLine": "ancillary", "confidence": 0.31, "reason": "weak header overlap" } + ] + } + """)); + + var file = FileUpload.FromBytes("a,b\n1,2\n"u8.ToArray(), "statement.csv", "text/csv"); + var result = await client.ResolveCarrierAsync("grp_uhc", file); + + Assert.Equal(HttpMethod.Post, handler.LastRequest!.Method); + Assert.Equal($"{BaseUrl}/carriers/resolve", handler.LastRequest.RequestUri!.ToString()); + Assert.StartsWith("multipart/form-data", handler.LastRequest.Content!.Headers.ContentType!.MediaType); + Assert.False(result.Ambiguous); + Assert.Equal("car_uhc_med", result.Best!.CarrierId); + Assert.Equal(ProductLine.Medicare, result.Best.ProductLine); + Assert.Equal(0.92, result.Best.Confidence); + Assert.Equal(2, result.Ranked.Count); + Assert.Equal(ProductLine.Ancillary, result.Ranked[1].ProductLine); + } + + [Fact] + public async Task ResolveCarrier_unknown_product_line_falls_back_to_Unknown() + { + var (client, _) = Make(_ => StubHttpMessageHandler.Json( + """ + { + "groupId": "grp_x", "ambiguous": true, "best": null, + "ranked": [{ "carrierId": "car_x", "name": null, "slug": null, + "productLine": "dental_supplemental", "confidence": 0.1, "reason": "?" }] + } + """)); + + var file = FileUpload.FromBytes("x"u8.ToArray(), "s.csv", "text/csv"); + var result = await client.ResolveCarrierAsync("grp_x", file); + + Assert.True(result.Ambiguous); + Assert.Null(result.Best); + Assert.Equal(ProductLine.Unknown, Assert.Single(result.Ranked).ProductLine); + } + [Fact] public async Task Health_works_without_a_token() {