Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/CommissionSight/CommissionSight.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<!-- NuGet package metadata -->
<PropertyGroup>
<PackageId>CommissionSight</PackageId>
<Version>1.1.0</Version>
<Version>1.2.0</Version>
<Authors>CommissionSight</Authors>
<Company>CommissionSight</Company>
<Product>CommissionSight .NET SDK</Product>
Expand Down
19 changes: 19 additions & 0 deletions src/CommissionSight/CommissionSightClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,25 @@ public Task<Page<CarrierSummary>> ListCarriersAsync(bool withConfig = false, Can
public Task<CarrierSummary> GetCarrierAsync(string carrierId, CancellationToken cancellationToken = default) =>
SendAsync<CarrierSummary>(HttpMethod.Get, $"/carriers/{carrierId}", cancellationToken: cancellationToken);

/// <summary>List carrier groups (brands) and their per-product member carriers.</summary>
public Task<Page<CarrierGroup>> ListCarrierGroupsAsync(CancellationToken cancellationToken = default) =>
SendAsync<Page<CarrierGroup>>(HttpMethod.Get, "/carriers/groups", cancellationToken: cancellationToken);

/// <summary>
/// Resolve a carrier brand + a sample statement file to the concrete member carrier
/// it belongs to. Returns ranked candidates; check <c>Ambiguous</c> before trusting
/// <c>Best</c>.
/// </summary>
public Task<CarrierResolveResult> ResolveCarrierAsync(string groupId, FileUpload file, CancellationToken cancellationToken = default)
{
var form = new MultipartFormDataContent
{
{ new StringContent(groupId), "groupId" },
{ FileContent(file), "file", file.FileName },
};
return SendAsync<CarrierResolveResult>(HttpMethod.Post, "/carriers/resolve", form, cancellationToken: cancellationToken);
}

/// <summary>List a carrier's config versions (global + your overrides).</summary>
public Task<Page<JsonElement>> ListConfigsAsync(string carrierId, CancellationToken cancellationToken = default) =>
SendAsync<Page<JsonElement>>(HttpMethod.Get, $"/carriers/{carrierId}/configs", cancellationToken: cancellationToken);
Expand Down
43 changes: 43 additions & 0 deletions src/CommissionSight/Models/CoreModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,49 @@ public sealed record JobResultsPage

// ─── Carriers & configs ───────────────────────────────────────────────────────

/// <summary>A carrier brand (e.g. "UHC") and its per-product member carriers.</summary>
public sealed record CarrierGroup
{
public required string Id { get; init; }
public required string Name { get; init; }
public required string Slug { get; init; }

/// <summary>The concrete carriers (one per product line) that belong to this brand.</summary>
public IReadOnlyList<CarrierSummary> Members { get; init; } = [];
}

/// <summary>One ranked candidate from <c>ResolveCarrierAsync</c>.</summary>
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; }

/// <summary>Match confidence, 0..1.</summary>
public double Confidence { get; init; }

/// <summary>Why this candidate matched (human-readable).</summary>
public required string Reason { get; init; }
}

/// <summary>
/// Result of resolving a brand + sample file to a concrete carrier. When
/// <see cref="Ambiguous"/> is true, multiple candidates scored closely — inspect
/// <see cref="Ranked"/> rather than trusting <see cref="Best"/> alone.
/// </summary>
public sealed record CarrierResolveResult
{
public required string GroupId { get; init; }
public bool Ambiguous { get; init; }

/// <summary>The top candidate, or null when nothing matched.</summary>
public CarrierResolveCandidate? Best { get; init; }

/// <summary>All candidates, most confident first.</summary>
public IReadOnlyList<CarrierResolveCandidate> Ranked { get; init; } = [];
}

/// <summary>Result of creating a carrier config.</summary>
public sealed record CreateConfigResult
{
Expand Down
9 changes: 9 additions & 0 deletions src/CommissionSight/Models/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ public enum WebhookEvent
[JsonStringEnumMemberName("job.failed")] JobFailed,
}

/// <summary>The product line a carrier writes business in.</summary>
public enum ProductLine
{
Unknown = 0,
[JsonStringEnumMemberName("major_medical")] MajorMedical,
[JsonStringEnumMemberName("medicare")] Medicare,
[JsonStringEnumMemberName("ancillary")] Ancillary,
}

/// <summary>Account lifecycle status.</summary>
public enum AccountStatus
{
Expand Down
79 changes: 79 additions & 0 deletions test/CommissionSight.Tests/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading