Skip to content

feat(centralServer): no-issue: add patient program registration importer#9613

Open
julianam-w wants to merge 8 commits intomainfrom
feature/patient-program-registration-importer
Open

feat(centralServer): no-issue: add patient program registration importer#9613
julianam-w wants to merge 8 commits intomainfrom
feature/patient-program-registration-importer

Conversation

@julianam-w
Copy link
Copy Markdown
Contributor

@julianam-w julianam-w commented Apr 20, 2026

Changes

Adds a bulk importer for enrolling patients into program registries via xlsx upload. The importer uses a single ppr sheet with the following columns:

  • date, registration_status, patient_display_id, program_registry_id, clinician_id
  • Optional: clinical_status_id, registering_facility_id, deactivated_clinician_id, deactivated_date
  • Optional: program_registry_condition_ids (comma-separated), program_registry_condition_category_id

Features:

  • Upsert semantics (re-importing a patient+registry row updates the existing registration)
  • FK validation for all reference fields before writing
  • Excel serial date conversion
  • Dry run support
  • Conditions created per-row if condition IDs are provided (category required when conditions are present)
  • Hoists registration lookup above the condition loop to avoid redundant DB hits; condition creation errors are isolated so a failed condition does not mark the whole registration as errored

Route: POST /api/admin/import/patientProgramRegistrations

Auto-Deploy

  • Deploy
Options
  • Synthetic test

Tests

  • Run E2E Tests

Review Hero

  • Run Review Hero
  • Auto-fix review suggestions Wait for Review Hero to finish, resolve any comments you disagree with or want to fix manually, then check this to auto-fix the rest.
  • Auto-fix CI failures Check this to auto-fix lint errors, test failures, and other CI issues.
  • Auto-merge upstream Check this to merge the base branch into this PR, with AI conflict resolution if needed.

Remember to...

  • ...write or update tests
  • ...add UI screenshots and testing notes to the Linear issue
  • ...add any manual upgrade steps to the Linear issue
  • ...update the config reference, settings reference, or any relevant runbook(s)
  • ...call out additions or changes to config files for the deployment team to take note of

julianam-w and others added 6 commits April 20, 2026 21:46
Add bulk importer for enrolling patients into program registries via
xlsx upload. Uses a single 'ppr' sheet with comma-separated condition
IDs. Supports upsert semantics, FK validation, Excel date conversion,
and dry run mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ondition errors

Move the registration findOne query above the condition loop to avoid
redundant DB hits. Catch condition creation errors individually so a
failed condition does not incorrectly mark the registration as errored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update actions/checkout, actions/setup-node from non-existent v6 to v4,
and actions/cache/restore and actions/cache/save from v3 to v4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Postgres doesn't reliably return whether an upserted row was created or
updated, so check for an existing record before the upsert to determine
the correct stat. Also reuse the upsert's returned instance for condition
creation to avoid a redundant findOne.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
patientId: patient.id,
programRegistryId,
date: dateString,
...(registrationStatus && { registrationStatus }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bugs & Correctness] critical

Re-importing the same spreadsheet will create duplicate PatientProgramRegistrationCondition rows. The registration itself is upserted (composite PK on patientId+programRegistryId), but conditions are always create()d with no duplicate check. On re-import, each condition row is inserted again with a new UUID. Either check for an existing condition before creating, or delete all conditions for the registration before re-creating them (within the same row handler).

