feat: dev infrastructure, mock removal, k8s standardization#527
feat: dev infrastructure, mock removal, k8s standardization#527aurelianware merged 6 commits intomainfrom
Conversation
- docker-compose.development.yml: full 11-service backend + MongoDB/Redis + seed container - .env.example: configurable credentials for local dev - appsettings.Development.json: portal config pointing to localhost docker-compose ports - seed-demo-users.js: 11 tenant users across 7 departments - seed-demo-data.js: rewritten with 525 documents across 12 collections (Members, Claims, Providers, BenefitPlans, Sponsors, Authorizations, WorkQueueItems, Payments, Accumulators, Appeals, Correspondence, EnrollmentFiles) with full cross-referential integrity - README.md: added Full Development Stack section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace all mock data fallbacks in ServiceImplementations.cs with ServiceUnavailableException. When a backend service is down, users now see a clear error message instead of fake data pretending to be real. - ServiceImplementations.cs: 5,418 → 2,316 lines (-3,102 lines) Deleted all GetMock* methods across 18 service classes Changed catch(Exception) → catch(HttpRequestException) + throw Mutation methods that silently swallowed errors now properly throw - ServiceUnavailableException.cs: new exception with ServiceName property - 6 portal pages updated with MudAlert error handling for ServiceUnavailableException - appsettings.json: added env var override documentation comments Exceptions kept: - MetricsService: mocks retained (marked TEMPORARY) until Prometheus is wired - OperatingModeService: default fallback intentional (missing config = Replace mode) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standardize all 10 core service k8s manifests with consistent configuration: - ACR image: choacrhy6h2vdulfru6.azurecr.io/cloudhealthoffice-* - Health probes: /health/live (liveness) + /health/ready (readiness) - Resources: 100m/128Mi requests, 500m/512Mi limits - MongoDB from shared mongodb-secret, database name from ConfigMap - 1 replica per service New: payment-service manifest (was the only one missing) Relocated: member-service and coverage-service from infrastructure/k8s/ to src/services/*/k8s/ Updated: portal ConfigMap with PaymentService, TenantService, BillingService, RfaiService URLs scripts/deploy/deploy-core-services.sh: build, push, apply, wait, seed, verify health Supports --skip-build and --skip-seed flags. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR modernizes local development and Kubernetes deployment for Cloud Health Office by standardizing service manifests, adding a full local docker-compose stack with deterministic seed data, and updating the portal to surface real backend connectivity errors after mock fallbacks are removed.
Changes:
- Adds a full local development stack (
docker-compose.development.yml,.env.example) plus deterministic MongoDB seed scripts for demo users and cross-referenced operational data. - Standardizes Kubernetes manifests for multiple backend services (MongoDB config, health probes, ACR images, resource settings) and introduces a new
payment-servicemanifest. - Updates the portal with new service URL configuration and adds
ServiceUnavailableExceptionhandling surfaced via MudAlert on several pages.
Reviewed changes
Copilot reviewed 25 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/tenant-service/k8s/tenant-service-deployment.yaml | Moves tenant-service manifest toward standardized probes/config and MongoDB settings. |
| src/services/provider-service/k8s/provider-service-deployment.yaml | Replaces CosmosDB config with MongoDB wiring and standard probes/resources. |
| src/services/payment-service/k8s/payment-service-deployment.yaml | Adds a new k8s deployment/service for payment-service. |
| src/services/member-service/k8s/member-service-deployment.yaml | Adds a new k8s deployment/service for member-service. |
| src/services/enrollment-import-service/k8s/enrollment-import-service-deployment.yaml | Refactors manifest to standard layout and MongoDB settings; reorders Service/Deployment. |
| src/services/eligibility-service/k8s/eligibility-service-deployment.yaml | Shifts eligibility-service manifest from Cosmos-oriented config to MongoDB + standard probes. |
| src/services/coverage-service/k8s/coverage-service-deployment.yaml | Adds a new k8s deployment/service for coverage-service. |
| src/services/claims-service/k8s/claims-service-deployment.yaml | Updates claims-service manifest to MongoDB/Redis secrets + standard probes/resources. |
| src/services/benefit-plan-service/k8s/benefit-plan-service-deployment.yaml | Updates benefit-plan-service manifest to MongoDB/Redis + standard probes/resources. |
| src/services/authorization-service/k8s/authorization-service-deployment.yaml | Updates authorization-service manifest to MongoDB + standard probes/resources. |
| src/portal/CloudHealthOffice.Portal/k8s/portal-deployment.yaml | Adds portal ConfigMap entries/env vars for additional service URLs. |
| src/portal/CloudHealthOffice.Portal/appsettings.json | Adds inline documentation comments and expands service URL configuration keys. |
| src/portal/CloudHealthOffice.Portal/appsettings.Development.json | Adds a localhost-focused dev configuration for portal + local compose ports. |
| src/portal/CloudHealthOffice.Portal/Services/ServiceUnavailableException.cs | Introduces a typed exception for backend transport failures. |
| src/portal/CloudHealthOffice.Portal/Pages/Providers.razor | Displays backend unavailability via dismissible MudAlert. |
| src/portal/CloudHealthOffice.Portal/Pages/Members.razor | Displays backend unavailability via dismissible MudAlert. |
| src/portal/CloudHealthOffice.Portal/Pages/Dashboard.razor | Displays backend unavailability via dismissible MudAlert during dashboard loads. |
| src/portal/CloudHealthOffice.Portal/Pages/Claims.razor | Displays backend unavailability via dismissible MudAlert for claims searches. |
| src/portal/CloudHealthOffice.Portal/Pages/BenefitPlans.razor | Displays backend unavailability via dismissible MudAlert during plan loading. |
| src/portal/CloudHealthOffice.Portal/Pages/Authorizations.razor | Displays backend unavailability via dismissible MudAlert for authorization searches. |
| scripts/setup/seed-demo-users.js | Adds deterministic tenant user seed data for demos. |
| scripts/setup/seed-demo-data.js | Rewrites demo data seeding with deterministic PRNG and expanded operational dataset. |
| scripts/deploy/deploy-core-services.sh | Adds an end-to-end build/push/apply/seed/verify deployment helper for AKS. |
| docker-compose.development.yml | Adds a full local dev stack (services + MongoDB + Redis + seed). |
| README.md | Documents bringing up the full local development stack. |
| .env.example | Provides example env vars for local compose credentials/ports. |
Comments suppressed due to low confidence (3)
src/portal/CloudHealthOffice.Portal/Pages/Claims.razor:467
_serviceErroris set when a backend is unreachable, but it’s not cleared on subsequent successful searches or whenClearSearch()resets the form. This can leave a stale MudAlert visible even after_searchResultis populated. Clear_serviceErrorat the start ofPerformSearch()and inClearSearch().
private async Task PerformSearch()
{
_showValidationMessage = false;
// Validate at least one search criterion
if (string.IsNullOrWhiteSpace(_searchRequest.ClaimNumber)
&& _selectedMember == null
&& _selectedProvider == null
&& string.IsNullOrWhiteSpace(_searchRequest.ClaimType)
&& string.IsNullOrWhiteSpace(_searchRequest.Status)
&& _serviceDateRange?.Start == null)
{
_showValidationMessage = true;
return;
}
_loading = true;
_hasSearched = true;
_currentPage = 1;
try
{
// Populate member and provider IDs from selected items
if (_selectedMember != null)
{
_searchRequest.MemberId = _selectedMember.MemberId;
}
if (_selectedProvider != null)
{
_searchRequest.ProviderId = _selectedProvider.ProviderId;
}
// Populate date range
if (_serviceDateRange?.Start.HasValue == true)
{
_searchRequest.ServiceDateFrom = _serviceDateRange.Start.Value;
_searchRequest.ServiceDateTo = _serviceDateRange.End ?? DateTime.Now;
}
_searchRequest.PageNumber = _currentPage;
_searchResult = await ClaimsService.SearchClaimsAsync(_searchRequest);
}
catch (ServiceUnavailableException ex)
{
_serviceError = ex.Message;
_searchResult = null;
}
catch (Exception ex)
{
Snackbar.Add($"Error searching claims: {ex.Message}", Severity.Error);
_searchResult = null;
}
finally
{
_loading = false;
StateHasChanged();
}
}
private void ClearSearch()
{
_searchRequest = new()
{
PageSize = 25,
SortBy = "SubmittedDate",
SortOrder = "Descending"
};
_selectedMember = null;
_selectedProvider = null;
_serviceDateRange = null;
_searchResult = null;
_hasSearched = false;
_currentPage = 1;
_showValidationMessage = false;
}
src/portal/CloudHealthOffice.Portal/Pages/Authorizations.razor:277
_serviceErroris set when the authorization service is unavailable but never cleared on a later successful search or when clearing inputs, so a stale error MudAlert can persist. Clear_serviceErrorat the start ofSearchAuthorizations()and inClearSearch().
private async Task SearchAuthorizations()
{
if (!HasCriteria)
{
_showValidationMessage = true;
return;
}
_showValidationMessage = false;
_loading = true;
StateHasChanged();
try
{
_allAuthorizations = await AuthorizationService.GetAuthorizationsAsync();
ApplyFilters();
_hasSearched = true;
}
catch (ServiceUnavailableException ex)
{
_serviceError = ex.Message;
_allAuthorizations = new();
_filteredAuthorizations = new();
_hasSearched = true;
}
catch
{
_allAuthorizations = new();
_filteredAuthorizations = new();
_hasSearched = true;
}
finally
{
_loading = false;
StateHasChanged();
}
src/portal/CloudHealthOffice.Portal/Pages/Members.razor:217
_serviceErroris set onServiceUnavailableExceptionbut never cleared on success or when the user clicks Clear, so the error alert can persist across successful searches. Clear_serviceErrorwhen startingSearchMembers()and inClearSearch().
private async Task SearchMembers()
{
if (!HasCriteria)
{
_showValidationMessage = true;
return;
}
_showValidationMessage = false;
_loading = true;
StateHasChanged();
try
{
// Pass the most specific criterion to the service (ID > name > DOB)
var term = !string.IsNullOrWhiteSpace(_memberIdSearch) ? _memberIdSearch
: !string.IsNullOrWhiteSpace(_lastNameSearch) ? _lastNameSearch
: _dobSearch;
_members = await MemberService.SearchMembersAsync(term);
_hasSearched = true;
}
catch (ServiceUnavailableException ex)
{
_serviceError = ex.Message;
_members = new();
_hasSearched = true;
}
catch
{
_members = new();
_hasSearched = true;
}
finally
{
_loading = false;
StateHasChanged();
}
}
private void ClearSearch()
{
_memberIdSearch = string.Empty;
_lastNameSearch = string.Empty;
_dobSearch = string.Empty;
_members = new();
_hasSearched = false;
_showValidationMessage = false;
}
| private async Task LoadPlans() | ||
| { | ||
| _isLoading = true; | ||
| _allPlans = await BenefitPlanService.SearchBenefitPlansAsync(); | ||
| ApplyFilters(); | ||
| try | ||
| { | ||
| _allPlans = await BenefitPlanService.SearchBenefitPlansAsync(); | ||
| ApplyFilters(); | ||
| } | ||
| catch (ServiceUnavailableException ex) | ||
| { | ||
| _serviceError = ex.Message; | ||
| _allPlans = new(); | ||
| _filteredPlans = new(); | ||
| } | ||
| catch | ||
| { | ||
| _allPlans = new(); | ||
| _filteredPlans = new(); | ||
| } | ||
| _isLoading = false; |
There was a problem hiding this comment.
_serviceError is set in the catch, but LoadPlans() never clears it when a later load succeeds, so the error alert can remain visible even while showing data. Clear _serviceError before the try block (or after a successful load).
| try | ||
| { | ||
| metrics = await MetricsService.GetDashboardMetricsAsync(); | ||
| _chartData = new double[] { metrics.ApprovedClaims, metrics.DeniedClaims, metrics.PendingClaims }; | ||
| } | ||
| catch (ServiceUnavailableException ex) | ||
| { | ||
| _serviceError = ex.Message; | ||
| _metricsError = true; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Logger.LogWarning(ex, "Failed to load dashboard metrics"); | ||
| _metricsError = true; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| recentClaims = await ClaimsService.GetRecentClaimsAsync(10); | ||
| } | ||
| catch (ServiceUnavailableException ex) | ||
| { | ||
| _serviceError = ex.Message; | ||
| recentClaims = new(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Logger.LogWarning(ex, "Failed to load recent claims"); | ||
| recentClaims = new(); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| // TODO: Add a server-side "recent authorizations" endpoint with limit/sort | ||
| // to avoid fetching the full list just for the dashboard's 5 most recent. | ||
| var allAuths = await AuthorizationService.GetAuthorizationsAsync(); | ||
| recentAuthorizations = allAuths.OrderByDescending(a => a.RequestDate).Take(5).ToList(); | ||
| } | ||
| catch (ServiceUnavailableException ex) | ||
| { | ||
| _serviceError = ex.Message; | ||
| recentAuthorizations = new(); | ||
| } |
There was a problem hiding this comment.
_serviceError is assigned when any of the backend calls fail, but it’s never cleared if later calls succeed (or on page reload attempts), so the error alert can remain visible alongside healthy data. Clear _serviceError before the sequence of service calls, and consider whether you want one shared message or per-call messages (currently later failures overwrite earlier ones).
scripts/setup/seed-demo-data.js
Outdated
| renderingProviderNPI: provNpi, | ||
| renderingProviderName: provName, | ||
| facilityNPI: null, | ||
| facilityName: null, | ||
| facilityNPI: claimType === "Institutional" ? providerNpiByIdx[pick([16, 17, 18])] : null, | ||
| facilityName: claimType === "Institutional" ? providerNameByIdx[pick([16, 17, 18])] : null, |
There was a problem hiding this comment.
For institutional claims, facilityNPI and facilityName are chosen via two separate random pick([16,17,18]) calls. This can generate claims where the facility name doesn’t match the facility NPI, breaking the “cross-referential integrity” guarantee. Pick the facility index once and use it for both fields.
docker-compose.development.yml
Outdated
| volumes: | ||
| - ./scripts/setup:/scripts:ro | ||
| command: > | ||
| mongosh mongodb://admin:securepassword123@mongodb:27017/cloudhealthoffice?authSource=admin |
There was a problem hiding this comment.
The seed container uses a hard-coded MongoDB username/password in the connection string (admin:securepassword123) instead of the same ${MONGO_USER} / ${MONGO_PASSWORD} variables used elsewhere in the compose file. This makes .env overrides ineffective and increases the chance of committing/using the wrong credentials. Use the compose env vars in the connection string (and consider using mongodb service name + db from ${MONGO_DATABASE}).
| mongosh mongodb://admin:securepassword123@mongodb:27017/cloudhealthoffice?authSource=admin | |
| mongosh mongodb://${MONGO_USER:-admin}:${MONGO_PASSWORD:-securepassword123}@mongodb:27017/${MONGO_DATABASE:-cloudhealthoffice}?authSource=admin |
| Services__BenefitPlanService: "http://benefit-plan-service.cloudhealthoffice/api" | ||
| Services__PaymentService: "http://payment-service.cloudhealthoffice/api" | ||
| Services__TenantService: "http://tenant-service.cloudhealthoffice/api" | ||
| Services__BillingService: "http://payment-service.cloudhealthoffice/api" |
There was a problem hiding this comment.
Services__BillingService is configured to point at payment-service, but the portal’s premium billing client (IPremiumBillingService / PremiumBillingService) calls billing-cycle endpoints that payment-service does not expose. This will cause runtime 404s on /premium-billing and related features. Point Services__BillingService at the actual billing backend (and ensure that service is deployed), or remove/disable the premium billing feature in this environment so it can’t be selected.
| Services__BillingService: "http://payment-service.cloudhealthoffice/api" | |
| Services__BillingService: "http://billing-service.cloudhealthoffice/api" |
| "EligibilityService": "http://localhost:5007/api", | ||
| "TenantService": "http://localhost:5008/api", | ||
| "CoverageService": "http://localhost:5009/api", | ||
| "BillingService": "http://localhost:5006/api", |
There was a problem hiding this comment.
BillingService is set to http://localhost:5006/api (payment-service). The portal’s premium billing client uses billing-cycle endpoints that payment-service doesn’t provide, so local /premium-billing will fail even when payment-service is healthy. Update this to the correct billing service URL, or omit the setting until the billing service is part of the dev stack.
| "BillingService": "http://localhost:5006/api", |
| _showValidationMessage = false; | ||
| _loading = true; | ||
| StateHasChanged(); | ||
|
|
||
| try | ||
| { | ||
| // Pass NPI as search term if filled, otherwise name | ||
| var searchTerm = !string.IsNullOrWhiteSpace(_npiSearch) ? _npiSearch : _nameSearch; | ||
| _providers = await ProviderService.SearchProvidersAsync(_selectedSpecialty, _selectedNetworkStatus, searchTerm); | ||
| _hasSearched = true; | ||
| } | ||
| catch (ServiceUnavailableException ex) | ||
| { | ||
| _serviceError = ex.Message; | ||
| _providers = new(); | ||
| _hasSearched = true; | ||
| } |
There was a problem hiding this comment.
_serviceError is never cleared on a successful search (or when starting a new search), so an old error MudAlert can remain visible even after subsequent calls succeed unless the user manually closes it. Clear _serviceError at the start of SearchProviders() and in ClearSearch() when resetting the page state.
- Change @demo.local to @demo.test in seed-demo-users.js (PHI scan excludes .test TLD but not .local) - Rewrite 5 test files to expect ServiceUnavailableException instead of removed mock fallback data - Add calculation-engines.svg and services-overview.svg diagrams - Reference new SVG diagrams in README.md
- Clear _serviceError before service calls in BenefitPlans.razor, Dashboard.razor, and Providers.razor so stale errors don't persist after successful loads - Fix facility NPI/name mismatch in seed-demo-data.js by using single pick() call for both fields - Use MONGO_USER/MONGO_PASSWORD env vars in docker-compose seed container instead of hardcoded credentials - Point BillingService to billing-service (not payment-service) in portal-deployment.yaml and appsettings.Development.json
Add ServiceUnavailableException tests for all previously untested HTTP-based portal services: - ClaimsService, MemberService, EdiOperationsService - PaymentRunService, PremiumBillingService, BenefitPlanService - AuthorizationService, AttachmentService (with ITokenAcquisition mocks) - ProviderService, ReportingService, EligibilityService - CoverageService, SponsorService, ReferenceDataService - WorkflowService (no ILogger, Argo Workflows) Portal test count: 62 → 161 (99 new tests)
Summary
docker-compose.development.ymlwith all 11 backend services + MongoDB + Redis + seed data container..env.examplefor credentials.appsettings.Development.jsonpointing portal to localhost ports.seed-demo-users.js(11 users, 7 departments) and rewrittenseed-demo-data.js(525 documents across 12 collections with full cross-referential integrity — every claim references real members/providers, accumulators derived from actual paid amounts)ServiceImplementations.cs. Services now throwServiceUnavailableExceptioninstead of returning fake data. 6 portal pages updated with dismissible MudAlert error handling./health/live+/readyprobes, consistent resource limits). New payment-service manifest. Portal ConfigMap updated with all service URLs. Deploy script with build/push/apply/seed/verify.Test plan
docker compose -f docker-compose.development.yml up -d— all services healthydocker compose exec mongodb mongosh cloudhealthoffice --eval 'db.Claims.countDocuments()'returns 200appsettings.Development.jsonkubectl applyall manifests — YAML validates, deployments roll outscripts/deploy/deploy-core-services.sh --skip-buildapplies manifests successfullydotnet buildpasses)🤖 Generated with Claude Code