diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 81b0c537d6..81f173456a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,7 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + --name sr_test_db ports: - 5432:5432 steps: @@ -60,6 +61,7 @@ jobs: run: | chmod 0600 $PGPASSFILE db-setup/create-db.sh + docker restart --time 0 sr_test_db - name: Run tests env: OKTA_TESTING_DISABLEHTTPS: true diff --git a/backend/db-setup/reset-db.sql b/backend/db-setup/reset-db.sql index 7ad613e0cb..5878329033 100644 --- a/backend/db-setup/reset-db.sql +++ b/backend/db-setup/reset-db.sql @@ -1,3 +1,5 @@ +ALTER SYSTEM SET max_connections = 500; + DROP SCHEMA IF EXISTS simple_report CASCADE; CREATE SCHEMA IF NOT EXISTS simple_report; diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/SimpleReportApplication.java b/backend/src/main/java/gov/cdc/usds/simplereport/SimpleReportApplication.java index cbb96015a6..66d63e3ccc 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/SimpleReportApplication.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/SimpleReportApplication.java @@ -14,6 +14,7 @@ import gov.cdc.usds.simplereport.service.OrganizationInitializingService; import gov.cdc.usds.simplereport.service.ScheduledTasksService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -24,7 +25,9 @@ import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.core.context.SecurityContextHolder; @Slf4j @SpringBootApplication @@ -40,6 +43,7 @@ CorsProperties.class, AzureStorageQueueReportingProperties.class }) +@EnableAsync @EnableScheduling @EnableFeignClients public class SimpleReportApplication { @@ -47,6 +51,12 @@ public static void main(String[] args) { SpringApplication.run(SimpleReportApplication.class, args); } + @Bean + public InitializingBean initializingBean() { + return () -> + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); + } + @Bean public CommandLineRunner initDiseasesOnStartup(DiseaseService initService) { return args -> initService.initDiseases(); diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepository.java b/backend/src/main/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepository.java index 3729f45198..675042558d 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepository.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepository.java @@ -26,8 +26,11 @@ import java.util.UUID; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.support.ScopeNotActiveException; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; /** Handles all user/organization management in Okta */ @@ -309,13 +312,20 @@ private Optional getOrganizationRoleClaimsFromTenantData public Optional getOrganizationRoleClaimsForUser(String username) { // when accessing tenant data, bypass okta and get org from the altered authorities - if (tenantDataContextHolder.hasBeenPopulated() - && username.equals(tenantDataContextHolder.getUsername())) { - return getOrganizationRoleClaimsFromTenantDataAccess( - tenantDataContextHolder.getAuthorities()); + try { + if (tenantDataContextHolder.hasBeenPopulated() + && username.equals(tenantDataContextHolder.getUsername())) { + return getOrganizationRoleClaimsFromTenantDataAccess( + tenantDataContextHolder.getAuthorities()); + } + return Optional.ofNullable(usernameOrgRolesMap.get(username)); + } catch (ScopeNotActiveException e) { + Set authorities = + SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return getOrganizationRoleClaimsFromTenantDataAccess(authorities); } - - return Optional.ofNullable(usernameOrgRolesMap.get(username)); } public void reset() { diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java index 6929095b42..36c55ade30 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java @@ -37,6 +37,7 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.ScopeNotActiveException; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; @@ -505,13 +506,17 @@ private ApiUser getCurrentApiUser() { return getCurrentApiUserNoCache(); } - if (_apiUserContextHolder.hasBeenPopulated()) { - log.debug("Retrieving user from request context"); - return _apiUserContextHolder.getCurrentApiUser(); + try { + if (_apiUserContextHolder.hasBeenPopulated()) { + log.debug("Retrieving user from request context"); + return _apiUserContextHolder.getCurrentApiUser(); + } + ApiUser user = getCurrentApiUserNoCache(); + _apiUserContextHolder.setCurrentApiUser(user); + return user; + } catch (ScopeNotActiveException e) { + return getCurrentApiUserNoCache(); } - ApiUser user = getCurrentApiUserNoCache(); - _apiUserContextHolder.setCurrentApiUser(user); - return user; } private ApiUser getCurrentApiUserNoCache() { diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java index 598fb24b7d..1cd386c7e0 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.support.ScopeNotActiveException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -51,12 +52,16 @@ public void resetOrganizationRolesContext() { } public Optional getCurrentOrganizationRoles() { - if (organizationRolesContext.hasBeenPopulated()) { - return organizationRolesContext.getOrganizationRoles(); + try { + if (organizationRolesContext.hasBeenPopulated()) { + return organizationRolesContext.getOrganizationRoles(); + } + var result = fetchCurrentOrganizationRoles(); + organizationRolesContext.setOrganizationRoles(result); + return result; + } catch (ScopeNotActiveException e) { + return fetchCurrentOrganizationRoles(); } - var result = fetchCurrentOrganizationRoles(); - organizationRolesContext.setOrganizationRoles(result); - return result; } private Optional fetchCurrentOrganizationRoles() { diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadService.java index 07a3d7238d..b9e21ea74b 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadService.java @@ -1,75 +1,39 @@ package gov.cdc.usds.simplereport.service; -import static gov.cdc.usds.simplereport.api.Translators.parsePersonRole; -import static gov.cdc.usds.simplereport.api.Translators.parsePhoneType; -import static gov.cdc.usds.simplereport.api.Translators.parseUserShortDate; -import static gov.cdc.usds.simplereport.api.Translators.parseYesNo; -import static gov.cdc.usds.simplereport.validators.CsvValidatorUtils.convertEthnicityToDatabaseValue; -import static gov.cdc.usds.simplereport.validators.CsvValidatorUtils.convertRaceToDatabaseValue; -import static gov.cdc.usds.simplereport.validators.CsvValidatorUtils.convertSexToDatabaseValue; - -import com.fasterxml.jackson.databind.MappingIterator; import gov.cdc.usds.simplereport.api.model.errors.CsvProcessingException; import gov.cdc.usds.simplereport.api.model.filerow.PatientUploadRow; import gov.cdc.usds.simplereport.api.uploads.PatientBulkUploadResponse; import gov.cdc.usds.simplereport.config.AuthorizationConfiguration; -import gov.cdc.usds.simplereport.db.model.Facility; -import gov.cdc.usds.simplereport.db.model.Organization; -import gov.cdc.usds.simplereport.db.model.Person; -import gov.cdc.usds.simplereport.db.model.PhoneNumber; -import gov.cdc.usds.simplereport.db.model.auxiliary.StreetAddress; import gov.cdc.usds.simplereport.db.model.auxiliary.UploadStatus; import gov.cdc.usds.simplereport.service.model.reportstream.FeedbackMessage; -import gov.cdc.usds.simplereport.validators.CsvValidatorUtils; import gov.cdc.usds.simplereport.validators.FileValidator; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; /** * Service to upload a roster of patient data given a CSV input. Formerly restricted to superusers - * but now (almost) available to end users. - * - *

Updated by emmastephenson on 10/24/2022 + * but now available to end users. */ @Service -@Transactional @RequiredArgsConstructor @Slf4j public class PatientBulkUploadService { - private final PersonService _personService; - private final AddressValidationService _addressValidationService; - private final OrganizationService _organizationService; - private final FileValidator _patientBulkUploadFileValidator; + private final FileValidator patientUploadRowFileValidator; + private final PatientBulkUploadServiceAsync patientBulkUploadServiceAsync; - // This authorization will change once we open the feature to end users - @AuthorizationConfiguration.RequireGlobalAdminUser + @AuthorizationConfiguration.RequirePermissionCreatePatientAtFacility public PatientBulkUploadResponse processPersonCSV(InputStream csvStream, UUID facilityId) throws IllegalArgumentException { PatientBulkUploadResponse result = new PatientBulkUploadResponse(); - Organization currentOrganization = _organizationService.getCurrentOrganization(); - - // Patients do not need to be assigned to a facility, but if an id is given it must be valid - Optional assignedFacility = - Optional.ofNullable(facilityId).map(_organizationService::getFacilityInCurrentOrg); - - Set patientsList = new HashSet<>(); - List phoneNumbersList = new ArrayList<>(); - byte[] content; try { @@ -80,7 +44,7 @@ public PatientBulkUploadResponse processPersonCSV(InputStream csvStream, UUID fa } List errors = - _patientBulkUploadFileValidator.validate(new ByteArrayInputStream(content)); + patientUploadRowFileValidator.validate(new ByteArrayInputStream(content)); if (!errors.isEmpty()) { result.setStatus(UploadStatus.FAILURE); @@ -88,102 +52,8 @@ public PatientBulkUploadResponse processPersonCSV(InputStream csvStream, UUID fa return result; } - // This is the point where we need to figure out multithreading - // because what needs to happen is that we return a success message to the end user - // but continue to process the csv (create person records) in the background. - // Putting a pin in it for now. - - final MappingIterator> valueIterator = - CsvValidatorUtils.getIteratorForCsv(new ByteArrayInputStream(content)); - - while (valueIterator.hasNext()) { - final Map row = CsvValidatorUtils.getNextRow(valueIterator); - - try { - - PatientUploadRow extractedData = new PatientUploadRow(row); - - // Fetch address information - StreetAddress address = - _addressValidationService.getValidatedAddress( - extractedData.getStreet().getValue(), - extractedData.getStreet2().getValue(), - extractedData.getCity().getValue(), - extractedData.getState().getValue(), - extractedData.getZipCode().getValue(), - null); - - String country = - extractedData.getCountry().getValue() == null - ? "USA" - : extractedData.getCountry().getValue(); - - if (_personService.isDuplicatePatient( - extractedData.getFirstName().getValue(), - extractedData.getLastName().getValue(), - parseUserShortDate(extractedData.getDateOfBirth().getValue()), - currentOrganization, - assignedFacility)) { - continue; - } - - // create new person with current organization, then add to new patients list - Person newPatient = - new Person( - currentOrganization, - assignedFacility.orElse(null), - null, // lookupid - extractedData.getFirstName().getValue(), - extractedData.getMiddleName().getValue(), - extractedData.getLastName().getValue(), - extractedData.getSuffix().getValue(), - parseUserShortDate(extractedData.getDateOfBirth().getValue()), - address, - country, - parsePersonRole(extractedData.getRole().getValue(), false), - List.of(extractedData.getEmail().getValue()), - convertRaceToDatabaseValue(extractedData.getRace().getValue()), - convertEthnicityToDatabaseValue(extractedData.getEthnicity().getValue()), - null, // tribalAffiliation - convertSexToDatabaseValue(extractedData.getBiologicalSex().getValue()), - parseYesNo(extractedData.getResidentCongregateSetting().getValue()), - parseYesNo(extractedData.getEmployedInHealthcare().getValue()), - null, // preferredLanguage - null // testResultDeliveryPreference - ); - - if (!patientsList.contains(newPatient)) { - // collect phone numbers and associate them with the patient - // then add to phone numbers list and set primary phone, if exists - List newPhoneNumbers = - _personService.assignPhoneNumbersToPatient( - newPatient, - List.of( - new PhoneNumber( - parsePhoneType(extractedData.getPhoneNumberType().getValue()), - extractedData.getPhoneNumber().getValue()))); - phoneNumbersList.addAll(newPhoneNumbers); - newPhoneNumbers.stream().findFirst().ifPresent(newPatient::setPrimaryPhone); - - patientsList.add(newPatient); - } - } catch (IllegalArgumentException e) { - String errorMessage = "Error uploading patient roster"; - log.error( - errorMessage - + " for organization " - + currentOrganization.getExternalId() - + " and facility " - + facilityId); - throw new IllegalArgumentException(errorMessage); - } - } - - _personService.addPatientsAndPhoneNumbers(patientsList, phoneNumbersList); - - log.info("CSV patient upload completed for {}", currentOrganization.getOrganizationName()); + patientBulkUploadServiceAsync.savePatients(content, facilityId); result.setStatus(UploadStatus.SUCCESS); - // eventually want to send an email here instead of return success return result; } } diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceAsync.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceAsync.java new file mode 100644 index 0000000000..6004863f0e --- /dev/null +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceAsync.java @@ -0,0 +1,164 @@ +package gov.cdc.usds.simplereport.service; + +import static gov.cdc.usds.simplereport.api.Translators.parsePersonRole; +import static gov.cdc.usds.simplereport.api.Translators.parsePhoneType; +import static gov.cdc.usds.simplereport.api.Translators.parseUserShortDate; +import static gov.cdc.usds.simplereport.api.Translators.parseYesNo; +import static gov.cdc.usds.simplereport.validators.CsvValidatorUtils.convertEthnicityToDatabaseValue; +import static gov.cdc.usds.simplereport.validators.CsvValidatorUtils.convertRaceToDatabaseValue; +import static gov.cdc.usds.simplereport.validators.CsvValidatorUtils.convertSexToDatabaseValue; + +import com.fasterxml.jackson.databind.MappingIterator; +import gov.cdc.usds.simplereport.api.model.filerow.PatientUploadRow; +import gov.cdc.usds.simplereport.config.AuthorizationConfiguration; +import gov.cdc.usds.simplereport.db.model.Facility; +import gov.cdc.usds.simplereport.db.model.Organization; +import gov.cdc.usds.simplereport.db.model.Person; +import gov.cdc.usds.simplereport.db.model.PhoneNumber; +import gov.cdc.usds.simplereport.db.model.auxiliary.StreetAddress; +import gov.cdc.usds.simplereport.validators.CsvValidatorUtils; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PatientBulkUploadServiceAsync { + + private final PersonService personService; + private final AddressValidationService addressValidationService; + private final OrganizationService organizationService; + + @Value("${simple-report.batch-size:1000}") + private int batchSize; + + @Async + @Transactional + @AuthorizationConfiguration.RequirePermissionCreatePatientAtFacility + public CompletableFuture> savePatients(byte[] content, UUID facilityId) { + Organization currentOrganization = organizationService.getCurrentOrganization(); + + // Patients do not need to be assigned to a facility, but if an id is given it must be valid + Optional assignedFacility = + Optional.ofNullable(facilityId).map(organizationService::getFacilityInCurrentOrg); + + Set patientsList = new HashSet<>(); + List phoneNumbersList = new ArrayList<>(); + + Set allPatients = new HashSet<>(); + + final MappingIterator> valueIterator = + CsvValidatorUtils.getIteratorForCsv(new ByteArrayInputStream(content)); + + while (valueIterator.hasNext()) { + final Map row = CsvValidatorUtils.getNextRow(valueIterator); + + try { + PatientUploadRow extractedData = new PatientUploadRow(row); + + // Fetch address information + StreetAddress address = + addressValidationService.getValidatedAddress( + extractedData.getStreet().getValue(), + extractedData.getStreet2().getValue(), + extractedData.getCity().getValue(), + extractedData.getState().getValue(), + extractedData.getZipCode().getValue(), + null); + + String country = + extractedData.getCountry().getValue() == null + ? "USA" + : extractedData.getCountry().getValue(); + + if (personService.isDuplicatePatient( + extractedData.getFirstName().getValue(), + extractedData.getLastName().getValue(), + parseUserShortDate(extractedData.getDateOfBirth().getValue()), + currentOrganization, + assignedFacility)) { + continue; + } + + // create new person with current organization, then add to new patients list + Person newPatient = + new Person( + currentOrganization, + assignedFacility.orElse(null), + null, // lookupid + extractedData.getFirstName().getValue(), + extractedData.getMiddleName().getValue(), + extractedData.getLastName().getValue(), + extractedData.getSuffix().getValue(), + parseUserShortDate(extractedData.getDateOfBirth().getValue()), + address, + country, + parsePersonRole(extractedData.getRole().getValue(), false), + List.of(extractedData.getEmail().getValue()), + convertRaceToDatabaseValue(extractedData.getRace().getValue()), + convertEthnicityToDatabaseValue(extractedData.getEthnicity().getValue()), + null, // tribalAffiliation + convertSexToDatabaseValue(extractedData.getBiologicalSex().getValue()), + parseYesNo(extractedData.getResidentCongregateSetting().getValue()), + parseYesNo(extractedData.getEmployedInHealthcare().getValue()), + null, // preferredLanguage + null // testResultDeliveryPreference + ); + + if (!allPatients.contains(newPatient)) { + // collect phone numbers and associate them with the patient + // then add to phone numbers list and set primary phone, if exists + List newPhoneNumbers = + personService.assignPhoneNumbersToPatient( + newPatient, + List.of( + new PhoneNumber( + parsePhoneType(extractedData.getPhoneNumberType().getValue()), + extractedData.getPhoneNumber().getValue()))); + phoneNumbersList.addAll(newPhoneNumbers); + newPhoneNumbers.stream().findFirst().ifPresent(newPatient::setPrimaryPhone); + + patientsList.add(newPatient); + allPatients.add(newPatient); + } + + if (patientsList.size() >= batchSize) { + personService.addPatientsAndPhoneNumbers(patientsList, phoneNumbersList); + // clear lists after save, so we don't try to save duplicate records + patientsList.clear(); + phoneNumbersList.clear(); + } + + } catch (IllegalArgumentException e) { + String errorMessage = "Error uploading patient roster"; + log.error( + errorMessage + + " for organization " + + currentOrganization.getExternalId() + + " and facility " + + facilityId); + throw new IllegalArgumentException(errorMessage); + } + } + + personService.addPatientsAndPhoneNumbers(patientsList, phoneNumbersList); + + log.info("CSV patient upload completed for {}", currentOrganization.getOrganizationName()); + // eventually want to send an email here instead of return success + + return CompletableFuture.completedFuture(patientsList); + } +} diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 8490054214..7c0b2c1f5a 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -125,6 +125,7 @@ simple-report: - GET - HEAD - POST + batch-size: 1000 twilio: messaging-service-sid: ${TWILIO_MESSAGING_SID} logging: diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/api/uploads/FileUploadControllerTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/api/FileUploadControllerTest.java similarity index 98% rename from backend/src/test/java/gov/cdc/usds/simplereport/api/uploads/FileUploadControllerTest.java rename to backend/src/test/java/gov/cdc/usds/simplereport/api/FileUploadControllerTest.java index 04e842d833..43e4ce2517 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/api/uploads/FileUploadControllerTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/api/FileUploadControllerTest.java @@ -1,4 +1,4 @@ -package gov.cdc.usds.simplereport.api.uploads; +package gov.cdc.usds.simplereport.api; import static gov.cdc.usds.simplereport.api.uploads.FileUploadController.TEXT_CSV_CONTENT_TYPE; import static gov.cdc.usds.simplereport.config.WebConfiguration.PATIENT_UPLOAD; @@ -13,9 +13,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import gov.cdc.usds.simplereport.api.BaseFullStackTest; import gov.cdc.usds.simplereport.api.model.errors.BadRequestException; import gov.cdc.usds.simplereport.api.model.errors.CsvProcessingException; +import gov.cdc.usds.simplereport.api.uploads.PatientBulkUploadResponse; import gov.cdc.usds.simplereport.db.model.Organization; import gov.cdc.usds.simplereport.db.model.TestResultUpload; import gov.cdc.usds.simplereport.db.model.auxiliary.UploadStatus; diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/api/graphql/BaseGraphqlTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/api/graphql/BaseGraphqlTest.java index 42347cede8..2883f8ba48 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/api/graphql/BaseGraphqlTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/api/graphql/BaseGraphqlTest.java @@ -195,7 +195,6 @@ protected ObjectNode runQuery( .headers(httpHeaders -> httpHeaders.addAll(_customHeaders)) .build(); - System.out.println(queryFileName); GraphQlTester.Request request = webGraphQlTester.documentName(queryFileName); if (operationName != null) { diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceAsyncTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceAsyncTest.java new file mode 100644 index 0000000000..6d6ed16704 --- /dev/null +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceAsyncTest.java @@ -0,0 +1,245 @@ +package gov.cdc.usds.simplereport.service; + +import static gov.cdc.usds.simplereport.api.Translators.parsePhoneType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import gov.cdc.usds.simplereport.api.graphql.BaseGraphqlTest; +import gov.cdc.usds.simplereport.db.model.Facility; +import gov.cdc.usds.simplereport.db.model.Person; +import gov.cdc.usds.simplereport.db.model.PhoneNumber; +import gov.cdc.usds.simplereport.db.model.auxiliary.PersonRole; +import gov.cdc.usds.simplereport.db.model.auxiliary.PhoneType; +import gov.cdc.usds.simplereport.db.repository.PhoneNumberRepository; +import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.context.TestPropertySource; + +/* + * We can't use the standard BaseServiceTest here because this service is async and requires a request context to operate. + * BaseFullStackTest doesn't have the authorization setup required for an authenticated test, but BaseGraphqlTest does. + */ +@TestPropertySource( + properties = { + "hibernate.query.interceptor.error-level=ERROR", + "spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true" + }) +@SliceTestConfiguration.WithSimpleReportStandardAllFacilitiesUser +class PatientBulkUploadServiceAsyncTest extends BaseGraphqlTest { + + @Autowired PatientBulkUploadServiceAsync _service; + + @SpyBean PersonService _personService; + @Autowired PhoneNumberRepository phoneNumberRepository; + + public static final int PATIENT_PAGE_OFFSET = 0; + public static final int PATIENT_PAGE_SIZE = 1000; + + private UUID firstFacilityId; + private UUID secondFacilityId; + + @BeforeEach + void setupData() { + List facilityIds = + _orgService.getFacilities(_orgService.getCurrentOrganization()).stream() + .map(Facility::getInternalId) + .collect(Collectors.toList()); + if (facilityIds.isEmpty()) { + throw new IllegalStateException("This organization has no facilities"); + } + firstFacilityId = facilityIds.get(0); + secondFacilityId = facilityIds.get(1); + } + + @Test + void validPerson_savedToDatabase() throws IOException, ExecutionException, InterruptedException { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); + byte[] content = inputStream.readAllBytes(); + + // WHEN + CompletableFuture> futureSavedPatients = this._service.savePatients(content, null); + Set savedPatients = futureSavedPatients.get(); + + // THEN + assertThat(savedPatients).hasSameSizeAs(fetchDatabasePatients()); + assertThat(fetchDatabasePatientsForFacility(firstFacilityId)) + .hasSameSizeAs(fetchDatabasePatientsForFacility(secondFacilityId)); + assertThat(fetchDatabasePatients()).hasSize(1); + + Person patient = fetchDatabasePatients().get(0); + + assertThat(patient.getLastName()).isEqualTo("Doe"); + assertThat(patient.getRace()).isEqualTo("black"); + assertThat(patient.getEthnicity()).isEqualTo("not_hispanic"); + assertThat(patient.getBirthDate()).isEqualTo(LocalDate.of(1980, 11, 3)); + assertThat(patient.getGender()).isEqualTo("female"); + assertThat(patient.getRole()).isEqualTo(PersonRole.STAFF); + + assertThat(patient.getCountry()).isEqualTo("USA"); + + List phoneNumbers = + phoneNumberRepository.findAllByPersonInternalId(patient.getInternalId()); + assertThat(phoneNumbers).hasSize(1); + PhoneNumber phoneNumber = phoneNumbers.get(0); + assertThat(phoneNumber.getNumber()).isEqualTo("410-867-5309"); + assertThat(phoneNumber.getType()).isEqualTo(PhoneType.MOBILE); + assertThat(patient.getEmail()).isEqualTo("jane@testingorg.com"); + } + + @Test + void noPhoneNumberTypes_savesPatient() + throws IOException, ExecutionException, InterruptedException { + // WHEN + InputStream inputStream = loadCsv("patientBulkUpload/noPhoneNumberTypes.csv"); + byte[] content = inputStream.readAllBytes(); + + CompletableFuture> futurePatients = this._service.savePatients(content, null); + futurePatients.get(); + + // THEN + assertThat(fetchDatabasePatients()).hasSize(1); + } + + @Test + void duplicatePatient_isNotSaved() throws IOException, ExecutionException, InterruptedException { + // GIVEN + _personService.addPatient( + firstFacilityId, + "", + "Jane", + "", + "Doe", + "", + LocalDate.of(1980, 11, 3), + _dataFactory.getAddress(), + "USA", + List.of(new PhoneNumber(parsePhoneType("mobile"), "410-867-5309")), + PersonRole.STAFF, + List.of("jane@testingorg.com"), + "black", + "not_hispanic", + null, + "female", + false, + false, + "", + null); + assertThat(fetchDatabasePatients()).hasSize(1); + + // WHEN + InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); + byte[] content = inputStream.readAllBytes(); + CompletableFuture> futurePatients = this._service.savePatients(content, null); + futurePatients.get(); + + // THEN + assertThat(fetchDatabasePatients()).hasSize(1); + } + + @Test + void duplicatePatientInCsv_isNotAddedToBatch() + throws IOException, ExecutionException, InterruptedException { + // WHEN + InputStream inputStream = loadCsv("patientBulkUpload/duplicatePatients.csv"); + byte[] content = inputStream.readAllBytes(); + CompletableFuture> futurePatients = this._service.savePatients(content, null); + futurePatients.get(); + + // THEN + assertThat(fetchDatabasePatients()).hasSize(1); + } + + @Test + void patientSavedToSingleFacility_successful() + throws IOException, ExecutionException, InterruptedException { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); + byte[] content = inputStream.readAllBytes(); + + // WHEN + CompletableFuture> futurePatients = + this._service.savePatients(content, firstFacilityId); + futurePatients.get(); + + // THEN + assertThat(fetchDatabasePatientsForFacility(firstFacilityId)).hasSize(1); + assertThat(fetchDatabasePatientsForFacility(secondFacilityId)).isEmpty(); + } + + @Test + void largeFile_isBatched() throws IOException, ExecutionException, InterruptedException { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/slightlyLargeFile.csv"); + byte[] content = inputStream.readAllBytes(); + + // WHEN + CompletableFuture> futurePatients = + this._service.savePatients(content, firstFacilityId); + futurePatients.get(); + + // THEN + verify(_personService, times(2)).addPatientsAndPhoneNumbers(any(), any()); + assertThat(fetchDatabasePatients()).hasSize(17); + } + + @Test + void invalidData_throwsException() throws IOException, ExecutionException, InterruptedException { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/missingRequiredFields.csv"); + byte[] content = inputStream.readAllBytes(); + + // WHEN + CompletableFuture> futurePatients = + this._service.savePatients(content, firstFacilityId); + + // THEN + assertThrows(ExecutionException.class, futurePatients::get); + assertThat(fetchDatabasePatients()).isEmpty(); + } + + @Test + @SliceTestConfiguration.WithSimpleReportStandardUser + void requiresPermissionAddPatientsToFacility() + throws IOException, ExecutionException, InterruptedException { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); + byte[] content = inputStream.readAllBytes(); + + // WHEN + CompletableFuture> futurePatients = + this._service.savePatients(content, secondFacilityId); + ExecutionException caught = assertThrows(ExecutionException.class, futurePatients::get); + assertThat(caught.getCause().getClass()).isEqualTo(AccessDeniedException.class); + } + + private InputStream loadCsv(String csvFile) { + return PatientBulkUploadServiceAsyncTest.class.getClassLoader().getResourceAsStream(csvFile); + } + + private List fetchDatabasePatients() { + return this._personService.getPatients( + null, PATIENT_PAGE_OFFSET, PATIENT_PAGE_SIZE, false, null, false); + } + + private List fetchDatabasePatientsForFacility(UUID facilityId) { + return this._personService.getPatients( + facilityId, PATIENT_PAGE_OFFSET, PATIENT_PAGE_SIZE, false, null, false); + } +} diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceIntegrationTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceIntegrationTest.java deleted file mode 100644 index a8830740c7..0000000000 --- a/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceIntegrationTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package gov.cdc.usds.simplereport.service; - -import static gov.cdc.usds.simplereport.api.Translators.parsePhoneType; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import gov.cdc.usds.simplereport.api.uploads.PatientBulkUploadResponse; -import gov.cdc.usds.simplereport.db.model.Facility; -import gov.cdc.usds.simplereport.db.model.Person; -import gov.cdc.usds.simplereport.db.model.PhoneNumber; -import gov.cdc.usds.simplereport.db.model.auxiliary.PersonRole; -import gov.cdc.usds.simplereport.db.model.auxiliary.PhoneType; -import gov.cdc.usds.simplereport.db.model.auxiliary.StreetAddress; -import gov.cdc.usds.simplereport.db.model.auxiliary.UploadStatus; -import gov.cdc.usds.simplereport.db.repository.PhoneNumberRepository; -import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration.Role; -import gov.cdc.usds.simplereport.test_util.TestUserIdentities; -import java.io.InputStream; -import java.time.LocalDate; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.TestPropertySource; - -@TestPropertySource(properties = "hibernate.query.interceptor.error-level=ERROR") -@WithMockUser( - username = TestUserIdentities.SITE_ADMIN_USER, - authorities = {Role.SITE_ADMIN, Role.DEFAULT_ORG_ADMIN}) -class PatientBulkUploadServiceIntegrationTest extends BaseServiceTest { - public static final int PATIENT_PAGE_OFFSET = 0; - public static final int PATIENT_PAGE_SIZE = 1000; - - @Autowired private PersonService personService; - @Autowired private PhoneNumberRepository phoneNumberRepository; - @Autowired private OrganizationService organizationService; - - @MockBean protected AddressValidationService addressValidationService; - private StreetAddress address; - private UUID firstFacilityId; - private UUID secondFacilityId; - - @BeforeEach - void setupData() { - address = new StreetAddress("123 Main Street", null, "Washington", "DC", "20008", null); - initSampleData(); - when(addressValidationService.getValidatedAddress(any(), any(), any(), any(), any(), any())) - .thenReturn(address); - List facilityIds = - organizationService.getFacilities(organizationService.getCurrentOrganization()).stream() - .map(Facility::getInternalId) - .collect(Collectors.toList()); - if (facilityIds.isEmpty()) { - throw new IllegalStateException("This organization has no facilities"); - } - firstFacilityId = facilityIds.get(0); - secondFacilityId = facilityIds.get(1); - } - - @Test - void validCsv_savesPatientToOrganization() { - // GIVEN - InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); - - // WHEN - PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); - - assertThat(getPatients()).hasSize(1); - Person patient = getPatients().get(0); - - assertThat(patient.getLastName()).isEqualTo("Doe"); - assertThat(patient.getRace()).isEqualTo("black"); - assertThat(patient.getEthnicity()).isEqualTo("not_hispanic"); - assertThat(patient.getBirthDate()).isEqualTo(LocalDate.of(1980, 11, 3)); - assertThat(patient.getGender()).isEqualTo("female"); - assertThat(patient.getRole()).isEqualTo(PersonRole.STAFF); - - assertThat(patient.getAddress()).isEqualTo(address); - assertThat(patient.getCountry()).isEqualTo("USA"); - - List phoneNumbers = - phoneNumberRepository.findAllByPersonInternalId(patient.getInternalId()); - assertThat(phoneNumbers).hasSize(1); - PhoneNumber pn = phoneNumbers.get(0); - assertThat(pn.getNumber()).isEqualTo("410-867-5309"); - assertThat(pn.getType()).isEqualTo(PhoneType.MOBILE); - assertThat(patient.getEmail()).isEqualTo("jane@testingorg.com"); - - assertThat(getPatientsForFacility(firstFacilityId)) - .hasSameSizeAs(getPatientsForFacility(secondFacilityId)); - } - - @Test - void noPhoneNumberTypes_savesPatient() { - // WHEN - InputStream inputStream = loadCsv("patientBulkUpload/noPhoneNumberTypes.csv"); - PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); - assertThat(getPatients()).hasSize(1); - } - - @Test - void duplicatePatient_isNotSaved() { - // GIVEN - personService.addPatient( - firstFacilityId, - "", - "Jane", - "", - "Doe", - "", - LocalDate.of(1980, 11, 3), - address, - "USA", - List.of(new PhoneNumber(parsePhoneType("mobile"), "410-867-5309")), - PersonRole.STAFF, - List.of("jane@testingorg.com"), - "black", - "not_hispanic", - null, - "female", - false, - false, - "", - null); - assertThat(getPatients()).hasSize(1); - - // WHEN - InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); - PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); - assertThat(getPatients()).hasSize(1); - } - - @Test - void duplicatePatient_isNotAddedToBatch() { - // WHEN - InputStream inputStream = loadCsv("patientBulkUpload/duplicatePatients.csv"); - PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); - assertThat(getPatients()).hasSize(1); - } - - @Test - void patientSavedToSingleFacility_successful() { - // GIVEN - InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); - - // WHEN - PatientBulkUploadResponse response = - this._service.processPersonCSV(inputStream, firstFacilityId); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); - - assertThat(getPatientsForFacility(firstFacilityId)).hasSize(1); - assertThat(getPatientsForFacility(secondFacilityId)).isEmpty(); - } - - @Test - void missingHeaders_returnsError() { - // GIVEN - InputStream inputStream = loadCsv("patientBulkUpload/missingHeaders.csv"); - - // WHEN - PatientBulkUploadResponse response = - this._service.processPersonCSV(inputStream, firstFacilityId); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.FAILURE); - assertThat(response.getErrors()).isNotEmpty(); - assertThat(getPatients()).isEmpty(); - } - - @Test - void invalidData_returnsError() { - // GIVEN - InputStream inputStream = loadCsv("patientBulkUpload/missingRequiredFields.csv"); - - // WHEN - PatientBulkUploadResponse response = - this._service.processPersonCSV(inputStream, firstFacilityId); - - // THEN - assertThat(response.getStatus()).isEqualTo(UploadStatus.FAILURE); - assertThat(response.getErrors()).isNotEmpty(); - assertThat(getPatients()).isEmpty(); - } - - private InputStream loadCsv(String csvFile) { - return PatientBulkUploadServiceIntegrationTest.class - .getClassLoader() - .getResourceAsStream(csvFile); - } - - private List getPatients() { - return this.personService.getPatients( - null, PATIENT_PAGE_OFFSET, PATIENT_PAGE_SIZE, false, null, false); - } - - private List getPatientsForFacility(UUID facilityId) { - return this.personService.getPatients( - facilityId, PATIENT_PAGE_OFFSET, PATIENT_PAGE_SIZE, false, null, false); - } -} diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceTest.java new file mode 100644 index 0000000000..7c476236e7 --- /dev/null +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/PatientBulkUploadServiceTest.java @@ -0,0 +1,94 @@ +package gov.cdc.usds.simplereport.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import gov.cdc.usds.simplereport.api.uploads.PatientBulkUploadResponse; +import gov.cdc.usds.simplereport.db.model.auxiliary.StreetAddress; +import gov.cdc.usds.simplereport.db.model.auxiliary.UploadStatus; +import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration; +import java.io.InputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "hibernate.query.interceptor.error-level=ERROR") +@SliceTestConfiguration.WithSimpleReportStandardAllFacilitiesUser +class PatientBulkUploadServiceTest extends BaseServiceTest { + + @MockBean private PatientBulkUploadServiceAsync mockAsyncService; + @MockBean private PersonService mockPersonService; + @MockBean protected AddressValidationService addressValidationService; + + @BeforeEach + void setupData() { + initSampleData(); + when(addressValidationService.getValidatedAddress(any(), any(), any(), any(), any(), any())) + .thenReturn(new StreetAddress("123 Main Street", null, "Washington", "DC", "20008", null)); + } + + @Test + void validCsv_returnsSuccessToUser() { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); + + // WHEN + PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); + + // THEN + assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); + verify(mockAsyncService, times(1)).savePatients(any(), any()); + } + + @Test + void exceptionDuringSave_doesNotPopulateToUser() { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/valid.csv"); + doThrow(new IllegalStateException("database unavailable")) + .when(mockPersonService) + .addPatientsAndPhoneNumbers(any(), any()); + + // WHEN + PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); + + // THEN + assertThat(response.getStatus()).isEqualTo(UploadStatus.SUCCESS); + } + + @Test + void missingHeaders_returnsError() { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/missingHeaders.csv"); + + // WHEN + PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); + + // THEN + assertThat(response.getStatus()).isEqualTo(UploadStatus.FAILURE); + assertThat(response.getErrors()).isNotEmpty(); + verify(mockAsyncService, times(0)).savePatients(any(), any()); + } + + @Test + void invalidData_returnsError() { + // GIVEN + InputStream inputStream = loadCsv("patientBulkUpload/missingRequiredFields.csv"); + + // WHEN + PatientBulkUploadResponse response = this._service.processPersonCSV(inputStream, null); + + // THEN + assertThat(response.getStatus()).isEqualTo(UploadStatus.FAILURE); + assertThat(response.getErrors()).isNotEmpty(); + verify(mockAsyncService, times(0)).savePatients(any(), any()); + } + + private InputStream loadCsv(String csvFile) { + return PatientBulkUploadServiceTest.class.getClassLoader().getResourceAsStream(csvFile); + } +} diff --git a/backend/src/test/resources/application-default.yaml b/backend/src/test/resources/application-default.yaml index ff51499162..672b84899a 100644 --- a/backend/src/test/resources/application-default.yaml +++ b/backend/src/test/resources/application-default.yaml @@ -49,6 +49,8 @@ logging: # Other possibilities: # com.okta: DEBUG # org.springframework.security: DEBUG + # com.zaxxer.hikari: TRACE + # com.zaxxer.hikari.HikariConfig: DEBUG org.springframework.web.client.RestTemplate: DEBUG simple-report: authorization: @@ -63,6 +65,7 @@ simple-report: azure-reporting-queue: exception-webhook-enabled: true exception-webhook-token: WATERMELON + batch-size: 10 demo-users: site-admin-emails: - ruby@example.com diff --git a/backend/src/test/resources/patientBulkUpload/slightlyLargeFile.csv b/backend/src/test/resources/patientBulkUpload/slightlyLargeFile.csv new file mode 100644 index 0000000000..eab35fbe3e --- /dev/null +++ b/backend/src/test/resources/patientBulkUpload/slightlyLargeFile.csv @@ -0,0 +1,18 @@ +last_name,first_name,middle_name,suffix,race,date_of_birth,biological_sex,ethnicity,street,street_2,city,county,state,zip_code,phone_number,employed_in_healthcare,resident_congregate_setting,role,email +Grady,Blake,985,,other,02/03/1988,male,hispanic or latino,10817 Gleichner Canyon,,Kaylahbury,,AK,99501,410-107-8920,No,No,Staff,Blake19@yahoo.com +D'Amore,Lambert,986,,white,01/27/1977,female,hispanic or latino,000 Marianna Junctions,,Gottliebview,,AK,99501,410-068-4203,No,No,Staff,Lambert_DAmore19@gmail.com +Luettgen,Jared,987,,other,05/09/1968,male,not hispanic or latino,46180 Runolfsson Valley,,Cronaburgh,,AK,99501,410-843-1750,No,No,Staff,Jared87@gmail.com +Champlin,Dallin,988,,asian,07/13/1973,female,hispanic or latino,9371 Hodkiewicz Bridge,,Schuppebury,,AK,99501,410-206-9516,No,No,Staff,Dallin_Champlin40@gmail.com +Grimes,Effie,989,,asian,08/06/1973,male,hispanic or latino,27169 Leone Island,,Port Ginaville,,AK,99501,410-312-6855,No,No,Staff,Effie_Grimes@hotmail.com +Heidenreich,Albin,990,,black or african american,03/21/2013,female,not hispanic or latino,07001 Schamberger Fall,,Bethesda,,AK,99501,410-384-5011,No,No,Staff,Albin56@yahoo.com +Wisozk,Hubert,991,,other,02/08/1943,male,hispanic or latino,646 Sean Points,,Stantonfurt,,AK,99501,410-122-5632,No,No,Staff,Hubert.Wisozk@yahoo.com +Weber,Dorothy,992,,other,02/13/1936,female,hispanic or latino,36184 Meda Divide,,Leuschkeside,,AK,99501,410-007-4869,No,No,Staff,Dorothy.Weber42@gmail.com +Russel,Sheldon,993,,black or african american,11/28/1989,male,not hispanic or latino,38268 Hermiston Pine,,South Wyatt,,AK,99501,410-291-5883,No,No,Staff,Sheldon58@hotmail.com +Jast,Reinhold,994,,other,05/28/1948,female,hispanic or latino,874 Gleichner Viaduct,,St. Paul,,AK,99501,410-859-0811,No,No,Staff,Reinhold.Jast56@yahoo.com +Jacobi,Hallie,995,,other,01/24/2017,male,hispanic or latino,16724 Anabel Branch,,Considinefield,,AK,99501,410-525-2095,No,No,Staff,Hallie.Jacobi@hotmail.com +Okuneva,Makayla,996,,american indian or alaska native,12/05/1955,female,not hispanic or latino,143 Lottie Fords,,West Alice,,AK,99501,410-301-0109,No,No,Staff,Makayla0@gmail.com +O'Kon,Gabriel,997,,other,12/29/1981,male,hispanic or latino,6076 Daisy Branch,,Reynoldscester,,AK,99501,410-043-1413,No,No,Staff,Gabriel.OKon59@hotmail.com +Schulist,Elisabeth,998,,asian,04/04/2018,female,hispanic or latino,9557 Howe Rue,,Janesville,,AK,99501,410-999-3782,No,No,Staff,Elisabeth46@hotmail.com +Keeling,Grady,999,,american indian or alaska native,09/08/1956,male,not hispanic or latino,34902 Lavon Knoll,,Appleton,,AK,99501,410-599-5355,No,No,Staff,Grady.Keeling52@hotmail.com +Smith,Kody,1000,,native hawaiian or other pacific islander,06/13/2017,male,hispanic or latino,669 Jerome Highway,,East Jacklyn,,AK,99501,410-172-5377,No,No,Staff,Kody81@yahoo.com +Prosacco,Cleta,1001,,white,08/20/2013,female,not hispanic or latino,04840 Zboncak Dale,,Fort Jose,,AK,99501,410-121-9375,No,No,Staff,Cleta.Prosacco@yahoo.com