const record = await models[modelName].findByPk(id);
if (!record) {
throw new Error(`${fieldLabel} "${id}" does not exist`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Security] suggestion

validateForeignKey uses findByPk(id) where id comes from untrimmed spreadsheet cell values. While Sequelize parameterises the query (no SQL injection), the id value is only trimmed at the row level by trimRow (which trims keys, not values). A cell value like ' some-id ' with leading/trailing whitespace would fail lookup silently. Consider trimming id inside validateForeignKey or in parseCommaSeparated for condition IDs, to avoid confusing 'does not exist' errors caused by invisible whitespace.

@review-hero
Copy link
Copy Markdown

review-hero Bot commented Apr 20, 2026

🦸 Review Hero Summary
9 agents reviewed this PR | 1 critical | 1 suggestion | 0 nitpicks | Filtering: consensus 3 voters, 7 below threshold

Below consensus threshold (7 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/app/admin/patientProgramRegistrationImporter/importPatientProgramRegistrations.js:14 BES Requirements suggestion convertExcelDate chains getJsDateFromExcel (which produces a JS Date, typically UTC-based) into toDateTimeString which calls formatISO9075 — that formats in the system's local timezone....
packages/central-server/app/admin/patientProgramRegistrationImporter/importPatientProgramRegistrations.js:14 Bugs & Correctness suggestion convertExcelDate does not validate that the input is a number before passing it to getJsDateFromExcel. If a user enters a text date string (e.g. '2024-03-15') instead of an Excel serial number,...
packages/central-server/app/admin/patientProgramRegistrationImporter/importPatientProgramRegistrations.js:63 Bugs & Correctness suggestion for (const [sheetRow, data] of sheetRows.entries()) — when sheetRow is 0 (first data row), ValidationError(SHEET_NAME, 0, ...) will report row 2 (0 + 2 for header offset). This is correct for...
packages/central-server/app/admin/patientProgramRegistrationImporter/importPatientProgramRegistrations.js:80 Bugs & Correctness suggestion registrationStatus is not listed in the required-field checks (lines 73-76), so a row with no registration_status column will pass validation. Then at line 171 the spread `...(registrationStatu...
packages/central-server/app/admin/patientProgramRegistrationImporter/patientProgramRegistrationImporter.js:24 Security critical Missing write permission check. The importer calls upsert (line 178 of importPatientProgramRegistrations.js), which can update existing registrations, but only checks create permission. A use...
packages/central-server/app/admin/patientProgramRegistrationImporter/patientProgramRegistrationImporter.js:28 BES Requirements suggestion let workbook with conditional assignment is a let smell per coding rules. Simplify to: const workbook = data ? read(data, { type: 'buffer' }) : readFile(file);
packages/central-server/app/admin/patientProgramRegistrationImporter/patientProgramRegistrationImporter.js:32 Security suggestion readFile(file) passes the file parameter directly to xlsx.readFile(). When the request uses application/json content-type instead of multipart/form-data, file is sourced from req.body...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/central-server/app/admin/patientProgramRegistrationImporter/importPatientProgramRegistrations.js:190: Re-importing the same spreadsheet will create duplicate PatientProgramRegistrationCondition rows. The registration itself is upserted (composite PK on patientId+programRegistryId), but conditions are always create()d with no duplicate check. On re-import, each condition row is inserted again with a new UUID. Either check for an existing condition before creating, or delete all conditions for the registration before re-creating them (within the same row handler).


packages/central-server/app/admin/patientProgramRegistrationImporter/importPatientProgramRegistrations.js:35: validateForeignKey uses findByPk(id) where id comes from untrimmed spreadsheet cell values. While Sequelize parameterises the query (no SQL injection), the id value is only trimmed at the row level by trimRow (which trims keys, not values). A cell value like ' some-id ' with leading/trailing whitespace would fail lookup silently. Consider trimming id inside validateForeignKey or in parseCommaSeparated for condition IDs, to avoid confusing 'does not exist' errors caused by invisible whitespace.

claude added 2 commits April 20, 2026 13:36
…nd trim cell values

Trim string values in trimRow (was only trimming keys) so leading/trailing
whitespace in spreadsheet cells does not cause silent FK lookup failures.

Delete existing PatientProgramRegistrationCondition rows before re-creating
them when condition IDs are provided, so re-importing the same spreadsheet
does not accumulate duplicate condition rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eting on re-import

Rather than deleting and re-creating PatientProgramRegistrationCondition
rows on re-import, check for an existing record and skip creation if one
already exists, tracking it as 'skipped' in the import stats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants