mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-08 04:06:16 -04:00
UI Upgrades
This commit is contained in:
@@ -401,22 +401,25 @@ func (s *Store) UpsertAllergyIntolerance(a *models.AllergyIntolerance) (string,
|
||||
if err == nil {
|
||||
_, err = s.db.Exec(`
|
||||
UPDATE allergy_intolerances SET
|
||||
patient_fhir_id = ?,
|
||||
clinical_status = ?,
|
||||
verification_status = ?,
|
||||
type = ?,
|
||||
category = ?,
|
||||
criticality = ?,
|
||||
code_text = ?,
|
||||
code_system = ?,
|
||||
code_code = ?,
|
||||
recorded_date = ?,
|
||||
synced_at = ?
|
||||
patient_fhir_id = ?,
|
||||
clinical_status = ?,
|
||||
verification_status = ?,
|
||||
type = ?,
|
||||
category = ?,
|
||||
criticality = ?,
|
||||
code_text = ?,
|
||||
code_system = ?,
|
||||
code_code = ?,
|
||||
recorded_date = ?,
|
||||
reaction_severity = ?,
|
||||
reaction_manifestation = ?,
|
||||
synced_at = ?
|
||||
WHERE id = ?`,
|
||||
a.PatientFHIRID, a.ClinicalStatus, a.VerificationStatus,
|
||||
a.Type, a.Category, a.Criticality,
|
||||
a.CodeText, a.CodeSystem, a.CodeCode,
|
||||
a.RecordedDate, now, existingID,
|
||||
a.RecordedDate, a.ReactionSeverity, a.ReactionManifestation,
|
||||
now, existingID,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: update allergy_intolerance %s: %w", existingID, err)
|
||||
@@ -429,11 +432,13 @@ func (s *Store) UpsertAllergyIntolerance(a *models.AllergyIntolerance) (string,
|
||||
INSERT INTO allergy_intolerances (
|
||||
id, fhir_id, ehr_url, patient_fhir_id,
|
||||
clinical_status, verification_status, type, category, criticality,
|
||||
code_text, code_system, code_code, recorded_date, synced_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
code_text, code_system, code_code, recorded_date,
|
||||
reaction_severity, reaction_manifestation, synced_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, a.FHIRID, a.EHRURL, a.PatientFHIRID,
|
||||
a.ClinicalStatus, a.VerificationStatus, a.Type, a.Category, a.Criticality,
|
||||
a.CodeText, a.CodeSystem, a.CodeCode, a.RecordedDate, now,
|
||||
a.CodeText, a.CodeSystem, a.CodeCode, a.RecordedDate,
|
||||
a.ReactionSeverity, a.ReactionManifestation, now,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: insert allergy_intolerance fhir_id=%s: %w", a.FHIRID, err)
|
||||
@@ -446,7 +451,8 @@ func (s *Store) ListAllergyIntolerances(patientFHIRID, ehrURL string) ([]models.
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, fhir_id, ehr_url, patient_fhir_id,
|
||||
clinical_status, verification_status, type, category, criticality,
|
||||
code_text, code_system, code_code, recorded_date, synced_at
|
||||
code_text, code_system, code_code, recorded_date,
|
||||
reaction_severity, reaction_manifestation, synced_at
|
||||
FROM allergy_intolerances
|
||||
WHERE patient_fhir_id = ? AND ehr_url = ?
|
||||
ORDER BY recorded_date DESC`,
|
||||
@@ -463,7 +469,8 @@ func (s *Store) ListAllergyIntolerances(patientFHIRID, ehrURL string) ([]models.
|
||||
if err := rows.Scan(
|
||||
&a.ID, &a.FHIRID, &a.EHRURL, &a.PatientFHIRID,
|
||||
&a.ClinicalStatus, &a.VerificationStatus, &a.Type, &a.Category, &a.Criticality,
|
||||
&a.CodeText, &a.CodeSystem, &a.CodeCode, &a.RecordedDate, &a.SyncedAt,
|
||||
&a.CodeText, &a.CodeSystem, &a.CodeCode, &a.RecordedDate,
|
||||
&a.ReactionSeverity, &a.ReactionManifestation, &a.SyncedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("db: scan allergy_intolerance: %w", err)
|
||||
}
|
||||
@@ -472,6 +479,262 @@ func (s *Store) ListAllergyIntolerances(patientFHIRID, ehrURL string) ([]models.
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Immunization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UpsertImmunization inserts or updates an Immunization record keyed on (fhir_id, ehr_url).
|
||||
func (s *Store) UpsertImmunization(imm *models.Immunization) (string, error) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
var existingID string
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id FROM immunizations WHERE fhir_id = ? AND ehr_url = ?`,
|
||||
imm.FHIRID, imm.EHRURL,
|
||||
).Scan(&existingID)
|
||||
|
||||
if err == nil {
|
||||
_, err = s.db.Exec(`
|
||||
UPDATE immunizations SET
|
||||
patient_fhir_id = ?,
|
||||
status = ?,
|
||||
vaccine_text = ?,
|
||||
vaccine_system = ?,
|
||||
vaccine_code = ?,
|
||||
occurrence_date = ?,
|
||||
primary_source = ?,
|
||||
lot_number = ?,
|
||||
synced_at = ?
|
||||
WHERE id = ?`,
|
||||
imm.PatientFHIRID, imm.Status,
|
||||
imm.VaccineText, imm.VaccineSystem, imm.VaccineCode,
|
||||
imm.OccurrenceDate, imm.PrimarySource, imm.LotNumber,
|
||||
now, existingID,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: update immunization %s: %w", existingID, err)
|
||||
}
|
||||
return existingID, nil
|
||||
}
|
||||
|
||||
id := uuid.NewString()
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO immunizations (
|
||||
id, fhir_id, ehr_url, patient_fhir_id,
|
||||
status, vaccine_text, vaccine_system, vaccine_code,
|
||||
occurrence_date, primary_source, lot_number, synced_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, imm.FHIRID, imm.EHRURL, imm.PatientFHIRID,
|
||||
imm.Status, imm.VaccineText, imm.VaccineSystem, imm.VaccineCode,
|
||||
imm.OccurrenceDate, imm.PrimarySource, imm.LotNumber, now,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: insert immunization fhir_id=%s: %w", imm.FHIRID, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ListImmunizations returns all Immunizations for the given patient, newest first.
|
||||
func (s *Store) ListImmunizations(patientFHIRID, ehrURL string) ([]models.Immunization, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, fhir_id, ehr_url, patient_fhir_id,
|
||||
status, vaccine_text, vaccine_system, vaccine_code,
|
||||
occurrence_date, primary_source, lot_number, synced_at
|
||||
FROM immunizations
|
||||
WHERE patient_fhir_id = ? AND ehr_url = ?
|
||||
ORDER BY occurrence_date DESC`,
|
||||
patientFHIRID, ehrURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db: list immunizations: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.Immunization
|
||||
for rows.Next() {
|
||||
var imm models.Immunization
|
||||
if err := rows.Scan(
|
||||
&imm.ID, &imm.FHIRID, &imm.EHRURL, &imm.PatientFHIRID,
|
||||
&imm.Status, &imm.VaccineText, &imm.VaccineSystem, &imm.VaccineCode,
|
||||
&imm.OccurrenceDate, &imm.PrimarySource, &imm.LotNumber, &imm.SyncedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("db: scan immunization: %w", err)
|
||||
}
|
||||
out = append(out, imm)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Procedure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UpsertProcedure inserts or updates a Procedure record keyed on (fhir_id, ehr_url).
|
||||
func (s *Store) UpsertProcedure(p *models.Procedure) (string, error) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
var existingID string
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id FROM procedures WHERE fhir_id = ? AND ehr_url = ?`,
|
||||
p.FHIRID, p.EHRURL,
|
||||
).Scan(&existingID)
|
||||
|
||||
if err == nil {
|
||||
_, err = s.db.Exec(`
|
||||
UPDATE procedures SET
|
||||
patient_fhir_id = ?,
|
||||
status = ?,
|
||||
code_text = ?,
|
||||
code_system = ?,
|
||||
code_code = ?,
|
||||
performed_date = ?,
|
||||
reason_text = ?,
|
||||
outcome = ?,
|
||||
synced_at = ?
|
||||
WHERE id = ?`,
|
||||
p.PatientFHIRID, p.Status,
|
||||
p.CodeText, p.CodeSystem, p.CodeCode,
|
||||
p.PerformedDate, p.ReasonText, p.Outcome,
|
||||
now, existingID,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: update procedure %s: %w", existingID, err)
|
||||
}
|
||||
return existingID, nil
|
||||
}
|
||||
|
||||
id := uuid.NewString()
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO procedures (
|
||||
id, fhir_id, ehr_url, patient_fhir_id,
|
||||
status, code_text, code_system, code_code,
|
||||
performed_date, reason_text, outcome, synced_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, p.FHIRID, p.EHRURL, p.PatientFHIRID,
|
||||
p.Status, p.CodeText, p.CodeSystem, p.CodeCode,
|
||||
p.PerformedDate, p.ReasonText, p.Outcome, now,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: insert procedure fhir_id=%s: %w", p.FHIRID, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ListProcedures returns all Procedures for the given patient, newest first.
|
||||
func (s *Store) ListProcedures(patientFHIRID, ehrURL string) ([]models.Procedure, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, fhir_id, ehr_url, patient_fhir_id,
|
||||
status, code_text, code_system, code_code,
|
||||
performed_date, reason_text, outcome, synced_at
|
||||
FROM procedures
|
||||
WHERE patient_fhir_id = ? AND ehr_url = ?
|
||||
ORDER BY performed_date DESC`,
|
||||
patientFHIRID, ehrURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db: list procedures: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.Procedure
|
||||
for rows.Next() {
|
||||
var p models.Procedure
|
||||
if err := rows.Scan(
|
||||
&p.ID, &p.FHIRID, &p.EHRURL, &p.PatientFHIRID,
|
||||
&p.Status, &p.CodeText, &p.CodeSystem, &p.CodeCode,
|
||||
&p.PerformedDate, &p.ReasonText, &p.Outcome, &p.SyncedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("db: scan procedure: %w", err)
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Encounter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UpsertEncounter inserts or updates an Encounter record keyed on (fhir_id, ehr_url).
|
||||
func (s *Store) UpsertEncounter(e *models.Encounter) (string, error) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
var existingID string
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id FROM encounters WHERE fhir_id = ? AND ehr_url = ?`,
|
||||
e.FHIRID, e.EHRURL,
|
||||
).Scan(&existingID)
|
||||
|
||||
if err == nil {
|
||||
_, err = s.db.Exec(`
|
||||
UPDATE encounters SET
|
||||
patient_fhir_id = ?,
|
||||
status = ?,
|
||||
class = ?,
|
||||
type_text = ?,
|
||||
period_start = ?,
|
||||
period_end = ?,
|
||||
reason_text = ?,
|
||||
synced_at = ?
|
||||
WHERE id = ?`,
|
||||
e.PatientFHIRID, e.Status, e.Class, e.TypeText,
|
||||
e.PeriodStart, e.PeriodEnd, e.ReasonText,
|
||||
now, existingID,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: update encounter %s: %w", existingID, err)
|
||||
}
|
||||
return existingID, nil
|
||||
}
|
||||
|
||||
id := uuid.NewString()
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO encounters (
|
||||
id, fhir_id, ehr_url, patient_fhir_id,
|
||||
status, class, type_text,
|
||||
period_start, period_end, reason_text, synced_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, e.FHIRID, e.EHRURL, e.PatientFHIRID,
|
||||
e.Status, e.Class, e.TypeText,
|
||||
e.PeriodStart, e.PeriodEnd, e.ReasonText, now,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("db: insert encounter fhir_id=%s: %w", e.FHIRID, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ListEncounters returns all Encounters for the given patient, newest first.
|
||||
func (s *Store) ListEncounters(patientFHIRID, ehrURL string) ([]models.Encounter, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, fhir_id, ehr_url, patient_fhir_id,
|
||||
status, class, type_text,
|
||||
period_start, period_end, reason_text, synced_at
|
||||
FROM encounters
|
||||
WHERE patient_fhir_id = ? AND ehr_url = ?
|
||||
ORDER BY period_start DESC`,
|
||||
patientFHIRID, ehrURL,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db: list encounters: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []models.Encounter
|
||||
for rows.Next() {
|
||||
var e models.Encounter
|
||||
if err := rows.Scan(
|
||||
&e.ID, &e.FHIRID, &e.EHRURL, &e.PatientFHIRID,
|
||||
&e.Status, &e.Class, &e.TypeText,
|
||||
&e.PeriodStart, &e.PeriodEnd, &e.ReasonText, &e.SyncedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("db: scan encounter: %w", err)
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PatientSync
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -271,6 +271,414 @@ func TestListDocumentReferences_Empty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MedicationRequest tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpsertMedicationRequest_NewAndUpdate(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
med := &models.MedicationRequest{
|
||||
FHIRID: "med-001",
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
Status: "active",
|
||||
Intent: "order",
|
||||
MedCodeText: "Metformin 500mg",
|
||||
MedCodeSystem: "http://www.nlm.nih.gov/research/umls/rxnorm",
|
||||
MedCodeCode: "860975",
|
||||
AuthoredOn: "2024-01-10",
|
||||
RequesterDisplay: "Dr. Smith",
|
||||
DosageText: "1 tablet twice daily",
|
||||
}
|
||||
|
||||
id1, err := store.UpsertMedicationRequest(med)
|
||||
if err != nil {
|
||||
t.Fatalf("initial UpsertMedicationRequest: %v", err)
|
||||
}
|
||||
if id1 == "" {
|
||||
t.Fatal("expected non-empty ID")
|
||||
}
|
||||
|
||||
med.Status = "stopped"
|
||||
id2, err := store.UpsertMedicationRequest(med)
|
||||
if err != nil {
|
||||
t.Fatalf("update UpsertMedicationRequest: %v", err)
|
||||
}
|
||||
if id1 != id2 {
|
||||
t.Errorf("ID changed on upsert: was %q, got %q", id1, id2)
|
||||
}
|
||||
|
||||
rows, err := store.ListMedicationRequests(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListMedicationRequests: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 med, got %d", len(rows))
|
||||
}
|
||||
if rows[0].Status != "stopped" {
|
||||
t.Errorf("Status: got %q, want stopped", rows[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMedicationRequests_Empty(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
rows, err := store.ListMedicationRequests("no-such-patient", testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListMedicationRequests: %v", err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AllergyIntolerance tests (including reaction fields)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpsertAllergyIntolerance_WithReaction(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
allergy := &models.AllergyIntolerance{
|
||||
FHIRID: "allergy-001",
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
ClinicalStatus: "active",
|
||||
VerificationStatus: "confirmed",
|
||||
Type: "allergy",
|
||||
Category: "medication",
|
||||
Criticality: "high",
|
||||
CodeText: "Penicillin",
|
||||
CodeSystem: "http://www.nlm.nih.gov/research/umls/rxnorm",
|
||||
CodeCode: "7980",
|
||||
RecordedDate: "2018-05-01",
|
||||
ReactionSeverity: "severe",
|
||||
ReactionManifestation: "Anaphylaxis",
|
||||
}
|
||||
|
||||
id1, err := store.UpsertAllergyIntolerance(allergy)
|
||||
if err != nil {
|
||||
t.Fatalf("initial UpsertAllergyIntolerance: %v", err)
|
||||
}
|
||||
if id1 == "" {
|
||||
t.Fatal("expected non-empty ID")
|
||||
}
|
||||
|
||||
// Update reaction.
|
||||
allergy.ReactionSeverity = "moderate"
|
||||
allergy.ReactionManifestation = "Rash"
|
||||
id2, err := store.UpsertAllergyIntolerance(allergy)
|
||||
if err != nil {
|
||||
t.Fatalf("update UpsertAllergyIntolerance: %v", err)
|
||||
}
|
||||
if id1 != id2 {
|
||||
t.Errorf("ID changed on upsert: was %q, got %q", id1, id2)
|
||||
}
|
||||
|
||||
rows, err := store.ListAllergyIntolerances(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListAllergyIntolerances: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 allergy, got %d", len(rows))
|
||||
}
|
||||
got := rows[0]
|
||||
if got.ReactionSeverity != "moderate" {
|
||||
t.Errorf("ReactionSeverity: got %q, want moderate", got.ReactionSeverity)
|
||||
}
|
||||
if got.ReactionManifestation != "Rash" {
|
||||
t.Errorf("ReactionManifestation: got %q, want Rash", got.ReactionManifestation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertAllergyIntolerance_NoReaction(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
allergy := &models.AllergyIntolerance{
|
||||
FHIRID: "allergy-no-rxn",
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
ClinicalStatus: "active",
|
||||
CodeText: "Latex",
|
||||
}
|
||||
_, err := store.UpsertAllergyIntolerance(allergy)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertAllergyIntolerance (no reaction): %v", err)
|
||||
}
|
||||
|
||||
rows, err := store.ListAllergyIntolerances(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListAllergyIntolerances: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1, got %d", len(rows))
|
||||
}
|
||||
if rows[0].ReactionSeverity != "" {
|
||||
t.Errorf("expected empty ReactionSeverity, got %q", rows[0].ReactionSeverity)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Immunization tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpsertImmunization_NewAndUpdate(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
imm := &models.Immunization{
|
||||
FHIRID: "imm-001",
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
Status: "completed",
|
||||
VaccineText: "Influenza, seasonal",
|
||||
VaccineSystem: "http://hl7.org/fhir/sid/cvx",
|
||||
VaccineCode: "141",
|
||||
OccurrenceDate: "2023-10-01",
|
||||
PrimarySource: true,
|
||||
LotNumber: "LOT123",
|
||||
}
|
||||
|
||||
id1, err := store.UpsertImmunization(imm)
|
||||
if err != nil {
|
||||
t.Fatalf("initial UpsertImmunization: %v", err)
|
||||
}
|
||||
if id1 == "" {
|
||||
t.Fatal("expected non-empty ID")
|
||||
}
|
||||
|
||||
imm.LotNumber = "LOT456"
|
||||
id2, err := store.UpsertImmunization(imm)
|
||||
if err != nil {
|
||||
t.Fatalf("update UpsertImmunization: %v", err)
|
||||
}
|
||||
if id1 != id2 {
|
||||
t.Errorf("ID changed on upsert: was %q, got %q", id1, id2)
|
||||
}
|
||||
|
||||
rows, err := store.ListImmunizations(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListImmunizations: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 immunization, got %d", len(rows))
|
||||
}
|
||||
if rows[0].LotNumber != "LOT456" {
|
||||
t.Errorf("LotNumber: got %q, want LOT456", rows[0].LotNumber)
|
||||
}
|
||||
if !rows[0].PrimarySource {
|
||||
t.Error("PrimarySource should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImmunizations_Empty(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
rows, err := store.ListImmunizations("no-such-patient", testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListImmunizations: %v", err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImmunizations_OrderedNewestFirst(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
for _, item := range []struct {
|
||||
id string
|
||||
date string
|
||||
}{
|
||||
{"imm-a", "2022-09-01"},
|
||||
{"imm-b", "2023-10-15"},
|
||||
{"imm-c", "2021-03-01"},
|
||||
} {
|
||||
_, err := store.UpsertImmunization(&models.Immunization{
|
||||
FHIRID: item.id,
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
Status: "completed",
|
||||
OccurrenceDate: item.date,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertImmunization %s: %v", item.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := store.ListImmunizations(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListImmunizations: %v", err)
|
||||
}
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("expected 3, got %d", len(rows))
|
||||
}
|
||||
if rows[0].FHIRID != "imm-b" {
|
||||
t.Errorf("first: got %q, want imm-b", rows[0].FHIRID)
|
||||
}
|
||||
if rows[2].FHIRID != "imm-c" {
|
||||
t.Errorf("last: got %q, want imm-c", rows[2].FHIRID)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Procedure tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpsertProcedure_NewAndUpdate(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
proc := &models.Procedure{
|
||||
FHIRID: "proc-001",
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
Status: "completed",
|
||||
CodeText: "Appendectomy",
|
||||
CodeSystem: "http://snomed.info/sct",
|
||||
CodeCode: "80146002",
|
||||
PerformedDate: "2019-06-15",
|
||||
ReasonText: "Acute appendicitis",
|
||||
Outcome: "Successful procedure",
|
||||
}
|
||||
|
||||
id1, err := store.UpsertProcedure(proc)
|
||||
if err != nil {
|
||||
t.Fatalf("initial UpsertProcedure: %v", err)
|
||||
}
|
||||
if id1 == "" {
|
||||
t.Fatal("expected non-empty ID")
|
||||
}
|
||||
|
||||
proc.Outcome = "Procedure completed without complications"
|
||||
id2, err := store.UpsertProcedure(proc)
|
||||
if err != nil {
|
||||
t.Fatalf("update UpsertProcedure: %v", err)
|
||||
}
|
||||
if id1 != id2 {
|
||||
t.Errorf("ID changed on upsert: was %q, got %q", id1, id2)
|
||||
}
|
||||
|
||||
rows, err := store.ListProcedures(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListProcedures: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 procedure, got %d", len(rows))
|
||||
}
|
||||
if rows[0].Outcome != "Procedure completed without complications" {
|
||||
t.Errorf("Outcome: got %q", rows[0].Outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProcedures_Empty(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
rows, err := store.ListProcedures("no-such-patient", testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListProcedures: %v", err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Encounter tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpsertEncounter_NewAndUpdate(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
enc := &models.Encounter{
|
||||
FHIRID: "enc-001",
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
Status: "finished",
|
||||
Class: "AMB",
|
||||
TypeText: "Office visit",
|
||||
PeriodStart: "2024-03-10",
|
||||
PeriodEnd: "2024-03-10",
|
||||
ReasonText: "Annual physical",
|
||||
}
|
||||
|
||||
id1, err := store.UpsertEncounter(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("initial UpsertEncounter: %v", err)
|
||||
}
|
||||
if id1 == "" {
|
||||
t.Fatal("expected non-empty ID")
|
||||
}
|
||||
|
||||
enc.Status = "cancelled"
|
||||
id2, err := store.UpsertEncounter(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("update UpsertEncounter: %v", err)
|
||||
}
|
||||
if id1 != id2 {
|
||||
t.Errorf("ID changed on upsert: was %q, got %q", id1, id2)
|
||||
}
|
||||
|
||||
rows, err := store.ListEncounters(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListEncounters: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 encounter, got %d", len(rows))
|
||||
}
|
||||
if rows[0].Status != "cancelled" {
|
||||
t.Errorf("Status: got %q, want cancelled", rows[0].Status)
|
||||
}
|
||||
if rows[0].Class != "AMB" {
|
||||
t.Errorf("Class: got %q, want AMB", rows[0].Class)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEncounters_Empty(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
rows, err := store.ListEncounters("no-such-patient", testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListEncounters: %v", err)
|
||||
}
|
||||
if len(rows) != 0 {
|
||||
t.Errorf("expected 0, got %d", len(rows))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEncounters_OrderedNewestFirst(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
|
||||
for _, item := range []struct {
|
||||
id string
|
||||
start string
|
||||
}{
|
||||
{"enc-a", "2023-01-01"},
|
||||
{"enc-b", "2024-06-01"},
|
||||
{"enc-c", "2022-12-01"},
|
||||
} {
|
||||
_, err := store.UpsertEncounter(&models.Encounter{
|
||||
FHIRID: item.id,
|
||||
EHRURL: testEHRURL,
|
||||
PatientFHIRID: testPatientID,
|
||||
Status: "finished",
|
||||
PeriodStart: item.start,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertEncounter %s: %v", item.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := store.ListEncounters(testPatientID, testEHRURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ListEncounters: %v", err)
|
||||
}
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("expected 3, got %d", len(rows))
|
||||
}
|
||||
if rows[0].FHIRID != "enc-b" {
|
||||
t.Errorf("first: got %q, want enc-b", rows[0].FHIRID)
|
||||
}
|
||||
if rows[2].FHIRID != "enc-c" {
|
||||
t.Errorf("last: got %q, want enc-c", rows[2].FHIRID)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PatientSync tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
102
app/db/db.go
102
app/db/db.go
@@ -239,12 +239,81 @@ var migrations = []migration{
|
||||
CREATE INDEX IF NOT EXISTS idx_allergy_intolerances_patient ON allergy_intolerances(patient_fhir_id, ehr_url);
|
||||
`,
|
||||
},
|
||||
// Future migrations: append new entries here with incrementing version numbers.
|
||||
// Example:
|
||||
// {
|
||||
// version: 2,
|
||||
// sql: `ALTER TABLE users ADD COLUMN phone TEXT NOT NULL DEFAULT '';`,
|
||||
// },
|
||||
{
|
||||
version: 5,
|
||||
sql: `
|
||||
ALTER TABLE allergy_intolerances ADD COLUMN reaction_severity TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE allergy_intolerances ADD COLUMN reaction_manifestation TEXT NOT NULL DEFAULT '';
|
||||
`,
|
||||
},
|
||||
{
|
||||
version: 6,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS immunizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
fhir_id TEXT NOT NULL,
|
||||
ehr_url TEXT NOT NULL,
|
||||
patient_fhir_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
vaccine_text TEXT NOT NULL DEFAULT '',
|
||||
vaccine_system TEXT NOT NULL DEFAULT '',
|
||||
vaccine_code TEXT NOT NULL DEFAULT '',
|
||||
occurrence_date TEXT NOT NULL DEFAULT '',
|
||||
primary_source INTEGER NOT NULL DEFAULT 0,
|
||||
lot_number TEXT NOT NULL DEFAULT '',
|
||||
synced_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(fhir_id, ehr_url)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_immunizations_patient ON immunizations(patient_fhir_id, ehr_url);
|
||||
`,
|
||||
},
|
||||
{
|
||||
version: 7,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS procedures (
|
||||
id TEXT PRIMARY KEY,
|
||||
fhir_id TEXT NOT NULL,
|
||||
ehr_url TEXT NOT NULL,
|
||||
patient_fhir_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
code_text TEXT NOT NULL DEFAULT '',
|
||||
code_system TEXT NOT NULL DEFAULT '',
|
||||
code_code TEXT NOT NULL DEFAULT '',
|
||||
performed_date TEXT NOT NULL DEFAULT '',
|
||||
reason_text TEXT NOT NULL DEFAULT '',
|
||||
outcome TEXT NOT NULL DEFAULT '',
|
||||
synced_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(fhir_id, ehr_url)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_procedures_patient ON procedures(patient_fhir_id, ehr_url);
|
||||
`,
|
||||
},
|
||||
{
|
||||
version: 8,
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS encounters (
|
||||
id TEXT PRIMARY KEY,
|
||||
fhir_id TEXT NOT NULL,
|
||||
ehr_url TEXT NOT NULL,
|
||||
patient_fhir_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT '',
|
||||
class TEXT NOT NULL DEFAULT '',
|
||||
type_text TEXT NOT NULL DEFAULT '',
|
||||
period_start TEXT NOT NULL DEFAULT '',
|
||||
period_end TEXT NOT NULL DEFAULT '',
|
||||
reason_text TEXT NOT NULL DEFAULT '',
|
||||
synced_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(fhir_id, ehr_url)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_encounters_patient ON encounters(patient_fhir_id, ehr_url);
|
||||
`,
|
||||
},
|
||||
{
|
||||
version: 9,
|
||||
sql: `
|
||||
ALTER TABLE users ADD COLUMN mrn TEXT NOT NULL DEFAULT '';
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
// migrate applies any migrations that have not yet been run, in order.
|
||||
@@ -315,6 +384,7 @@ func (s *Store) UpsertUser(u *models.User) (string, error) {
|
||||
first_name = ?,
|
||||
middle_name = ?,
|
||||
last_name = ?,
|
||||
mrn = ?,
|
||||
dob = ?,
|
||||
gender = ?,
|
||||
email = ?,
|
||||
@@ -322,7 +392,7 @@ func (s *Store) UpsertUser(u *models.User) (string, error) {
|
||||
role = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?`,
|
||||
u.FirstName, u.MiddleName, u.LastName,
|
||||
u.FirstName, u.MiddleName, u.LastName, u.MRN,
|
||||
u.DOB, u.Gender, u.Email,
|
||||
u.FHIRResourceType, string(u.Role),
|
||||
now, existingID,
|
||||
@@ -342,11 +412,11 @@ func (s *Store) UpsertUser(u *models.User) (string, error) {
|
||||
_, err = s.db.Exec(`
|
||||
INSERT INTO users (
|
||||
id, fhir_resource_type, fhir_id, ehr_url, role,
|
||||
first_name, middle_name, last_name, dob, gender, email,
|
||||
first_name, middle_name, last_name, mrn, dob, gender, email,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, u.FHIRResourceType, u.FHIRID, u.EHRURL, string(u.Role),
|
||||
u.FirstName, u.MiddleName, u.LastName, u.DOB, u.Gender, u.Email,
|
||||
u.FirstName, u.MiddleName, u.LastName, u.MRN, u.DOB, u.Gender, u.Email,
|
||||
now, now,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -361,13 +431,13 @@ func (s *Store) GetUserByFHIRID(fhirID, ehrURL string) (*models.User, error) {
|
||||
u := &models.User{}
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, fhir_resource_type, fhir_id, ehr_url, role,
|
||||
first_name, middle_name, last_name, dob, gender, email,
|
||||
first_name, middle_name, last_name, mrn, dob, gender, email,
|
||||
created_at, updated_at
|
||||
FROM users WHERE fhir_id = ? AND ehr_url = ?`,
|
||||
fhirID, ehrURL,
|
||||
).Scan(
|
||||
&u.ID, &u.FHIRResourceType, &u.FHIRID, &u.EHRURL, &u.Role,
|
||||
&u.FirstName, &u.MiddleName, &u.LastName, &u.DOB, &u.Gender, &u.Email,
|
||||
&u.FirstName, &u.MiddleName, &u.LastName, &u.MRN, &u.DOB, &u.Gender, &u.Email,
|
||||
&u.CreatedAt, &u.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -381,12 +451,12 @@ func (s *Store) GetUserByID(id string) (*models.User, error) {
|
||||
u := &models.User{}
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, fhir_resource_type, fhir_id, ehr_url, role,
|
||||
first_name, middle_name, last_name, dob, gender, email,
|
||||
first_name, middle_name, last_name, mrn, dob, gender, email,
|
||||
created_at, updated_at
|
||||
FROM users WHERE id = ?`, id,
|
||||
).Scan(
|
||||
&u.ID, &u.FHIRResourceType, &u.FHIRID, &u.EHRURL, &u.Role,
|
||||
&u.FirstName, &u.MiddleName, &u.LastName, &u.DOB, &u.Gender, &u.Email,
|
||||
&u.FirstName, &u.MiddleName, &u.LastName, &u.MRN, &u.DOB, &u.Gender, &u.Email,
|
||||
&u.CreatedAt, &u.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -399,7 +469,7 @@ func (s *Store) GetUserByID(id string) (*models.User, error) {
|
||||
func (s *Store) ListUsersByRole(role models.Role, ehrURL string) ([]models.User, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, fhir_resource_type, fhir_id, ehr_url, role,
|
||||
first_name, middle_name, last_name, dob, gender, email,
|
||||
first_name, middle_name, last_name, mrn, dob, gender, email,
|
||||
created_at, updated_at
|
||||
FROM users WHERE role = ? AND ehr_url = ?
|
||||
ORDER BY last_name ASC, first_name ASC`,
|
||||
@@ -415,7 +485,7 @@ func (s *Store) ListUsersByRole(role models.Role, ehrURL string) ([]models.User,
|
||||
var u models.User
|
||||
if err := rows.Scan(
|
||||
&u.ID, &u.FHIRResourceType, &u.FHIRID, &u.EHRURL, &u.Role,
|
||||
&u.FirstName, &u.MiddleName, &u.LastName, &u.DOB, &u.Gender, &u.Email,
|
||||
&u.FirstName, &u.MiddleName, &u.LastName, &u.MRN, &u.DOB, &u.Gender, &u.Email,
|
||||
&u.CreatedAt, &u.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("db: scan user: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user