Skip to content

Commit 377b00c

Browse files
committed
#13772 - Add Epipulse export functionality for IPI disease
1 parent c97279a commit 377b00c

15 files changed

+1569
-90
lines changed

sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportEntryDto.java

Lines changed: 531 additions & 0 deletions
Large diffs are not rendered by default.

sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseDiseaseExportFacade.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ public interface EpipulseDiseaseExportFacade {
2323
public void startPertussisExport(String uuid);
2424

2525
public void startMeaslesExport(String uuid);
26+
27+
public void startIpiExport(String uuid);
2628
}

sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseLaboratoryMapper.java

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,354 @@ public static Boolean deriveClinicalCriteriaStatus(YesNoUnknown clinicalConfirma
324324
}
325325
return clinicalConfirmation == YesNoUnknown.YES;
326326
}
327+
328+
// ==================== IPI-Specific Mappers ====================
329+
330+
/**
331+
* Maps SORMAS SampleMaterial enum to EpiPulse IPI specimen type codes.
332+
* <p>
333+
* EpiPulse Reference Values for IPI:
334+
* - BLOOD = Blood
335+
* - CSF = Cerebrospinal fluid
336+
* - OTH = Other
337+
* - PLEURAL = Pleural fluid
338+
* - SER = Serum
339+
* - SYNOVIAL = Synovial fluid (joint fluid)
340+
* - THROAT = Throat swab
341+
* - NPSWAB = Nasopharyngeal swab
342+
*
343+
* @param sampleMaterial
344+
* SORMAS sample material enum
345+
* @return EpiPulse IPI specimen type code, or null if not mappable
346+
*/
347+
public static String mapSampleMaterialToIpiSpecimenCode(SampleMaterial sampleMaterial) {
348+
if (sampleMaterial == null) {
349+
return null;
350+
}
351+
352+
switch (sampleMaterial) {
353+
case BLOOD:
354+
return "BLOOD";
355+
case SERA:
356+
return "SER"; // Serum
357+
case CEREBROSPINAL_FLUID:
358+
return "CSF";
359+
case PLEURAL_FLUID:
360+
return "PLEURAL";
361+
case SYNOVIAL_FLUID:
362+
return "SYNOVIAL";
363+
case THROAT_SWAB:
364+
return "THROAT";
365+
case NP_SWAB:
366+
return "NPSWAB";
367+
case OTHER:
368+
return "OTH";
369+
default:
370+
return null;
371+
}
372+
}
373+
374+
/**
375+
* Maps SORMAS Symptoms to EpiPulse IPI clinical presentation codes.
376+
* <p>
377+
* EpiPulse Reference Values for IPI:
378+
* - MENING = Meningitis
379+
* - SEPT = Septicaemia
380+
* - PNEUM = Pneumonia
381+
* - OME = Otitis media
382+
* - PERITON = Peritonitis
383+
* - ARTH = Arthritis
384+
* - OTH = Other
385+
* - ASYMP = Asymptomatic
386+
* - NONE = No clinical presentation recorded
387+
*
388+
* @param meningitis
389+
* Meningitis symptom state (IPI-specific)
390+
* @param septicaemia
391+
* Septicaemia symptom state (IPI-specific)
392+
* @param pneumonia
393+
* Pneumonia symptom state
394+
* @param otitisMedia
395+
* Otitis media symptom state
396+
* @param peritonitis
397+
* Peritonitis symptom state
398+
* @param arthritis
399+
* Arthritis symptom state
400+
* @param otherClinical
401+
* Other clinical presentation symptom state
402+
* @param asymptomatic
403+
* Asymptomatic symptom state
404+
* @return List of EpiPulse clinical presentation codes (empty list returns "NONE" in CSV)
405+
*/
406+
public static List<String> mapSymptomsToClinicalPresentation(
407+
SymptomState meningitis,
408+
SymptomState septicaemia,
409+
SymptomState pneumonia,
410+
SymptomState otitisMedia,
411+
SymptomState peritonitis,
412+
SymptomState arthritis,
413+
SymptomState otherClinical,
414+
SymptomState asymptomatic) {
415+
416+
List<String> presentations = new ArrayList<>();
417+
418+
// Priority: IPI-defining symptoms first
419+
if (meningitis == SymptomState.YES) {
420+
presentations.add("MENING");
421+
}
422+
if (septicaemia == SymptomState.YES) {
423+
presentations.add("SEPT");
424+
}
425+
if (pneumonia == SymptomState.YES) {
426+
presentations.add("PNEUM");
427+
}
428+
if (otitisMedia == SymptomState.YES) {
429+
presentations.add("OME");
430+
}
431+
if (peritonitis == SymptomState.YES) {
432+
presentations.add("PERITON");
433+
}
434+
if (arthritis == SymptomState.YES) {
435+
presentations.add("ARTH");
436+
}
437+
if (otherClinical == SymptomState.YES) {
438+
presentations.add("OTH");
439+
}
440+
if (asymptomatic == SymptomState.YES) {
441+
presentations.add("ASYMP");
442+
}
443+
444+
// If no presentations found, empty list will result in "NONE" in CSV
445+
return presentations;
446+
}
447+
448+
/**
449+
* Maps SORMAS drug susceptibility test result to EpiPulse antibiotic resistance codes.
450+
* <p>
451+
* EpiPulse Reference Values:
452+
* - RESIST = Resistant
453+
* - SENS = Sensitive
454+
* - INTER = Intermediate resistance
455+
* - NOTEST = Not tested
456+
*
457+
* @param testResult
458+
* SORMAS PathogenTestResultType from drug susceptibility test
459+
* @return EpiPulse antibiotic resistance code
460+
*/
461+
public static String mapDrugSusceptibilityToEpipulseCode(PathogenTestResultType testResult) {
462+
if (testResult == null) {
463+
return null;
464+
}
465+
466+
switch (testResult) {
467+
case POSITIVE:
468+
return "RESIST"; // Positive drug susceptibility = Resistant
469+
case NEGATIVE:
470+
return "SENS"; // Negative drug susceptibility = Sensitive
471+
case INDETERMINATE:
472+
return "INTER"; // Intermediate resistance
473+
case PENDING:
474+
case NOT_DONE:
475+
return "NOTEST";
476+
default:
477+
return null;
478+
}
479+
}
480+
481+
/**
482+
* Validates and normalizes pneumococcal serotype string for EpiPulse export.
483+
* <p>
484+
* Pneumococcal serotypes include 90+ types: 1, 2, 3, 4, 5, 6A, 6B, 7F, 8, 9N, 9V, 10A, etc.
485+
* Accepts formats like "6A", "19F", "23F", "SEROTYPE 6A", "TYPE 19F", etc.
486+
*
487+
* @param serotypeText
488+
* SORMAS serotype string (from PathogenTest typingId or serotype field)
489+
* @return Normalized EpiPulse serotype code (e.g., "6A", "19F"), or null if invalid
490+
*/
491+
public static String normalizeSerotypeForEpipulse(String serotypeText) {
492+
if (serotypeText == null || serotypeText.trim().isEmpty()) {
493+
return null;
494+
}
495+
496+
String normalized = serotypeText.trim().toUpperCase();
497+
498+
// Remove common prefixes: "SEROTYPE ", "TYPE ", "S.", "PNEUMOCOCCAL ", etc.
499+
normalized = normalized.replaceAll("^(SEROTYPE|TYPE|S\\.|PNEUMOCOCCAL|STREPTOCOCCUS PNEUMONIAE)\\s*", "");
500+
501+
// Validate format: digit(s) optionally followed by letter(s)
502+
// Examples: "1", "6A", "6B", "19F", "23F", "15A", "33F"
503+
if (normalized.matches("^\\d{1,2}[A-Z]{0,2}$")) {
504+
return normalized;
505+
}
506+
507+
return null; // Invalid format
508+
}
509+
510+
/**
511+
* Maps SORMAS Symptoms to EpiPulse ClinicalCriteria codes for PNEU.
512+
* <p>
513+
* EpiPulse Reference Values for PNEU ClinicalCriteria:
514+
* - BACTERPNEUMO = Bacteraemic pneumonia
515+
* - MENI = Meningitis/Meningeal/Meningoencephalitic
516+
* - MENISEPTI = Meningitis and septicaemia
517+
* - OTH = Other
518+
* - SEPTI = Septicaemia
519+
*
520+
* @param meningitis Meningitis symptom state
521+
* @param septicaemia Septicaemia symptom state
522+
* @param pneumonia Pneumonia symptom state (clinical or radiologic)
523+
* @return EpiPulse clinical criteria code, or null if no criteria met
524+
*/
525+
public static String mapSymptomsToClinicalCriteria(
526+
SymptomState meningitis,
527+
SymptomState septicaemia,
528+
SymptomState pneumonia) {
529+
530+
boolean hasMeningitis = meningitis == SymptomState.YES;
531+
boolean hasSepticaemia = septicaemia == SymptomState.YES;
532+
boolean hasPneumonia = pneumonia == SymptomState.YES;
533+
534+
// Priority order based on severity/specificity
535+
if (hasMeningitis && hasSepticaemia) {
536+
return "MENISEPTI"; // Both present
537+
} else if (hasMeningitis) {
538+
return "MENI"; // Meningitis only
539+
} else if (hasSepticaemia) {
540+
return "SEPTI"; // Septicaemia only
541+
} else if (hasPneumonia) {
542+
return "BACTERPNEUMO"; // Bacteraemic pneumonia
543+
}
544+
545+
return null; // No specific clinical criteria met
546+
}
547+
548+
/**
549+
* Maps SORMAS Vaccine enum to EpiPulse vaccine codes for PNEU.
550+
* <p>
551+
* EpiPulse Reference Values for PNEU Vaccine:
552+
* - PCV7 = Pneumococcal conjugate vaccine 7
553+
* - PCV10 = Pneumococcal conjugate vaccine 10
554+
* - PCV13 = Pneumococcal conjugate vaccine 13
555+
* - PCV15 = Pneumococcal conjugate vaccine 15
556+
* - PCV20 = Pneumococcal conjugate vaccine 20
557+
* - PCV3 = Pneumococcal conjugate vaccine - third dose
558+
* - PPV23 = Pneumococcal polysaccharide vaccine
559+
*
560+
* @param vaccineName SORMAS vaccine name/type
561+
* @return EpiPulse vaccine code, or null if not pneumococcal vaccine
562+
*/
563+
public static String mapVaccineToEpipulseCode(String vaccineName) {
564+
if (vaccineName == null || vaccineName.trim().isEmpty()) {
565+
return null;
566+
}
567+
568+
String normalized = vaccineName.trim().toUpperCase();
569+
570+
// Map PCV vaccines
571+
if (normalized.contains("PCV") || normalized.contains("CONJUGATE")) {
572+
if (normalized.contains("20")) {
573+
return "PCV20";
574+
} else if (normalized.contains("15")) {
575+
return "PCV15";
576+
} else if (normalized.contains("13")) {
577+
return "PCV13";
578+
} else if (normalized.contains("10")) {
579+
return "PCV10";
580+
} else if (normalized.contains("7")) {
581+
return "PCV7";
582+
} else if (normalized.contains("3")) {
583+
return "PCV3";
584+
}
585+
// Default PCV if no specific number
586+
return "PCV13"; // Most common
587+
}
588+
589+
// Map PPV vaccine
590+
if (normalized.contains("PPV") || normalized.contains("POLYSACCHARIDE") || normalized.contains("23")) {
591+
return "PPV23";
592+
}
593+
594+
// Check for general pneumococcal terms
595+
if (normalized.contains("PNEUMO")) {
596+
return "PCV13"; // Default to most common PCV
597+
}
598+
599+
return null; // Not a pneumococcal vaccine
600+
}
601+
602+
/**
603+
* Maps DrugSusceptibilityType enum to EpiPulse SIR codes.
604+
* <p>
605+
* EpiPulse Reference Values for SIR (Susceptible/Intermediate/Resistant):
606+
* - S = Susceptible
607+
* - I = Intermediate
608+
* - R = Resistant
609+
*
610+
* @param susceptibility SORMAS drug susceptibility enum
611+
* @return EpiPulse SIR code (S/I/R), or null if not tested
612+
*/
613+
public static String mapDrugSusceptibilityToSIR(String susceptibility) {
614+
if (susceptibility == null || susceptibility.trim().isEmpty()) {
615+
return null;
616+
}
617+
618+
String normalized = susceptibility.trim().toUpperCase();
619+
620+
if (normalized.equals("SUSCEPTIBLE") || normalized.equals("S")) {
621+
return "S";
622+
} else if (normalized.equals("INTERMEDIATE") || normalized.equals("I")) {
623+
return "I";
624+
} else if (normalized.equals("RESISTANT") || normalized.equals("R")) {
625+
return "R";
626+
}
627+
628+
return null; // Unknown susceptibility
629+
}
630+
631+
/**
632+
* Maps SORMAS PathogenTestType to EpiPulse PathogenDetectionMethod codes for PNEU.
633+
* <p>
634+
* EpiPulse Reference Values for PNEU PathogenDetectionMethod:
635+
* - COAGG = Coagglutination
636+
* - GDIFF = Gel diffusion
637+
* - MPCR = Multiplex PCR
638+
* - OTH = Other
639+
* - PTEST = Pneumotest
640+
* - QUE = Quellung
641+
* - SLAGG = Slide agglutination
642+
*
643+
* @param testType SORMAS pathogen test type
644+
* @return EpiPulse detection method code, or null if not mappable
645+
*/
646+
public static String mapPathogenTestTypeToDetectionMethod(String testType) {
647+
if (testType == null || testType.trim().isEmpty()) {
648+
return null;
649+
}
650+
651+
String normalized = testType.trim().toUpperCase();
652+
653+
// Map specific test types
654+
if (normalized.contains("PCR") || normalized.contains("RT-PCR") || normalized.contains("RTPCR")) {
655+
if (normalized.contains("MULTIPLEX")) {
656+
return "MPCR"; // Multiplex PCR
657+
}
658+
return "MPCR"; // Default PCR to multiplex
659+
} else if (normalized.contains("QUELLUNG")) {
660+
return "QUE";
661+
} else if (normalized.contains("COAGG") || normalized.contains("CO-AGGLUTINATION")) {
662+
return "COAGG";
663+
} else if (normalized.contains("SLIDE") && normalized.contains("AGGLUT")) {
664+
return "SLAGG";
665+
} else if (normalized.contains("GEL") && normalized.contains("DIFF")) {
666+
return "GDIFF";
667+
} else if (normalized.contains("PNEUMOTEST")) {
668+
return "PTEST";
669+
} else if (normalized.contains("CULTURE")) {
670+
return "QUE"; // Culture often followed by Quellung
671+
} else if (normalized.contains("SEROGROUPING") || normalized.contains("SEROTYPING")) {
672+
return "QUE"; // Serotyping typically uses Quellung
673+
}
674+
675+
return "OTH"; // Other methods
676+
}
327677
}

sormas-api/src/main/java/de/symeda/sormas/api/epipulse/EpipulseSubjectCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
public enum EpipulseSubjectCode {
2525

2626
PERT(true, Disease.PERTUSSIS, false),
27-
MEAS(true, Disease.MEASLES, false);
27+
MEAS(true, Disease.MEASLES, false),
28+
PNEU(true, Disease.INVASIVE_PNEUMOCOCCAL_INFECTION, false); // Invasive Pneumococcal Infection
2829

2930
private final boolean diseaseModel;
3031
private final Disease disease;

sormas-api/src/main/java/de/symeda/sormas/api/epipulse/referencevalue/EpipulseDiseaseRef.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
public enum EpipulseDiseaseRef {
2222

2323
PERT(EpipulseSubjectCode.PERT),
24-
MEAS(EpipulseSubjectCode.MEAS);
24+
MEAS(EpipulseSubjectCode.MEAS),
25+
PNEU(EpipulseSubjectCode.PNEU); // Invasive Pneumococcal Infection
2526

2627
private final EpipulseSubjectCode[] subjectCodes;
2728

0 commit comments

Comments
 (0)