MMU Nikshay data for application - #181
Conversation
Adds a date-range-scoped export of Stop TB beneficiaries as a CSV formatted for the Nikshay ID Generator app, and an import endpoint to write the portal-generated Nikshay IDs back onto those beneficiaries. Scoped by visit-date range only, no vanID/servicePointID — MMU runs one local database per van, so everything in it already belongs to the current van/service point. Location columns (village/healthFacility/tu/district/state) are resolved against Nikshay's own, isolated location hierarchy (m_nikshay_village -> m_nikshay_facility -> m_nikshay_tu -> m_nikshay_district -> m_nikshay_state), not AMRIT's standard masters — a Stop TB beneficiary's I_bendemographics.DistrictBranchID actually holds their Nikshay Village ID (the same overload Common-API's NikshayAddressResolver relies on), so every other location field is derived by walking up that one chain. A beneficiary whose village doesn't resolve all the way to a state is silently left out of the export, same as one who already has a Nikshay ID. New endpoints: - GET /stopTb/nikshay/exportBeneficiariesCsv?fromDate=&toDate= - POST /stopTb/nikshay/importResultsCsv?visitDate=&file= Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hone+name Two corrections after checking real data: 1. nikshay_id now lives on tb_suspected, not tb_stoptb_diagnostics — both tables have the column, but tb_suspected is the one this feature is meant to use going forward. Both the export's already-has-an-ID check and the import's write path now target tb_suspected.benRegID. 2. A real Nikshay ID Generator results file has no beneficiary ID column of any kind — only the original template columns plus its own generatedId/status/durationSec/error. The import can no longer assume benRegId round-trips through untouched. Rows are now matched back to a beneficiary by normalized primaryPhone + case-insensitive firstName (NikshayExportRepository.findMatchingBeneficiaryIds); anything other than exactly one match is left for manual review rather than guessed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every table the export previously used for beneficiary/visit data turned out not to exist on a real server (i_beneficiary, I_bendemographics, tb_stoptb_visit) — confirmed via BadSqlGrammarException in production logs and direct schema inspection. Rewritten against tables and columns verified with real data: - Visit filter: t_benvisitdetail (VisitCategory = 'Stop TB'), not tb_stoptb_visit — confirmed 511 real rows vs. zero. - Beneficiary identity: MMU's datasource only connects to db_iemr, but beneficiary data lives in db_identity on the same physical server — reached via fully-qualified cross-schema table names: i_beneficiarymapping (BenRegId, unique) -> BenDetailsId -> i_beneficiarydetails (name/DOB/gender/caste/occupation/income/HIV, already denormalized, no extra master-table joins needed) and BenAddressId -> i_beneficiaryaddress (address/village/pincode), plus BenContactsId -> i_beneficiarycontacts (phone). - New confirmed real gap: many van-registered beneficiaries (i_beneficiarymapping.CreatedBy = 'reglocal') have a mapping row but their detail/address rows never synced to db_identity, sometimes for days. These are now silently skipped (no FirstName resolved), same category as an unresolved location — renamed countUnresolvedLocation -> countNotReadyToExport to reflect both cases. - Import's phone+name matching updated to the same verified join path. Tested against real data on a live server: 15 of 20 today's-date beneficiaries correctly skipped (identity never synced), 5 correctly resolved end-to-end including the full Nikshay location chain, producing valid CSV rows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…..]) exportBeneficiariesCsv was declared ResponseEntity<?> so it could return either a StreamingResponseBody (success) or a plain String (bad request/ error). Spring's StreamingResponseBodyReturnValueHandler decides whether to special-case the response based on the method's *declared* generic return type, not the runtime object — a wildcard resolves to Object, StreamingResponseBody isn't assignable from Object, so it falls through to normal HttpMessageConverters, which can't serialize a raw lambda: "No converter for [...NikshayExportController$$Lambda...] with preset Content-Type 'text/csv'" (confirmed in production logs, 2026-08-18). Fixed by keeping the return type consistently ResponseEntity<StreamingResponseBody> and wrapping error messages as a StreamingResponseBody that writes the message as bytes, instead of returning a plain String on any path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@PreAuthorize only listed clinical roles (Nurse/Doctor/Pharmacist/Lab Technician/etc.), copied from sn/nikshaya's original controller without verifying against real Stop TB camp roles. Confirmed in production: AccessDeniedException for a real Stop TB user (stoptb1) whose roles (checked directly via v_userservicerolemapping) include Registrar, Nurse, Doctor, Pharmacist, Lab Technician, Data Sync — Registrar was the one missing from the allow-list. Downloading/uploading a beneficiary CSV is fundamentally a registrar task, so this was very likely the actual active session role denied by AccessDeniedException in the logs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Role-based gating proved fragile — wrong role list on the first attempt, still AccessDeniedException after adding REGISTRAR. SecurityConfig's anyRequest().authenticated() still requires a logged-in user; dropping the role-specific check per explicit request rather than guessing a third time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Body Root cause of the persistent AccessDeniedException (survived removing @PreAuthorize entirely): StreamingResponseBody makes Spring process the response body on a Servlet async re-dispatch. Confirmed from production logs (2026-08-18) — two AccessDeniedExceptions per request, the second via ApplicationDispatcher/AsyncContextImpl. Spring Security's own core filter chain is registered for ASYNC dispatch by default and re-runs, but this app's custom JwtUserIdValidationFilter/RoleAuthenticationFilter (which read the JWT cookie and establish who's logged in) are not — AnonymousAuthenticationFilter fires instead on the second pass, and the global anyRequest().authenticated() rule denies it. Fixed by writing the CSV directly and synchronously onto HttpServletResponse instead of returning a StreamingResponseBody — avoids async dispatch entirely, so there's no second security pass to fail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Verified against real data: PhoneNum1 and CurrAddressValue are empty on every real beneficiary checked — the actual data lives in PreferredPhoneNum (phone) and CurrAddrLine1/2/3 + CurrHabitation (address), confirmed by direct query on real records. Falls back through PhoneNum1/PhoneNum2 if PreferredPhoneNum is empty. Address line parts are individually NULLIF'd before CONCAT_WS, since blank (not NULL) line fields otherwise leave stray ", ," artifacts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
findMatchingBeneficiaryIds only checked c.PhoneNum1 — same bug already fixed on the export side (commit 79d8422) but missed here. Confirmed in production: two real beneficiaries (17877 'K', 17875 'U') with PhoneNum1 IS NULL but PreferredPhoneNum populated ('9999999999') both came back "no beneficiary found" on upload. Now checks PreferredPhoneNum/PhoneNum1/PhoneNum2, same as the export's column preference order. Verified against real data: 17877 now matches correctly for phone '9999999999' + firstName 'K'. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…name If a results row carries a parseable benRegId (e.g. a manually-rebuilt test file, or a future ID Generator version that keeps it), use it directly — exact match, no ambiguity possible, skips the phone+name lookup entirely. Falls back to the existing phone+name matching only when benRegId is absent or unparseable, which is what a genuine Nikshay portal results file looks like (confirmed no ID column at all). Makes test files with benRegId (e.g. a rebuilt export) match instantly and unambiguously, while real portal files keep working exactly as before via the phone+name path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|



📋 Description
JIRA ID:
Please provide a summary of the change and the motivation behind it. Include relevant context and details.
✅ Type of Change
ℹ️ Additional Information
Please describe how the changes were tested, and include any relevant screenshots, logs, or other information that provides additional context.