Skip to content

Commit c42ad9c

Browse files
authored
Merge pull request #481 from aurelianware/claude/port-fhir-compliance-checker-zffzM
2 parents 74873ff + fd9bf87 commit c42ad9c

File tree

3 files changed

+835
-0
lines changed

3 files changed

+835
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
using FhirService.Services;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Mvc;
4+
5+
namespace FhirService.Controllers;
6+
7+
/// <summary>
8+
/// CMS-0057-F compliance self-assessment endpoint.
9+
/// Returns a structured report of which CMS-0057-F requirements are met/unmet
10+
/// for the current tenant — a key differentiator for CHO health plans.
11+
/// </summary>
12+
[Route("fhir/r4")]
13+
[Authorize]
14+
public class ComplianceController : FhirControllerBase
15+
{
16+
private readonly IConfiguration _config;
17+
private readonly ICms0057ComplianceChecker _complianceChecker;
18+
19+
public ComplianceController(IConfiguration config, ICms0057ComplianceChecker complianceChecker)
20+
{
21+
_config = config;
22+
_complianceChecker = complianceChecker;
23+
}
24+
25+
/// <summary>
26+
/// GET /fhir/r4/compliance-status
27+
/// Returns a structured report of CMS-0057-F compliance posture for the current tenant.
28+
/// </summary>
29+
[HttpGet("compliance-status")]
30+
[Produces("application/json")]
31+
[ProducesResponseType(typeof(Cms0057ComplianceReport), 200)]
32+
public IActionResult GetComplianceStatus()
33+
{
34+
var tenantId = TenantId;
35+
36+
var patientAccessCheck = CheckPatientAccessApi();
37+
var providerDirectoryCheck = CheckProviderDirectoryApi();
38+
var priorAuthCheck = CheckPriorAuthorizationApi();
39+
var payerToPayerCheck = CheckPayerToPayerExchange();
40+
var smartScopesCheck = CheckSmartOnFhirScopes();
41+
42+
var requirements = new List<Cms0057Requirement>
43+
{
44+
patientAccessCheck,
45+
providerDirectoryCheck,
46+
priorAuthCheck,
47+
payerToPayerCheck,
48+
smartScopesCheck
49+
};
50+
51+
var metCount = requirements.Count(r => r.Met);
52+
53+
var supportedResourceTypes = _complianceChecker.SupportedResourceTypes;
54+
55+
var report = new Cms0057ComplianceReport(
56+
TenantId: tenantId,
57+
OverallCompliant: requirements.All(r => r.Met),
58+
RequirementsMet: metCount,
59+
TotalRequirements: requirements.Count,
60+
CompliancePercentage: (int)Math.Round(100.0 * metCount / requirements.Count),
61+
Requirements: requirements,
62+
SupportedValidationResources: supportedResourceTypes,
63+
AssessedAt: DateTimeOffset.UtcNow,
64+
FhirVersion: "4.0.1",
65+
RuleName: "CMS-0057-F",
66+
RuleDescription: "CMS Interoperability and Prior Authorization Final Rule");
67+
68+
return Ok(report);
69+
}
70+
71+
/// <summary>
72+
/// Patient Access API: must be enabled and return valid FHIR R4.
73+
/// </summary>
74+
private Cms0057Requirement CheckPatientAccessApi()
75+
{
76+
var issues = new List<string>();
77+
78+
var patientAccessEnabled = _config.GetValue("Cms0057:PatientAccessApi:Enabled", true);
79+
if (!patientAccessEnabled)
80+
issues.Add("Patient Access API is not enabled");
81+
82+
var fhirVersion = _config["Cms0057:PatientAccessApi:FhirVersion"] ?? "4.0.1";
83+
if (!fhirVersion.StartsWith("4.", StringComparison.Ordinal))
84+
issues.Add($"FHIR version {fhirVersion} is not R4-compliant; must be 4.x");
85+
86+
var requiredResources = new[] { "Patient", "Coverage", "ExplanationOfBenefit", "Encounter" };
87+
var enabledResources = _config.GetSection("Cms0057:PatientAccessApi:Resources")
88+
.GetChildren().Select(c => c.Value).ToList();
89+
90+
// If no explicit config, assume all required resources are available (convention over configuration)
91+
if (enabledResources.Count > 0)
92+
{
93+
var missing = requiredResources.Except(enabledResources!, StringComparer.OrdinalIgnoreCase).ToList();
94+
if (missing.Count > 0)
95+
issues.Add($"Missing required resources: {string.Join(", ", missing)}");
96+
}
97+
98+
return new Cms0057Requirement(
99+
Id: "CMS-0057-F-01",
100+
Name: "Patient Access API",
101+
Description: "Patient Access API is enabled and returns valid FHIR R4 resources (Patient, Coverage, EOB, Encounter)",
102+
Met: issues.Count == 0,
103+
Issues: issues);
104+
}
105+
106+
/// <summary>
107+
/// Provider Directory API: must be enabled.
108+
/// </summary>
109+
private Cms0057Requirement CheckProviderDirectoryApi()
110+
{
111+
var issues = new List<string>();
112+
113+
var enabled = _config.GetValue("Cms0057:ProviderDirectoryApi:Enabled", true);
114+
if (!enabled)
115+
issues.Add("Provider Directory API is not enabled");
116+
117+
// Program.cs registers NppesApi HttpClient with a default base URL when Nppes:BaseUrl
118+
// is absent, so the provider directory is functional even without explicit config.
119+
// Only flag as non-compliant if the config explicitly disables NPPES.
120+
var nppesDisabled = _config.GetValue("Nppes:Disabled", false);
121+
if (nppesDisabled)
122+
issues.Add("NPPES integration is explicitly disabled for provider directory lookups");
123+
124+
return new Cms0057Requirement(
125+
Id: "CMS-0057-F-02",
126+
Name: "Provider Directory API",
127+
Description: "Provider Directory API is enabled and supports provider lookups",
128+
Met: issues.Count == 0,
129+
Issues: issues);
130+
}
131+
132+
/// <summary>
133+
/// Prior Authorization API: must support required operations ($submit, $inquire, status polling).
134+
/// </summary>
135+
private Cms0057Requirement CheckPriorAuthorizationApi()
136+
{
137+
var issues = new List<string>();
138+
139+
var enabled = _config.GetValue("Cms0057:PriorAuthorizationApi:Enabled", true);
140+
if (!enabled)
141+
{
142+
issues.Add("Prior Authorization API is not enabled");
143+
return new Cms0057Requirement(
144+
Id: "CMS-0057-F-03",
145+
Name: "Prior Authorization API",
146+
Description: "Prior Authorization API supports $submit, $inquire operations and status polling per Da Vinci PAS",
147+
Met: false,
148+
Issues: issues);
149+
}
150+
151+
var requiredOperations = new[] { "$submit", "$inquire" };
152+
var supportedOperations = _config.GetSection("Cms0057:PriorAuthorizationApi:Operations")
153+
.GetChildren().Select(c => c.Value).ToList();
154+
155+
if (supportedOperations.Count == 0)
156+
{
157+
issues.Add($"Required operations are not explicitly configured: {string.Join(", ", requiredOperations)}");
158+
}
159+
else
160+
{
161+
var missing = requiredOperations.Except(supportedOperations!, StringComparer.OrdinalIgnoreCase).ToList();
162+
if (missing.Count > 0)
163+
issues.Add($"Missing required operations: {string.Join(", ", missing)}");
164+
}
165+
166+
var timelineEnforcement = _config.GetValue("Cms0057:PriorAuthorizationApi:TimelineEnforcement", true);
167+
if (!timelineEnforcement)
168+
issues.Add("Timeline enforcement (72h urgent / 7d standard) is not enabled");
169+
170+
return new Cms0057Requirement(
171+
Id: "CMS-0057-F-03",
172+
Name: "Prior Authorization API",
173+
Description: "Prior Authorization API supports $submit, $inquire operations and status polling per Da Vinci PAS",
174+
Met: issues.Count == 0,
175+
Issues: issues);
176+
}
177+
178+
/// <summary>
179+
/// Payer-to-Payer data exchange: must be configured.
180+
/// </summary>
181+
private Cms0057Requirement CheckPayerToPayerExchange()
182+
{
183+
var issues = new List<string>();
184+
185+
var enabled = _config.GetValue("Cms0057:PayerToPayerExchange:Enabled", false);
186+
if (!enabled)
187+
issues.Add("Payer-to-Payer data exchange is not enabled");
188+
189+
var hasEndpoint = !string.IsNullOrEmpty(_config["Cms0057:PayerToPayerExchange:Endpoint"]);
190+
if (!hasEndpoint)
191+
issues.Add("Payer-to-Payer exchange endpoint is not configured");
192+
193+
return new Cms0057Requirement(
194+
Id: "CMS-0057-F-04",
195+
Name: "Payer-to-Payer Data Exchange",
196+
Description: "Payer-to-Payer data exchange is configured for member transitions between health plans",
197+
Met: issues.Count == 0,
198+
Issues: issues);
199+
}
200+
201+
/// <summary>
202+
/// SMART on FHIR scopes: required scopes must be registered.
203+
/// </summary>
204+
private Cms0057Requirement CheckSmartOnFhirScopes()
205+
{
206+
var issues = new List<string>();
207+
208+
var requiredScopes = new[]
209+
{
210+
"patient/Patient.read",
211+
"patient/Coverage.read",
212+
"patient/ExplanationOfBenefit.read",
213+
"user/Patient.read",
214+
"user/Coverage.read",
215+
"user/ExplanationOfBenefit.read",
216+
"launch/patient"
217+
};
218+
219+
var registeredScopes = _config.GetSection("Cms0057:SmartScopes:Registered")
220+
.GetChildren().Select(c => c.Value).ToList();
221+
222+
if (registeredScopes.Count > 0)
223+
{
224+
var missing = requiredScopes.Except(registeredScopes!, StringComparer.OrdinalIgnoreCase).ToList();
225+
if (missing.Count > 0)
226+
issues.Add($"Missing required SMART scopes: {string.Join(", ", missing)}");
227+
}
228+
229+
// Check that SMART configuration endpoint exists
230+
var smartConfigured = !string.IsNullOrEmpty(_config["SmartAuth:Issuer"]);
231+
if (!smartConfigured)
232+
issues.Add("SMART on FHIR authorization server (SmartAuth:Issuer) is not configured");
233+
234+
return new Cms0057Requirement(
235+
Id: "CMS-0057-F-05",
236+
Name: "SMART on FHIR Scopes",
237+
Description: "Required SMART on FHIR scopes are registered for patient and user access",
238+
Met: issues.Count == 0,
239+
Issues: issues);
240+
}
241+
}
242+
243+
// ── Response DTOs ────────────────────────────────────────────────────────────
244+
245+
public record Cms0057ComplianceReport(
246+
string TenantId,
247+
bool OverallCompliant,
248+
int RequirementsMet,
249+
int TotalRequirements,
250+
int CompliancePercentage,
251+
IReadOnlyList<Cms0057Requirement> Requirements,
252+
IReadOnlyList<string> SupportedValidationResources,
253+
DateTimeOffset AssessedAt,
254+
string FhirVersion,
255+
string RuleName,
256+
string RuleDescription);
257+
258+
public record Cms0057Requirement(
259+
string Id,
260+
string Name,
261+
string Description,
262+
bool Met,
263+
IReadOnlyList<string> Issues);

src/services/fhir-service/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
builder.Services.AddSingleton<IFhirDataAdapter, MockFhirDataAdapter>();
4242
builder.Services.AddSingleton<FhirBundleBuilder>();
4343
builder.Services.AddSingleton<IPatientAccessDataProvider, MockPatientAccessDataProvider>();
44+
builder.Services.AddSingleton<ICms0057ComplianceChecker, Cms0057ComplianceChecker>();
4445

4546
// ── Provider Directory: typed HttpClient for NPPES API ────────────────────────
4647
builder.Services.AddHttpClient("NppesApi", client =>

0 commit comments

Comments
 (0)