From 9f5cf8871603da8d12e050ccd639c8694b9b9fb0 Mon Sep 17 00:00:00 2001 From: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:57:44 +0000 Subject: [PATCH] x1210 Support bio risk (block registration) Add bio risk entity. Support admin for bio risks. Bio risks can be linked to samples. Copy bio risks from old samples when new samples are created. Expect bio risk in block registration. Expect bio risk in block file registration. Squashed commit of the following: commit 0b76158b8277e283f46e802a98ff2f9df8a793ce Author: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Wed Oct 30 11:51:12 2024 +0000 Support bio risk in block register excel file commit 7541d6e46646d248fa50c08708cdc6c10747f82f Author: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Wed Oct 30 09:59:30 2024 +0000 Complete the unit tests for BioRiskService commit 391f2df61126bd1e7f5300562b36826aa13d4eb7 Author: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Wed Oct 30 09:41:06 2024 +0000 When an op creates new samples, copy the bio risks from the sources commit 6f2e43df4ba29ec9e19b756a0aae5d0a58ea91f2 Author: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Mon Oct 28 15:46:12 2024 +0000 More support for bio risks, and save bio risks in block registration commit 4c1f3e5ef8f620b6e103d22e0ce510184d36cbaf Author: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Thu Oct 24 13:55:39 2024 +0100 Basic support for BioRisk (biological risk numbers) --- pom.xml | 2 +- .../sanger/sccp/stan/GraphQLDataFetchers.java | 8 +- .../ac/sanger/sccp/stan/GraphQLMutation.java | 12 +- .../ac/sanger/sccp/stan/GraphQLProvider.java | 3 + .../sccp/stan/config/FieldValidation.java | 6 + .../uk/ac/sanger/sccp/stan/model/BioRisk.java | 90 +++++++++++++ .../ac/sanger/sccp/stan/repo/BioRiskRepo.java | 96 ++++++++++++++ .../register/BlockRegisterRequest.java | 17 ++- .../sccp/stan/service/BioRiskService.java | 105 +++++++++++++++ .../service/BlockProcessingServiceImp.java | 13 +- .../stan/service/PotProcessingServiceImp.java | 7 +- .../service/ReagentTransferServiceImp.java | 5 +- .../sccp/stan/service/SlotCopyServiceImp.java | 5 +- .../service/extract/ExtractServiceImp.java | 6 +- .../service/operation/AliquotServiceImp.java | 5 +- .../operation/InPlaceOpServiceImp.java | 5 +- .../confirm/ConfirmOperationServiceImp.java | 12 +- .../confirm/ConfirmSectionServiceImp.java | 12 +- .../service/register/RegisterServiceImp.java | 9 +- .../service/register/RegisterValidation.java | 2 + .../register/RegisterValidationFactory.java | 9 +- .../register/RegisterValidationImp.java | 47 ++++++- .../filereader/BlockRegisterFileReader.java | 1 + .../BlockRegisterFileReaderImp.java | 1 + .../resources/db/changelog/changelog-3.00.xml | 46 +++++++ .../db/changelog/changelog-master.xml | 1 + src/main/resources/schema.graphqls | 15 +++ .../uk/ac/sanger/sccp/stan/EntityCreator.java | 7 + .../integrationtest/TestAdminMutations.java | 8 ++ .../TestFileBlockRegister.java | 3 + .../TestFindLatestOpQuery.java | 1 + .../integrationtest/TestHistoryQuery.java | 1 + .../integrationtest/TestRegisterMutation.java | 1 + .../sccp/stan/repo/TestBioRiskRepo.java | 105 +++++++++++++++ .../sccp/stan/service/TestBioRiskService.java | 122 ++++++++++++++++++ .../service/TestBlockProcessingService.java | 7 +- .../service/TestPotProcessingService.java | 11 +- .../service/TestReagentTransferService.java | 5 +- .../stan/service/TestSlotCopyService.java | 5 +- .../service/extract/TestExtractService.java | 6 +- .../service/operation/TestAliquotService.java | 6 +- .../operation/TestInPlaceOpService.java | 6 +- .../confirm/TestConfirmOperationService.java | 10 +- .../confirm/TestConfirmSectionService.java | 8 +- .../service/register/TestRegisterService.java | 13 +- .../register/TestRegisterValidation.java | 9 +- .../TestRegisterValidationFactory.java | 6 +- .../TestBlockRegisterFileReader.java | 6 +- src/test/resources/graphql/register.graphql | 1 + src/test/resources/testdata/block_reg.xlsx | Bin 10864 -> 11079 bytes .../testdata/block_reg_existing.xlsx | Bin 10992 -> 11047 bytes src/test/resources/testdata/reg_empty.xlsx | Bin 11900 -> 12182 bytes 52 files changed, 826 insertions(+), 61 deletions(-) create mode 100644 src/main/java/uk/ac/sanger/sccp/stan/model/BioRisk.java create mode 100644 src/main/java/uk/ac/sanger/sccp/stan/repo/BioRiskRepo.java create mode 100644 src/main/java/uk/ac/sanger/sccp/stan/service/BioRiskService.java create mode 100644 src/main/resources/db/changelog/changelog-3.00.xml create mode 100644 src/test/java/uk/ac/sanger/sccp/stan/repo/TestBioRiskRepo.java create mode 100644 src/test/java/uk/ac/sanger/sccp/stan/service/TestBioRiskService.java diff --git a/pom.xml b/pom.xml index 03f86c74..f7027ddc 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ uk.ac.sanger.sccp stan - 2.49.0 + 3.0.0 stan Spatial Genomics LIMS diff --git a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLDataFetchers.java b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLDataFetchers.java index a2b98e37..af7827c0 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLDataFetchers.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLDataFetchers.java @@ -46,6 +46,7 @@ public class GraphQLDataFetchers extends BaseGraphQLResource { final FixativeRepo fixativeRepo; final SpeciesRepo speciesRepo; final HmdmcRepo hmdmcRepo; + final BioRiskRepo bioRiskRepo; final LabwareRepo labwareRepo; final ReleaseDestinationRepo releaseDestinationRepo; final ReleaseRecipientRepo releaseRecipientRepo; @@ -92,7 +93,7 @@ public GraphQLDataFetchers(ObjectMapper objectMapper, AuthenticationComponent au SessionConfig sessionConfig, VersionInfo versionInfo, TissueTypeRepo tissueTypeRepo, LabwareTypeRepo labwareTypeRepo, MediumRepo mediumRepo, FixativeRepo fixativeRepo, - SpeciesRepo speciesRepo, HmdmcRepo hmdmcRepo, LabwareRepo labwareRepo, + SpeciesRepo speciesRepo, HmdmcRepo hmdmcRepo, BioRiskRepo bioRiskRepo, LabwareRepo labwareRepo, ReleaseDestinationRepo releaseDestinationRepo, ReleaseRecipientRepo releaseRecipientRepo, DestructionReasonRepo destructionReasonRepo, ProjectRepo projectRepo, ProgramRepo programRepo, CostCodeRepo costCodeRepo, DnapStudyRepo dnapStudyRepo, @@ -121,6 +122,7 @@ public GraphQLDataFetchers(ObjectMapper objectMapper, AuthenticationComponent au this.fixativeRepo = fixativeRepo; this.speciesRepo = speciesRepo; this.hmdmcRepo = hmdmcRepo; + this.bioRiskRepo = bioRiskRepo; this.labwareRepo = labwareRepo; this.dnapStudyRepo = dnapStudyRepo; this.solutionRepo = solutionRepo; @@ -187,6 +189,10 @@ public DataFetcher> getHmdmcs() { return allOrEnabled(hmdmcRepo::findAll, hmdmcRepo::findAllByEnabled); } + public DataFetcher> getBioRisks() { + return allOrEnabled(bioRiskRepo::findAll, bioRiskRepo::findAllByEnabled); + } + public DataFetcher> getFixatives() { return allOrEnabled(fixativeRepo::findAll, fixativeRepo::findAllByEnabled); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java index fd6a73d7..ce73f99c 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLMutation.java @@ -57,6 +57,7 @@ public class GraphQLMutation extends BaseGraphQLResource { final EquipmentAdminService equipmentAdminService; final DestructionReasonAdminService destructionReasonAdminService; final HmdmcAdminService hmdmcAdminService; + final BioRiskService bioRiskService; final ReleaseDestinationAdminService releaseDestinationAdminService; final ReleaseRecipientAdminService releaseRecipientAdminService; final SpeciesAdminService speciesAdminService; @@ -114,7 +115,7 @@ public GraphQLMutation(ObjectMapper objectMapper, AuthenticationComponent authCo DestructionService destructionService, SlotCopyService slotCopyService, InPlaceOpService inPlaceOpService, CommentAdminService commentAdminService, EquipmentAdminService equipmentAdminService, DestructionReasonAdminService destructionReasonAdminService, - HmdmcAdminService hmdmcAdminService, ReleaseDestinationAdminService releaseDestinationAdminService, + HmdmcAdminService hmdmcAdminService, BioRiskService bioRiskService, ReleaseDestinationAdminService releaseDestinationAdminService, ReleaseRecipientAdminService releaseRecipientAdminService, SpeciesAdminService speciesAdminService, ProjectService projectService, ProgramService programService, CostCodeService costCodeService, FixativeService fixativeService, @@ -153,6 +154,7 @@ public GraphQLMutation(ObjectMapper objectMapper, AuthenticationComponent authCo this.equipmentAdminService = equipmentAdminService; this.destructionReasonAdminService = destructionReasonAdminService; this.hmdmcAdminService = hmdmcAdminService; + this.bioRiskService = bioRiskService; this.releaseDestinationAdminService = releaseDestinationAdminService; this.releaseRecipientAdminService = releaseRecipientAdminService; this.speciesAdminService = speciesAdminService; @@ -400,6 +402,14 @@ public DataFetcher setHmdmcEnabled() { return adminSetEnabled(hmdmcAdminService::setEnabled, "SetHmdmcEnabled", "hmdmc"); } + public DataFetcher addBioRisk() { + return adminAdd(bioRiskService::addNew, "AddBioRisk", "code"); + } + + public DataFetcher setBioRiskEnabled() { + return adminSetEnabled(bioRiskService::setEnabled, "SetBioRiskEnabled", "code"); + } + public DataFetcher addReleaseDestination() { return adminAdd(releaseDestinationAdminService::addNew, "AddReleaseDestination", "name"); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java index d0153d3d..7a432c17 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/GraphQLProvider.java @@ -70,6 +70,7 @@ private RuntimeWiring buildWiring() { .dataFetcher("user", graphQLDataFetchers.getUser()) .dataFetcher("tissueTypes", graphQLDataFetchers.getTissueTypes()) .dataFetcher("hmdmcs", graphQLDataFetchers.getHmdmcs()) + .dataFetcher("bioRisks", graphQLDataFetchers.getBioRisks()) .dataFetcher("labwareTypes", graphQLDataFetchers.getLabwareTypes()) .dataFetcher("mediums", graphQLDataFetchers.getMediums()) .dataFetcher("fixatives", graphQLDataFetchers.getFixatives()) @@ -164,6 +165,8 @@ private RuntimeWiring buildWiring() { .dataFetcher("renameEquipment", transact(graphQLMutation.renameEquipment())) .dataFetcher("addHmdmc", graphQLMutation.addHmdmc()) // internal transaction .dataFetcher("setHmdmcEnabled", transact(graphQLMutation.setHmdmcEnabled())) + .dataFetcher("addBioRisk", graphQLMutation.addBioRisk()) // internal transaction + .dataFetcher("setBioRiskEnabled", transact(graphQLMutation.setBioRiskEnabled())) .dataFetcher("addDestructionReason", graphQLMutation.addDestructionReason()) // internal transaction .dataFetcher("setDestructionReasonEnabled", transact(graphQLMutation.setDestructionReasonEnabled())) .dataFetcher("addReleaseDestination", graphQLMutation.addReleaseDestination()) // internal transaction diff --git a/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java b/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java index 2a2f0177..19c3bc3c 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java @@ -360,6 +360,12 @@ public Validator roiValidator() { return new StringValidator("ROI", 1, 64, charTypes); } + @Bean + public Validator bioRiskCodeValidator() { + Set charTypes = EnumSet.of(CharacterType.ALPHA, CharacterType.DIGIT, CharacterType.UNDERSCORE); + return new StringValidator("Bio risk code", 2, 20, charTypes); + } + @Bean public Clock clock() { return Clock.systemUTC(); diff --git a/src/main/java/uk/ac/sanger/sccp/stan/model/BioRisk.java b/src/main/java/uk/ac/sanger/sccp/stan/model/BioRisk.java new file mode 100644 index 00000000..ac67a28b --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/model/BioRisk.java @@ -0,0 +1,90 @@ +package uk.ac.sanger.sccp.stan.model; + +import javax.persistence.*; +import java.util.Objects; + +import static uk.ac.sanger.sccp.utils.BasicUtils.describe; + +/** + * Biological risk assessment number. + * @author dr6 + */ +@Entity +public class BioRisk implements HasIntId, HasEnabled { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private String code; + private boolean enabled = true; + + public BioRisk() {} // required no-arg constructor + + public BioRisk(Integer id, String code, boolean enabled) { + this.id = id; + this.code = code; + this.enabled = enabled; + } + + public BioRisk(Integer id, String code) { + this(id, code, true); + } + + public BioRisk(String code) { + this(null, code, true); + } + + /** Primary key */ + @Override + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + /** The alphanumeric code representing this risk assessment. */ + public String getCode() { + return this.code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String toString() { + return describe(this) + .add("id", id) + .add("code", code) + .add("enabled", enabled) + .reprStringValues() + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || o.getClass() != this.getClass()) return false; + BioRisk that = (BioRisk) o; + return (this.enabled == that.enabled + && Objects.equals(this.id, that.id) + && Objects.equals(this.code, that.code) + ); + } + + @Override + public int hashCode() { + return (id != null ? id.hashCode() : code !=null ? code.hashCode() : 0); + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/sanger/sccp/stan/repo/BioRiskRepo.java b/src/main/java/uk/ac/sanger/sccp/stan/repo/BioRiskRepo.java new file mode 100644 index 00000000..5b5310fe --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/repo/BioRiskRepo.java @@ -0,0 +1,96 @@ +package uk.ac.sanger.sccp.stan.repo; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import uk.ac.sanger.sccp.stan.model.BioRisk; +import uk.ac.sanger.sccp.stan.model.Sample; + +import javax.persistence.EntityNotFoundException; +import java.util.*; + +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static uk.ac.sanger.sccp.utils.BasicUtils.*; + +/** Repo for {@link BioRisk} */ +public interface BioRiskRepo extends CrudRepository { + /** Finds the bio risk with the given code, if it exists. */ + Optional findByCode(String code); + + /** + * Gets the bio risk with the given code. + * @param code the code of the bio risk to get + * @return the bio risk with the given code + * @exception EntityNotFoundException if no such bio risk exists + */ + default BioRisk getByCode(String code) throws EntityNotFoundException { + return findByCode(code).orElseThrow(() -> new EntityNotFoundException("Unknown bio risk code: "+repr(code))); + } + + List findAllByCodeIn(Collection codes); + + /** Finds all bio risks matching the given value for enabled. */ + List findAllByEnabled(boolean enabled); + + @Query(value = "select bio_risk_id from sample_bio_risk where sample_id=?", nativeQuery = true) + Integer loadBioRiskIdForSampleId(int sampleId); + + @Query(value = "select sample_id, bio_risk_id from sample_bio_risk where sample_id in (?1)", nativeQuery = true) + int[][] _loadBioRiskIdsForSampleIds(Collection sampleIds); + + /** Loads the bio risk (if any) linked to the specified sample */ + default Optional loadBioRiskForSampleId(int sampleId) { + Integer bioRiskId = loadBioRiskIdForSampleId(sampleId); + return bioRiskId == null ? Optional.empty() : findById(bioRiskId); + } + + /** + * Loads the bio risk ids linked to the given sample ids + * @param sampleIds sample ids + * @return a map of sample id to bio risk id, omitting missing values + */ + default Map loadBioRiskIdsForSampleIds(Collection sampleIds) { + int[][] sambr = _loadBioRiskIdsForSampleIds(sampleIds); + if (sambr == null || sambr.length==0) { + return Map.of(); + } + return Arrays.stream(sambr) + .collect(toMap(arr -> arr[0], arr -> arr[1])); + } + + /** + * Loads the bio risks linked to the given sample ids + * @param sampleIds sample ids + * @return a map of sample id to bio risk, omitting missing values + */ + default Map loadBioRisksForSampleIds(Collection sampleIds) { + int[][] sambr = _loadBioRiskIdsForSampleIds(sampleIds); + if (sambr==null || sambr.length==0) { + return Map.of(); + } + Set bioRiskIds = Arrays.stream(sambr) + .map(arr -> arr[1]) + .collect(toSet()); + Map idBioRisks = stream(findAllById(bioRiskIds)) + .collect(inMap(BioRisk::getId)); + return Arrays.stream(sambr) + .collect(toMap(arr -> arr[0], arr -> idBioRisks.get(arr[1]))); + } + + /** + * Records the given bio risk id against the given sample id and operation id + * @param sampleId sample id + * @param bioRiskId bio risk id + * @param opId operation id + */ + @Modifying + @Query(value = "insert INTO sample_bio_risk (sample_id, bio_risk_id, operation_id) " + + "values (?, ?, ?)", nativeQuery = true) + void recordBioRisk(int sampleId, int bioRiskId, Integer opId); + + /** Links the given bio risk to the given sample and operation */ + default void recordBioRisk(Sample sample, BioRisk bioRisk, int opId) { + recordBioRisk(sample.getId(), bioRisk.getId(), opId); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java index b2db5ac2..d7e7c28e 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/register/BlockRegisterRequest.java @@ -1,11 +1,12 @@ package uk.ac.sanger.sccp.stan.request.register; -import com.google.common.base.MoreObjects; import uk.ac.sanger.sccp.stan.model.LifeStage; import java.time.LocalDate; import java.util.Objects; +import static uk.ac.sanger.sccp.utils.BasicUtils.describe; + /** * The information required to register a block. * @author dr6 @@ -25,6 +26,7 @@ public class BlockRegisterRequest { private String species; private boolean existingTissue; private LocalDate sampleCollectionDate; + private String bioRiskCode; public String getDonorIdentifier() { return this.donorIdentifier; @@ -138,6 +140,14 @@ public void setSampleCollectionDate(LocalDate sampleCollectionDate) { this.sampleCollectionDate = sampleCollectionDate; } + public String getBioRiskCode() { + return this.bioRiskCode; + } + + public void setBioRiskCode(String bioRiskCode) { + this.bioRiskCode = bioRiskCode; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -157,6 +167,7 @@ public boolean equals(Object o) { && Objects.equals(this.fixative, that.fixative) && Objects.equals(this.species, that.species) && Objects.equals(this.sampleCollectionDate, that.sampleCollectionDate) + && Objects.equals(this.bioRiskCode, that.bioRiskCode) ); } @@ -167,7 +178,7 @@ public int hashCode() { @Override public String toString() { - return MoreObjects.toStringHelper(this) + return describe(this) .add("donorIdentifier", donorIdentifier) .add("lifeStage", lifeStage) .add("hmdmc", hmdmc) @@ -182,6 +193,8 @@ public String toString() { .add("species", species) .add("existingTissue", existingTissue) .add("sampleCollectionDate", sampleCollectionDate) + .add("bioRiskCode", bioRiskCode) + .reprStringValues() .toString(); } } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/BioRiskService.java b/src/main/java/uk/ac/sanger/sccp/stan/service/BioRiskService.java new file mode 100644 index 00000000..756a98f0 --- /dev/null +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/BioRiskService.java @@ -0,0 +1,105 @@ +package uk.ac.sanger.sccp.stan.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import uk.ac.sanger.sccp.stan.Transactor; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.BioRiskRepo; +import uk.ac.sanger.sccp.utils.UCMap; + +import java.util.*; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toMap; + +/** + * Service for dealing with {@link BioRisk} + * @author dr6 + */ +@Service +public class BioRiskService extends BaseAdminService { + @Autowired + public BioRiskService(BioRiskRepo repo, + @Qualifier("bioRiskCodeValidator") Validator bioRiskCodeValidator, + Transactor transactor, AdminNotifyService notifyService) { + super(repo, "BioRisk", "Code", bioRiskCodeValidator, transactor, notifyService); + } + + @Override + protected BioRisk newEntity(String code) { + return new BioRisk(code); + } + + @Override + protected Optional findEntity(BioRiskRepo repo, String string) { + return repo.findByCode(string); + } + + /** + * Loads bio risks with the given codes. + * Unrecognised codes are omitted + * @param codes codes to load + * @return map of code to bio risk + */ + public UCMap loadBioRiskMap(Collection codes) { + if (codes.isEmpty()) { + return new UCMap<>(0); + } + List bioRisks = repo.findAllByCodeIn(codes); + return UCMap.from(bioRisks, BioRisk::getCode); + } + + /** + * Records the bio risks against samples + * @param sampleIdBioRisks map of sample id to bio risks + * @param opId the operation id to link + */ + public void recordSampleBioRisks(Map sampleIdBioRisks, final Integer opId) { + sampleIdBioRisks.forEach((sampleId, bioRisk) -> repo.recordBioRisk(sampleId, bioRisk.getId(), opId)); + } + + /** + * Copies bio risks from source samples to destination samples + * @param sampleDerivations map of destination to source sampleIds + */ + public void copySampleBioRisks(Map sampleDerivations) { + if (!sampleDerivations.isEmpty()) { + Map sourceBrIds = repo.loadBioRiskIdsForSampleIds(sampleDerivations.values()); + if (!sourceBrIds.isEmpty()) { + Map destBrIds = sampleDerivations.entrySet().stream() + .filter(e -> sourceBrIds.get(e.getValue())!=null) + .collect(toMap(Map.Entry::getKey, e -> sourceBrIds.get(e.getValue()))); + if (!destBrIds.isEmpty()) { + destBrIds.forEach((sampleId, brId) -> repo.recordBioRisk(sampleId, brId, null)); + } + } + } + } + + /** + * Copies bio risks from source samples to destination samples + * @param actions a stream of actions, linking source to destination samples + */ + public void copyActionSampleBioRisks(Stream actions) { + Map sampleDerivations = actions.filter(a -> !a.getSample().getId().equals(a.getSourceSample().getId())) + .collect(toMap(a -> a.getSample().getId(), a -> a.getSourceSample().getId(), (v1, v2) -> v1)); + copySampleBioRisks(sampleDerivations); + } + + /** + * Copies bio risks from source samples to destination samples + * @param op operation linking source to destination samples + */ + public void copyOpSampleBioRisks(Operation op) { + copyActionSampleBioRisks(op.getActions().stream()); + } + + /** + * Copies bio risks from source samples to destination samples + * @param ops operations linking source to destination samples + */ + public void copyOpSampleBioRisks(Collection ops) { + copyActionSampleBioRisks(ops.stream().flatMap(op -> op.getActions().stream())); + } +} diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java index 59a6745c..f3de92a8 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/BlockProcessingServiceImp.java @@ -41,6 +41,7 @@ public class BlockProcessingServiceImp implements BlockProcessingService { private final CommentValidationService commentValidationService; private final OperationService opService; private final LabwareService lwService; + private final BioRiskService bioRiskService; private final WorkService workService; private final StoreService storeService; @@ -54,7 +55,7 @@ public BlockProcessingServiceImp(LabwareValidatorFactory lwValFactory, OperationCommentRepo opCommentRepo, LabwareTypeRepo ltRepo, BioStateRepo bsRepo, TissueRepo tissueRepo, SampleRepo sampleRepo, CommentValidationService commentValidationService, OperationService opService, - LabwareService lwService, WorkService workService, StoreService storeService, Transactor transactor) { + LabwareService lwService, BioRiskService bioRiskService, WorkService workService, StoreService storeService, Transactor transactor) { this.lwValFactory = lwValFactory; this.prebarcodeValidator = prebarcodeValidator; this.replicateValidator = replicateValidator; @@ -69,6 +70,7 @@ public BlockProcessingServiceImp(LabwareValidatorFactory lwValFactory, this.commentValidationService = commentValidationService; this.opService = opService; this.lwService = lwService; + this.bioRiskService = bioRiskService; this.workService = workService; this.storeService = storeService; this.transactor = transactor; @@ -109,6 +111,7 @@ public OperationResult performInsideTransaction(User user, TissueBlockRequest re if (work!=null) { workService.link(work, ops); } + bioRiskService.copyOpSampleBioRisks(ops); discardSources(request.getDiscardSourceBarcodes(), sources); return new OperationResult(ops, destinations); @@ -194,14 +197,14 @@ public void checkPrebarcodes(Collection problems, TissueBlockRequest req if (!existing.isEmpty()) { List existingBarcodes = existing.stream() .map(Labware::getBarcode) - .collect(toList()); + .toList(); problems.add("Barcode already in use: "+existingBarcodes); } else { existing = lwRepo.findByExternalBarcodeIn(prebarcodes); if (!existing.isEmpty()) { List existingBarcodes = existing.stream() .map(Labware::getExternalBarcode) - .collect(toList()); + .toList(); problems.add("External barcode already in use: " + existingBarcodes); } } @@ -263,7 +266,7 @@ public UCMap loadEntities(Collection problems, TissueBlockRequest .filter(value -> entities.get(value)==null) .filter(distinctUCSerial()) .map(BasicUtils::repr) - .collect(toList()); + .toList(); if (!unknown.isEmpty()) { problems.add(fieldName+" unknown: "+unknown); } @@ -313,7 +316,7 @@ public void checkReplicates(Collection problems, TissueBlockRequest requ List alreadyExistRepKeys = repKeys.stream() .filter(rp -> !tissueRepo.findByDonorIdAndSpatialLocationIdAndReplicate( rp.donorId(), rp.spatialLocationId(), rp.replicate()).isEmpty() - ).collect(toList()); + ).toList(); if (!alreadyExistRepKeys.isEmpty()) { problems.add("Replicate already exists in the database: "+alreadyExistRepKeys); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/PotProcessingServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/PotProcessingServiceImp.java index 8397d95f..f28d84e1 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/PotProcessingServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/PotProcessingServiceImp.java @@ -31,6 +31,7 @@ public class PotProcessingServiceImp implements PotProcessingService { private final Transactor transactor; private final LabwareService lwService; + private final BioRiskService bioRiskService; private final OperationService opService; private final LabwareRepo lwRepo; private final BioStateRepo bsRepo; @@ -44,7 +45,7 @@ public class PotProcessingServiceImp implements PotProcessingService { public PotProcessingServiceImp(LabwareValidatorFactory lwValidatorFactory, WorkService workService, CommentValidationService commentValidationService, StoreService storeService, - Transactor transactor, LabwareService lwService, OperationService opService, + Transactor transactor, LabwareService lwService, BioRiskService bioRiskService, OperationService opService, LabwareRepo lwRepo, BioStateRepo bsRepo, FixativeRepo fixRepo, LabwareTypeRepo lwTypeRepo, TissueRepo tissueRepo, SampleRepo sampleRepo, SlotRepo slotRepo, OperationTypeRepo opTypeRepo, OperationCommentRepo opComRepo) { @@ -64,6 +65,7 @@ public PotProcessingServiceImp(LabwareValidatorFactory lwValidatorFactory, WorkS this.slotRepo = slotRepo; this.opTypeRepo = opTypeRepo; this.opComRepo = opComRepo; + this.bioRiskService = bioRiskService; } @Override @@ -189,7 +191,7 @@ private UCMap loadFromStrings(Collection problems, List unknown = strings.stream() .filter(string -> entities.get(string)==null) .map(BasicUtils::repr) - .collect(toList()); + .toList(); if (!unknown.isEmpty()) { problems.add(fieldName+" unknown: "+unknown); } @@ -254,6 +256,7 @@ public OperationResult record(User user, PotProcessingRequest request, Labware s source.setDiscarded(true); lwRepo.save(source); } + bioRiskService.copyOpSampleBioRisks(ops); if (work!=null) { workService.link(work, ops); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/ReagentTransferServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/ReagentTransferServiceImp.java index 4d605464..bd18d0d5 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/ReagentTransferServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/ReagentTransferServiceImp.java @@ -32,6 +32,7 @@ public class ReagentTransferServiceImp implements ReagentTransferService { private final ReagentTransferValidatorService rtValidatorService; private final LabwareValidatorFactory lwValFactory; + private final BioRiskService bioRiskService; private final OperationService opService; private final ReagentPlateService reagentPlateService; private final WorkService workService; @@ -41,7 +42,7 @@ public class ReagentTransferServiceImp implements ReagentTransferService { public ReagentTransferServiceImp(OperationTypeRepo opTypeRepo, ReagentActionRepo reagentActionRepo, LabwareRepo lwRepo, ReagentTransferValidatorService rtValidatorService, - LabwareValidatorFactory lwValFactory, + LabwareValidatorFactory lwValFactory, BioRiskService bioRiskService, OperationService opService, ReagentPlateService reagentPlateService, WorkService workService, BioStateReplacer bioStateReplacer) { this.opTypeRepo = opTypeRepo; @@ -49,6 +50,7 @@ public ReagentTransferServiceImp(OperationTypeRepo opTypeRepo, ReagentActionRepo this.lwRepo = lwRepo; this.rtValidatorService = rtValidatorService; this.lwValFactory = lwValFactory; + this.bioRiskService = bioRiskService; this.opService = opService; this.reagentPlateService = reagentPlateService; this.workService = workService; @@ -197,6 +199,7 @@ public Operation createOperation(User user, OperationType opType, Work work, Lab if (work!=null) { workService.link(work, List.of(op)); } + bioRiskService.copyOpSampleBioRisks(op); return op; } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/SlotCopyServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/SlotCopyServiceImp.java index e96e4a9b..3db0365c 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/SlotCopyServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/SlotCopyServiceImp.java @@ -50,6 +50,7 @@ public class SlotCopyServiceImp implements SlotCopyService { private final LabwareNoteRepo lwNoteRepo; private final SlotCopyValidationService valService; private final LabwareService lwService; + private final BioRiskService bioRiskService; private final OperationService opService; private final StoreService storeService; private final WorkService workService; @@ -58,7 +59,7 @@ public class SlotCopyServiceImp implements SlotCopyService { @Autowired public SlotCopyServiceImp(LabwareRepo lwRepo, SampleRepo sampleRepo, SlotRepo slotRepo, LabwareNoteRepo lwNoteRepo, - SlotCopyValidationService valService, LabwareService lwService, + SlotCopyValidationService valService, LabwareService lwService, BioRiskService bioRiskService, OperationService opService, StoreService storeService, WorkService workService, EntityManager entityManager, Transactor transactor) { this.lwRepo = lwRepo; @@ -67,6 +68,7 @@ public SlotCopyServiceImp(LabwareRepo lwRepo, SampleRepo sampleRepo, SlotRepo sl this.lwNoteRepo = lwNoteRepo; this.valService = valService; this.lwService = lwService; + this.bioRiskService = bioRiskService; this.opService = opService; this.storeService = storeService; this.workService = workService; @@ -127,6 +129,7 @@ public OperationResult executeOps(User user, Collection des ops.addAll(opres.getOperations()); destLabware.addAll(opres.getLabware()); } + bioRiskService.copyOpSampleBioRisks(ops); if (work != null) { workService.link(work, ops); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/extract/ExtractServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/extract/ExtractServiceImp.java index 5512504d..f3f49569 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/extract/ExtractServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/extract/ExtractServiceImp.java @@ -32,6 +32,7 @@ public class ExtractServiceImp implements ExtractService { private final LabwareService labwareService; private final OperationService opService; private final StoreService storeService; + private final BioRiskService bioRiskService; private final WorkService workService; private final LabwareRepo labwareRepo; @@ -46,7 +47,7 @@ public class ExtractServiceImp implements ExtractService { @Autowired public ExtractServiceImp(Transactor transactor, LabwareValidatorFactory labwareValidatorFactory, LabwareService labwareService, OperationService opService, - StoreService storeService, WorkService workService, + StoreService storeService, BioRiskService bioRiskService, WorkService workService, LabwareRepo labwareRepo, LabwareTypeRepo lwTypeRepo, OperationTypeRepo opTypeRepo, SampleRepo sampleRepo, SlotRepo slotRepo, ValidationHelperFactory valFactory) { this.transactor = transactor; @@ -54,6 +55,7 @@ public ExtractServiceImp(Transactor transactor, LabwareValidatorFactory labwareV this.labwareService = labwareService; this.opService = opService; this.storeService = storeService; + this.bioRiskService = bioRiskService; this.workService = workService; this.labwareRepo = labwareRepo; this.lwTypeRepo = lwTypeRepo; @@ -110,9 +112,11 @@ public OperationResult extract(User user, ExtractRequest request) { Map sampleMap = createSamples(labwareMap, bioState); Consumer opModifier = (equipment==null ? null : (op -> op.setEquipment(equipment))); List ops = createOperations(user, opType, labwareMap, sampleMap, opModifier); + bioRiskService.copyOpSampleBioRisks(ops); if (work!=null) { workService.link(work, ops); } + return new OperationResult(ops, labwareMap.values()); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/AliquotServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/AliquotServiceImp.java index a674a4aa..dd2136cd 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/AliquotServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/AliquotServiceImp.java @@ -31,6 +31,7 @@ public class AliquotServiceImp implements AliquotService { private final LabwareValidatorFactory lwValFactory; private final WorkService workService; private final LabwareService lwService; + private final BioRiskService bioRiskService; private final OperationService opService; private final StoreService storeService; private final Transactor transactor; @@ -39,7 +40,7 @@ public class AliquotServiceImp implements AliquotService { public AliquotServiceImp(LabwareRepo lwRepo, LabwareTypeRepo lwTypeRepo, SlotRepo slotRepo, OperationTypeRepo opTypeRepo, SampleRepo sampleRepo, LabwareValidatorFactory lwValFactory, - WorkService workService, LabwareService lwService, OperationService opService, + WorkService workService, LabwareService lwService, BioRiskService bioRiskService, OperationService opService, StoreService storeService, Transactor transactor) { this.lwRepo = lwRepo; this.lwTypeRepo = lwTypeRepo; @@ -49,6 +50,7 @@ public AliquotServiceImp(LabwareRepo lwRepo, LabwareTypeRepo lwTypeRepo, SlotRep this.lwValFactory = lwValFactory; this.workService = workService; this.lwService = lwService; + this.bioRiskService = bioRiskService; this.opService = opService; this.storeService = storeService; this.transactor = transactor; @@ -202,6 +204,7 @@ public OperationResult execute(User user, int numLabware, OperationType opType, List ops = destLabware.stream() .map(lw -> createOperation(user, opType, sourceSample, sourceSlot, destSample, lw.getFirstSlot())) .collect(toList()); + bioRiskService.copyOpSampleBioRisks(ops); if (work!=null) { workService.link(work, ops); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/InPlaceOpServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/InPlaceOpServiceImp.java index 230beb97..7b6262e3 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/InPlaceOpServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/InPlaceOpServiceImp.java @@ -24,6 +24,7 @@ public class InPlaceOpServiceImp implements InPlaceOpService { private final LabwareValidatorFactory labwareValidatorFactory; private final OperationService opService; + private final BioRiskService bioRiskService; private final WorkService workService; private final BioStateReplacer bioStateReplacer; @@ -32,10 +33,11 @@ public class InPlaceOpServiceImp implements InPlaceOpService { private final ValidationHelperFactory valFactory; public InPlaceOpServiceImp(LabwareValidatorFactory labwareValidatorFactory, - OperationService opService, WorkService workService, BioStateReplacer bioStateReplacer, + OperationService opService, BioRiskService bioRiskService, WorkService workService, BioStateReplacer bioStateReplacer, OperationTypeRepo opTypeRepo, LabwareRepo lwRepo, ValidationHelperFactory valFactory) { this.labwareValidatorFactory = labwareValidatorFactory; this.opService = opService; + this.bioRiskService = bioRiskService; this.workService = workService; this.bioStateReplacer = bioStateReplacer; this.opTypeRepo = opTypeRepo; @@ -146,6 +148,7 @@ public OperationResult createOperations(User user, Collection labware, Consumer opModifier = (equipment==null ? null : (op -> op.setEquipment(equipment))); List ops = labware.stream().map(lw -> createOperation(user, lw, opType, opModifier)) .collect(toList()); + bioRiskService.copyOpSampleBioRisks(ops); if (work!=null) { workService.link(work, ops); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmOperationServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmOperationServiceImp.java index 3cdefbd6..ac537de4 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmOperationServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmOperationServiceImp.java @@ -6,8 +6,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.AddressCommentId; import uk.ac.sanger.sccp.stan.request.confirm.*; -import uk.ac.sanger.sccp.stan.service.OperationService; -import uk.ac.sanger.sccp.stan.service.ValidationException; +import uk.ac.sanger.sccp.stan.service.*; import javax.persistence.EntityManager; import java.util.*; @@ -24,6 +23,7 @@ public class ConfirmOperationServiceImp implements ConfirmOperationService { private final ConfirmOperationValidationFactory validationFactory; private final EntityManager entityManager; + private final BioRiskService bioRiskService; private final OperationService operationService; private final LabwareRepo labwareRepo; private final PlanOperationRepo planOpRepo; @@ -35,12 +35,13 @@ public class ConfirmOperationServiceImp implements ConfirmOperationService { @Autowired public ConfirmOperationServiceImp(ConfirmOperationValidationFactory validationFactory, EntityManager entityManager, - OperationService operationService, + BioRiskService bioRiskService, OperationService operationService, LabwareRepo labwareRepo, PlanOperationRepo planOpRepo, SlotRepo slotRepo, SampleRepo sampleRepo, CommentRepo commentRepo, OperationCommentRepo opCommentRepo, MeasurementRepo measurementRepo) { this.validationFactory = validationFactory; this.entityManager = entityManager; + this.bioRiskService = bioRiskService; this.operationService = operationService; this.labwareRepo = labwareRepo; this.planOpRepo = planOpRepo; @@ -96,6 +97,7 @@ public ConfirmOperationResult recordConfirmation(User user, ConfirmOperationRequ } resultLabware.add(clr.labware); } + bioRiskService.copyOpSampleBioRisks(resultOps); return new ConfirmOperationResult(resultOps, resultLabware); } @@ -157,7 +159,7 @@ public ConfirmLabwareResult performConfirmation(ConfirmOperationLabware col, Lab } List planActions = plan.getPlanActions().stream() .filter(planActionFilter) - .collect(toList()); + .toList(); if (planActions.isEmpty()) { // effectively whole lw is cancelled lw.setDiscarded(true); @@ -238,7 +240,7 @@ public void recordComments(ConfirmOperationLabware col, Integer operationId, Lab if (commentIdMap.size() < commentIdSet.size()) { List missing = commentIdSet.stream() .filter(cmtId -> !commentIdMap.containsKey(cmtId)) - .collect(toList()); + .toList(); throw new IllegalArgumentException("Invalid comment ids: "+missing); } List opComments = col.getAddressComments().stream() diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java index 56269773..e29ab3e3 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/operation/confirm/ConfirmSectionServiceImp.java @@ -7,8 +7,7 @@ import uk.ac.sanger.sccp.stan.request.OperationResult; import uk.ac.sanger.sccp.stan.request.confirm.*; import uk.ac.sanger.sccp.stan.request.confirm.ConfirmSectionLabware.AddressCommentId; -import uk.ac.sanger.sccp.stan.service.OperationService; -import uk.ac.sanger.sccp.stan.service.ValidationException; +import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.work.WorkService; import uk.ac.sanger.sccp.utils.UCMap; @@ -27,6 +26,7 @@ @Service public class ConfirmSectionServiceImp implements ConfirmSectionService { private final ConfirmSectionValidationService validationService; + private final BioRiskService bioRiskService; private final OperationService opService; private final WorkService workService; private final LabwareRepo lwRepo; @@ -41,13 +41,14 @@ public class ConfirmSectionServiceImp implements ConfirmSectionService { private final EntityManager entityManager; @Autowired - public ConfirmSectionServiceImp(ConfirmSectionValidationService validationService, OperationService opService, - WorkService workService, + public ConfirmSectionServiceImp(ConfirmSectionValidationService validationService, BioRiskService bioRiskService, + OperationService opService, WorkService workService, LabwareRepo lwRepo, SlotRepo slotRepo, MeasurementRepo measurementRepo, SampleRepo sampleRepo, CommentRepo commentRepo, OperationCommentRepo opCommentRepo, LabwareNoteRepo lwNoteRepo, SamplePositionRepo samplePositionRepo, EntityManager entityManager) { this.validationService = validationService; + this.bioRiskService = bioRiskService; this.opService = opService; this.workService = workService; this.lwRepo = lwRepo; @@ -114,6 +115,7 @@ public OperationResult perform(User user, ConfirmSectionValidation validation, C if (request.getWorkNumber()!=null) { workService.link(request.getWorkNumber(), operations); } + bioRiskService.copyOpSampleBioRisks(operations); updateSourceBlocks(operations); return new OperationResult(operations, resultLabware); } @@ -275,7 +277,7 @@ public void recordAddressComments(ConfirmSectionLabware csl, Integer opId, Labwa if (commentIdMap.size() < commentIdSet.size()) { List missing = commentIdSet.stream() .filter(cmtId -> !commentIdMap.containsKey(cmtId)) - .collect(toList()); + .toList(); throw new IllegalArgumentException("Invalid comment ids: "+missing); } List opComments = csl.getAddressComments().stream() diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java index 65a7b646..8b34e1dd 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterServiceImp.java @@ -22,6 +22,7 @@ public class RegisterServiceImp implements IRegisterService { private final TissueRepo tissueRepo; private final SampleRepo sampleRepo; private final SlotRepo slotRepo; + private final BioRiskRepo bioRiskRepo; private final OperationTypeRepo opTypeRepo; private final LabwareService labwareService; private final OperationService operationService; @@ -31,7 +32,7 @@ public class RegisterServiceImp implements IRegisterService { @Autowired public RegisterServiceImp(EntityManager entityManager, RegisterValidationFactory validationFactory, DonorRepo donorRepo, TissueRepo tissueRepo, SampleRepo sampleRepo, SlotRepo slotRepo, - OperationTypeRepo opTypeRepo, + BioRiskRepo bioRiskRepo, OperationTypeRepo opTypeRepo, LabwareService labwareService, OperationService operationService, WorkService workService, RegisterClashChecker clashChecker) { this.entityManager = entityManager; @@ -40,6 +41,7 @@ public RegisterServiceImp(EntityManager entityManager, RegisterValidationFactory this.tissueRepo = tissueRepo; this.sampleRepo = sampleRepo; this.slotRepo = slotRepo; + this.bioRiskRepo = bioRiskRepo; this.opTypeRepo = opTypeRepo; this.labwareService = labwareService; this.operationService = operationService; @@ -162,7 +164,10 @@ public RegisterResult create(RegisterRequest request, User user, RegisterValidat slot = slotRepo.save(slot); entityManager.refresh(labware); labwareList.add(labware); - ops.add(operationService.createOperationInPlace(opType, user, slot, sample)); + final Operation op = operationService.createOperationInPlace(opType, user, slot, sample); + ops.add(op); + BioRisk bioRisk = validation.getBioRisk(block.getBioRiskCode()); + bioRiskRepo.recordBioRisk(sample, bioRisk, op.getId()); } if (!ops.isEmpty() && validation.getWorks()!=null && !validation.getWorks().isEmpty()) { diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidation.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidation.java index 8b79ea0e..f6acfca8 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidation.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidation.java @@ -21,5 +21,7 @@ public interface RegisterValidation { Tissue getTissue(String externalName); + BioRisk getBioRisk(String code); + Collection getWorks(); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java index a423e90d..8a94a6d3 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationFactory.java @@ -6,8 +6,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; import uk.ac.sanger.sccp.stan.request.register.SectionRegisterRequest; -import uk.ac.sanger.sccp.stan.service.SlotRegionService; -import uk.ac.sanger.sccp.stan.service.Validator; +import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.work.WorkService; /** @@ -34,6 +33,7 @@ public class RegisterValidationFactory { private final Validator replicateValidator; private final TissueFieldChecker tissueFieldChecker; private final SlotRegionService slotRegionService; + private final BioRiskService bioRiskService; private final WorkService workService; @Autowired @@ -48,7 +48,7 @@ public RegisterValidationFactory(DonorRepo donorRepo, HmdmcRepo hmdmcRepo, Tissu @Qualifier("xeniumBarcodeValidator") Validator xeniumBarcodeValidator, @Qualifier("replicateValidator") Validator replicateValidator, TissueFieldChecker tissueFieldChecker, - SlotRegionService slotRegionService, WorkService workService) { + SlotRegionService slotRegionService, BioRiskService bioRiskService, WorkService workService) { this.donorRepo = donorRepo; this.hmdmcRepo = hmdmcRepo; this.ttRepo = ttRepo; @@ -68,12 +68,13 @@ public RegisterValidationFactory(DonorRepo donorRepo, HmdmcRepo hmdmcRepo, Tissu this.tissueFieldChecker = tissueFieldChecker; this.slotRegionService = slotRegionService; this.workService = workService; + this.bioRiskService = bioRiskService; } public RegisterValidation createRegisterValidation(RegisterRequest request) { return new RegisterValidationImp(request, donorRepo, hmdmcRepo, ttRepo, ltRepo, mediumRepo, fixativeRepo, tissueRepo, speciesRepo, donorNameValidation, externalNameValidation, replicateValidator, - tissueFieldChecker, workService); + tissueFieldChecker, bioRiskService, workService); } public SectionRegisterValidation createSectionRegisterValidation(SectionRegisterRequest request) { diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java index 082737da..1d2fedea 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/RegisterValidationImp.java @@ -4,13 +4,16 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.service.BioRiskService; import uk.ac.sanger.sccp.stan.service.Validator; import uk.ac.sanger.sccp.stan.service.work.WorkService; +import uk.ac.sanger.sccp.utils.UCMap; import java.time.LocalDate; import java.util.*; import java.util.function.Function; +import static org.apache.commons.lang3.StringUtils.isBlank; import static uk.ac.sanger.sccp.utils.BasicUtils.*; /** @@ -31,6 +34,7 @@ public class RegisterValidationImp implements RegisterValidation { private final Validator externalNameValidation; private final Validator replicateValidator; private final TissueFieldChecker tissueFieldChecker; + private final BioRiskService bioRiskService; private final WorkService workService; final Map donorMap = new HashMap<>(); @@ -41,16 +45,18 @@ public class RegisterValidationImp implements RegisterValidation { final Map labwareTypeMap = new HashMap<>(); final Map mediumMap = new HashMap<>(); final Map fixativeMap = new HashMap<>(); + UCMap bioRiskMap; Collection works; final LinkedHashSet problems = new LinkedHashSet<>(); public RegisterValidationImp(RegisterRequest request, DonorRepo donorRepo, HmdmcRepo hmdmcRepo, TissueTypeRepo ttRepo, LabwareTypeRepo ltRepo, - MediumRepo mediumRepo, - FixativeRepo fixativeRepo, TissueRepo tissueRepo, SpeciesRepo speciesRepo, + MediumRepo mediumRepo, FixativeRepo fixativeRepo, TissueRepo tissueRepo, + SpeciesRepo speciesRepo, Validator donorNameValidation, Validator externalNameValidation, Validator replicateValidator, - TissueFieldChecker tissueFieldChecker, WorkService workService) { + TissueFieldChecker tissueFieldChecker, + BioRiskService bioRiskService, WorkService workService) { this.request = request; this.donorRepo = donorRepo; this.hmdmcRepo = hmdmcRepo; @@ -64,6 +70,7 @@ public RegisterValidationImp(RegisterRequest request, DonorRepo donorRepo, this.externalNameValidation = externalNameValidation; this.replicateValidator = replicateValidator; this.tissueFieldChecker = tissueFieldChecker; + this.bioRiskService = bioRiskService; this.workService = workService; } @@ -81,6 +88,7 @@ public Collection validate() { validateCollectionDates(); validateExistingTissues(); validateNewTissues(); + validateBioRisks(); validateWorks(); return problems; } @@ -353,6 +361,34 @@ public void validateNewTissues() { } } + public void validateBioRisks() { + Set bioRiskCodes = new LinkedHashSet<>(blocks().size()); + boolean missing = false; + for (BlockRegisterRequest block : blocks()) { + String bioRiskCode = block.getBioRiskCode(); + if (isBlank(bioRiskCode)) { + block.setBioRiskCode(null); + missing = true; + } else { + bioRiskCode = bioRiskCode.trim(); + block.setBioRiskCode(bioRiskCode); + bioRiskCodes.add(bioRiskCode); + } + } + if (missing) { + addProblem("Missing biological risk number."); + } + this.bioRiskMap = bioRiskService.loadBioRiskMap(bioRiskCodes); + if (!bioRiskCodes.isEmpty()) { + List unknown = bioRiskCodes.stream() + .filter(code -> bioRiskMap.get(code)==null) + .toList(); + if (!unknown.isEmpty()) { + addProblem("Unknown biological risk number: "+reprCollection(unknown)); + } + } + } + public void validateWorks() { if (request.getWorkNumbers().isEmpty()) { addProblem("No work number supplied."); @@ -443,6 +479,11 @@ public Tissue getTissue(String externalName) { return ucGet(this.tissueMap, externalName); } + @Override + public BioRisk getBioRisk(String code) { + return bioRiskMap.get(code); + } + @Override public Collection getWorks() { return this.works; diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java index 3e9b4bec..073e3a98 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReader.java @@ -25,6 +25,7 @@ enum Column implements IColumn { Life_stage, Collection_date(LocalDate.class, Pattern.compile("(if.*)?(date.*collection|collection.*date).*", Pattern.CASE_INSENSITIVE)), Species, + Bio_risk(Pattern.compile("bio\\w*\\s+risk.*", Pattern.CASE_INSENSITIVE)), HuMFre(Pattern.compile("humfre\\s*(number)?", Pattern.CASE_INSENSITIVE)), Tissue_type, External_identifier(Pattern.compile("external\\s*id.*", Pattern.CASE_INSENSITIVE)), diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java index eb61f3c8..4449495a 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/BlockRegisterFileReaderImp.java @@ -50,6 +50,7 @@ public BlockRegisterRequest createBlockRequest(Collection problems, Map< br.setDonorIdentifier((String) row.get(Column.Donor_identifier)); br.setFixative((String) row.get(Column.Fixative)); br.setHmdmc((String) row.get(Column.HuMFre)); + br.setBioRiskCode((String) row.get(Column.Bio_risk)); br.setMedium((String) row.get(Column.Embedding_medium)); br.setExternalIdentifier((String) row.get(Column.External_identifier)); br.setSpecies((String) row.get(Column.Species)); diff --git a/src/main/resources/db/changelog/changelog-3.00.xml b/src/main/resources/db/changelog/changelog-3.00.xml new file mode 100644 index 00000000..57233849 --- /dev/null +++ b/src/main/resources/db/changelog/changelog-3.00.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 196da7e8..0f599e2a 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -30,4 +30,5 @@ + diff --git a/src/main/resources/schema.graphqls b/src/main/resources/schema.graphqls index 590d2d06..112b2265 100644 --- a/src/main/resources/schema.graphqls +++ b/src/main/resources/schema.graphqls @@ -117,6 +117,13 @@ type Hmdmc { enabled: Boolean! } +"""Biological risk assessment number.""" +type BioRisk { + """The alphanumeric code representing this risk assessment.""" + code: String! + enabled: Boolean! +} + """A type of label that can be printed, typically including a barcode and some other information.""" type LabelType { name: String! @@ -304,6 +311,8 @@ input BlockRegisterRequest { existingTissue: Boolean """The date the original sample was collected, if known.""" sampleCollectionDate: Date + """The biological risk number for this block.""" + bioRiskCode: String! } """A request to register one or more blocks of tissue.""" @@ -1997,6 +2006,8 @@ type Query { labwareTypes: [LabwareType!]! """Get all the HMDMCs that are enabled, or get all including those that are disabled.""" hmdmcs(includeDisabled: Boolean): [Hmdmc!]! + """Get all bio risks that are enabled, or get all including those that are disabled.""" + bioRisks(includeDisabled: Boolean): [BioRisk!]! """Get all the mediums available.""" mediums: [Medium!]! """Get all the fixatives that are enabled, or get all including those that are disabled.""" @@ -2182,6 +2193,10 @@ type Mutation { addHmdmc(hmdmc: String!): Hmdmc! """Enable or disable an HMDMC.""" setHmdmcEnabled(hmdmc: String!, enabled: Boolean!): Hmdmc! + """Create a new Bio Risk.""" + addBioRisk(code: String!): BioRisk! + """Enable or disable a Bio Risk.""" + setBioRiskEnabled(code: String!, enabled: Boolean!): BioRisk! """Create a new release destination that can be associated with labware releases.""" addReleaseDestination(name: String!): ReleaseDestination! """Enable or disable a release destination.""" diff --git a/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java b/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java index 7abc0fab..696e46fa 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/EntityCreator.java @@ -74,6 +74,8 @@ public class EntityCreator { private OperationRepo opRepo; @Autowired private ActionRepo actionRepo; + @Autowired + private BioRiskRepo bioRiskRepo; @Autowired private EntityManager entityManager; @@ -287,6 +289,11 @@ public ReleaseDestination createReleaseDestination(String name) { return releaseDestinationRepo.save(dest); } + public BioRisk createBioRisk(String code) { + BioRisk br = new BioRisk(code); + return bioRiskRepo.save(br); + } + public BioState anyBioState() { return getAny(bioStateRepo); } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestAdminMutations.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestAdminMutations.java index 0eaf2bc4..fe5f7ade 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestAdminMutations.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestAdminMutations.java @@ -62,6 +62,8 @@ public class TestAdminMutations { private SlotRegionRepo slotRegionRepo; @Autowired private ProbePanelRepo probePanelRepo; + @Autowired + private BioRiskRepo bioRiskRepo; @Test @Transactional @@ -180,4 +182,10 @@ public void testAddNewOmeroProjectAndSetEnabled() throws Exception { public void testAddNewSlotRegionAndSetEnabled() throws Exception { testGenericAddNewAndSetEnabled("SlotRegion", "name", "North", slotRegionRepo::findByName, SlotRegion::getName, "slotRegions"); } + + @Test + @Transactional + public void testAddNewBioRiskAndSetEnabled() throws Exception { + testGenericAddNewAndSetEnabled("BioRisk", "code", "SIBAGMRA12_09v1", bioRiskRepo::findByCode, BioRisk::getCode, "bioRisks"); + } } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java index e0db4dd1..bab1ae13 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFileBlockRegister.java @@ -134,6 +134,8 @@ public void testExistingExtNames() throws Exception { @Transactional public void testIgnoreExtNames() throws Exception { User user = creator.createUser("user1"); + creator.createBioRisk("risk1"); + creator.createBioRisk("risk2"); tester.setUser(user); when(mockRegService.register(any(), any())).thenThrow(new ValidationException(List.of("Bad reg"))); var response = upload("testdata/block_reg_existing.xlsx", null, List.of("Ext17"), false); @@ -145,6 +147,7 @@ public void testIgnoreExtNames() throws Exception { BlockRegisterRequest br = request.getBlocks().getFirst(); assertEquals("EXT18", br.getExternalIdentifier()); assertEquals("Bad reg", getProblem(map)); + assertEquals("risk1", br.getBioRiskCode()); } private MockHttpServletResponse upload(String filename) throws Exception { diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java index 96164bc2..b307cdcc 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestFindLatestOpQuery.java @@ -34,6 +34,7 @@ public class TestFindLatestOpQuery { @Transactional @Test public void testFindLatestOp() throws Exception { + entityCreator.createBioRisk("biorisk1"); Work work = entityCreator.createWork(null, null, null, null, null); String mutation = tester.readGraphQL("register.graphql").replace("SGP1", work.getWorkNumber()); User user = entityCreator.createUser("user1"); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java index ed00c4d1..4c918e46 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestHistoryQuery.java @@ -48,6 +48,7 @@ public class TestHistoryQuery { @Transactional @Test public void testHistory() throws Exception { + entityCreator.createBioRisk("biorisk1"); Work work = entityCreator.createWork(null, null, null, null, null); String mutation = tester.readGraphQL("register.graphql").replace("SGP1", work.getWorkNumber()); User user = entityCreator.createUser("user1"); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java index 34fb5fa0..faf8ddcf 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestRegisterMutation.java @@ -46,6 +46,7 @@ public class TestRegisterMutation { @Test @Transactional public void testRegister() throws Exception { + entityCreator.createBioRisk("biorisk1"); Work work = entityCreator.createWork(null, null, null, null, null); tester.setUser(entityCreator.createUser("dr6")); String mutation = tester.readGraphQL("register.graphql").replace("SGP1", work.getWorkNumber()); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/repo/TestBioRiskRepo.java b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestBioRiskRepo.java new file mode 100644 index 00000000..b8fdd788 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/repo/TestBioRiskRepo.java @@ -0,0 +1,105 @@ +package uk.ac.sanger.sccp.stan.repo; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import uk.ac.sanger.sccp.stan.EntityCreator; +import uk.ac.sanger.sccp.stan.model.*; + +import javax.persistence.EntityNotFoundException; +import javax.transaction.Transactional; +import java.util.*; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** Test {@link BioRiskRepo} */ +@SpringBootTest +@ActiveProfiles(profiles = "test") +@Import(EntityCreator.class) +class TestBioRiskRepo { + @Autowired + BioRiskRepo bioRiskRepo; + @Autowired + EntityCreator entityCreator; + @Autowired + OperationRepo opRepo; + + @Test + @Transactional + public void testBioRisk() { + assertThat(bioRiskRepo.findAll()).isEmpty(); + assertThat(bioRiskRepo.findAllByCodeIn(List.of("alpha", "beta"))).isEmpty(); + assertThat(bioRiskRepo.findByCode("alpha")).isEmpty(); + assertThat(bioRiskRepo.findAllByEnabled(true)).isEmpty(); + + BioRisk alpha = bioRiskRepo.save(new BioRisk("alpha")); + BioRisk beta = bioRiskRepo.save(new BioRisk("beta")); + + assertTrue(alpha.isEnabled()); + assertTrue(beta.isEnabled()); + + assertThat(bioRiskRepo.findAll()).containsExactlyInAnyOrder(alpha, beta); + assertThat(bioRiskRepo.findAllByEnabled(true)).containsExactlyInAnyOrder(alpha, beta); + assertThat(bioRiskRepo.findByCode("alpha")).contains(alpha); + assertThat(bioRiskRepo.findByCode("beta")).contains(beta); + assertThat(bioRiskRepo.findAllByEnabled(false)).isEmpty(); + + beta.setEnabled(false); + bioRiskRepo.save(beta); + assertThat(bioRiskRepo.findAllByEnabled(true)).containsExactly(alpha); + assertThat(bioRiskRepo.findAllByEnabled(false)).containsExactly(beta); + + assertThat(bioRiskRepo.getByCode("alpha")).isEqualTo(alpha); + assertThrows(EntityNotFoundException.class, () -> bioRiskRepo.getByCode("gamma")); + } + + private int makeOpId() { + OperationType opType = entityCreator.createOpType("opname", null); + User user = entityCreator.createUser("username"); + Operation op = new Operation(); + op.setOperationType(opType); + op.setUser(user); + return opRepo.save(op).getId(); + } + + private Sample[] makeSamples(int num) { + Tissue tissue = entityCreator.createTissue(null, null); + return IntStream.range(0, num) + .mapToObj(i -> entityCreator.createSample(tissue, null)) + .toArray(Sample[]::new); + } + + @Test + @Transactional + void testSampleBioRisk() { + Sample[] samples = makeSamples(2); + int[] sampleIds = Arrays.stream(samples).mapToInt(Sample::getId).toArray(); + int opId = makeOpId(); + + BioRisk risk1 = bioRiskRepo.save(new BioRisk("alpha")); + BioRisk risk2 = bioRiskRepo.save(new BioRisk("beta")); + assertThat(bioRiskRepo.loadBioRiskForSampleId(sampleIds[0])).isEmpty(); + + bioRiskRepo.recordBioRisk(samples[0], risk1, opId); + assertThat(bioRiskRepo.loadBioRiskForSampleId(sampleIds[0])).contains(risk1); + assertThat(bioRiskRepo.loadBioRiskForSampleId(sampleIds[1])).isEmpty(); + + bioRiskRepo.recordBioRisk(samples[1], risk2, opId); + assertThat(bioRiskRepo.loadBioRiskForSampleId(sampleIds[0])).contains(risk1); + assertThat(bioRiskRepo.loadBioRiskForSampleId(sampleIds[1])).contains(risk2); + + Map brIdMap = bioRiskRepo.loadBioRiskIdsForSampleIds(List.of(sampleIds[0], sampleIds[1], -1)); + assertThat(brIdMap).hasSize(2); + assertEquals(risk1.getId(), brIdMap.get(sampleIds[0])); + assertEquals(risk2.getId(), brIdMap.get(sampleIds[1])); + + Map brMap = bioRiskRepo.loadBioRisksForSampleIds(List.of(sampleIds[0], sampleIds[1], -1)); + assertThat(brMap).hasSize(2); + assertEquals(risk1, brMap.get(sampleIds[0])); + assertEquals(risk2, brMap.get(sampleIds[1])); + } +} diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestBioRiskService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestBioRiskService.java new file mode 100644 index 00000000..62937e67 --- /dev/null +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestBioRiskService.java @@ -0,0 +1,122 @@ +package uk.ac.sanger.sccp.stan.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.BioRiskRepo; +import uk.ac.sanger.sccp.utils.UCMap; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static uk.ac.sanger.sccp.stan.Matchers.sameElements; +import static uk.ac.sanger.sccp.utils.BasicUtils.inMap; + +/** Test {@link BioRiskService} */ +class TestBioRiskService extends AdminServiceTestUtils { + public TestBioRiskService() { + super("BioRisk", BioRisk::new, + BioRiskRepo::findByCode, "Code not supplied."); + } + + @BeforeEach + void setUp() { + mockRepo = mock(BioRiskRepo.class); + service = spy(new BioRiskService(mockRepo, simpleValidator(), mockTransactor, mockNotifyService)); + } + + @ParameterizedTest + @MethodSource("addNewArgs") + public void testAddNew(String string, String existingString, Exception expectedException, String expectedResultString) { + genericTestAddNew(BioRiskService::addNew, + string, existingString, expectedException, expectedResultString); + } + + @ParameterizedTest + @MethodSource("setEnabledArgs") + public void testSetEnabled(String string, boolean newValue, Boolean oldValue, Exception expectedException) { + genericTestSetEnabled(BioRiskService::setEnabled, + string, newValue, oldValue, expectedException); + } + + @Test + void testLoadBioRiskMap() { + List risks = List.of(new BioRisk(1, "alpha", true), + new BioRisk(2, "beta", false)); + List codes = List.of("alpha", "beta", "gamma"); + when(mockRepo.findAllByCodeIn(any())).thenReturn(risks); + UCMap map = service.loadBioRiskMap(codes); + verify(mockRepo).findAllByCodeIn(codes); + assertThat(map).hasSize(2); + assertSame(risks.get(0), map.get("alpha")); + assertSame(risks.get(1), map.get("beta")); + } + + @Test + void testRecordSampleBioRisks() { + List risks = List.of(new BioRisk(1, "alpha", true), + new BioRisk(2, "beta", false)); + int[] sampleIds = {10,11}; + int opId = 999; + service.recordSampleBioRisks(Map.of(sampleIds[0], risks.get(0), sampleIds[1], risks.get(1)), opId); + verify(mockRepo, times(sampleIds.length)).recordBioRisk(anyInt(), anyInt(), anyInt()); + for (int i = 0; i < sampleIds.length; ++i) { + verify(mockRepo).recordBioRisk(sampleIds[i], risks.get(i).getId(), opId); + } + } + + @ParameterizedTest + @ValueSource(strings={"map", "actions", "op", "ops"}) + void testCopySampleBioRisks(String mode) { + Map sourceBrIds = Map.of( + 10, 100, + 11, 101 + ); + Map sampleDerivations = Map.of( + 20, 10, + 21, 10, + 22, 11, + 23, 12 + ); + when(mockRepo.loadBioRiskIdsForSampleIds(any())).thenReturn(sourceBrIds); + + if (mode.equalsIgnoreCase("map")) { + service.copySampleBioRisks(sampleDerivations); + } else { + Map samples = Stream.concat(sampleDerivations.keySet().stream(), sampleDerivations.values().stream()) + .distinct() + .map(id -> new Sample(id, null, null, null)) + .collect(inMap(Sample::getId)); + List actions = sampleDerivations.entrySet().stream() + .map(e -> new Action(null, null, null, null, samples.get(e.getKey()), samples.get(e.getValue()))) + .toList(); + if (mode.equalsIgnoreCase("actions")) { + service.copyActionSampleBioRisks(actions.stream()); + } else if (mode.equalsIgnoreCase("op")) { + Operation op = new Operation(); + op.setActions(actions); + service.copyOpSampleBioRisks(op); + } else if (mode.equalsIgnoreCase("ops")) { + List ops = List.of(new Operation(), new Operation()); + ops.get(0).setActions(actions.subList(0, 3)); + ops.get(1).setActions(actions.subList(3, actions.size())); + service.copyOpSampleBioRisks(ops); + } + } + + verify(mockRepo).loadBioRiskIdsForSampleIds(sameElements(sampleDerivations.values(), true)); + + verify(mockRepo, times(3)).recordBioRisk(anyInt(), anyInt(), isNull()); + verify(mockRepo).recordBioRisk(20, 100, null); + verify(mockRepo).recordBioRisk(21, 100, null); + verify(mockRepo).recordBioRisk(22, 101, null); + } +} \ No newline at end of file diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java index ea561f0c..6e6abf6c 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestBlockProcessingService.java @@ -61,6 +61,8 @@ public class TestBlockProcessingService { @Mock private LabwareService mockLwService; @Mock + private BioRiskService mockBioRiskService; + @Mock private WorkService mockWorkService; @Mock private StoreService mockStoreService; @@ -77,7 +79,8 @@ void setup() { service = spy(new BlockProcessingServiceImp(mockLwValFactory, mockPrebarcodeValidator, mockReplicateValidator, mockLwRepo, mockSlotRepo, mockOpTypeRepo, mockOpCommentRepo, mockLtRepo, mockBsRepo, mockTissueRepo, mockSampleRepo, - mockCommentValidationService, mockOpService, mockLwService, mockWorkService, mockStoreService, + mockCommentValidationService, mockOpService, mockLwService, mockBioRiskService, mockWorkService, + mockStoreService, mockTransactor)); } @@ -230,6 +233,7 @@ private void verifyNoCreation() { verify(service, never()).createSamples(any(), any()); verify(service, never()).createDestinations(any(), any(), any()); verify(service, never()).createOperations(any(), any(), any(), any(), any()); + verifyNoInteractions(mockBioRiskService); verify(mockWorkService, never()).link(any(Work.class), any()); verify(service, never()).discardSources(any(), any()); } @@ -240,6 +244,7 @@ private void verifyCreation(TissueBlockRequest request, User user, UCMap Answer addProblem(String problem, R returnValue) { @@ -378,6 +381,7 @@ public void testExecute(boolean discard, boolean hasWork, boolean setBioState, i } else { verify(mockWorkService).link(work, ops); } + verify(mockBioRiskService).copyOpSampleBioRisks(ops); } @Test diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java index f0e60576..f1a44089 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/TestInPlaceOpService.java @@ -31,6 +31,7 @@ public class TestInPlaceOpService { private InPlaceOpServiceImp service; private LabwareValidatorFactory mockLabwareValidatorFactory; + private BioRiskService mockBioRiskService; private OperationService mockOpService; private WorkService mockWorkService; private BioStateReplacer mockBioStateReplacer; @@ -42,6 +43,7 @@ public class TestInPlaceOpService { @BeforeEach void setup() { mockLabwareValidatorFactory = mock(LabwareValidatorFactory.class); + mockBioRiskService = mock(BioRiskService.class); mockOpService = mock(OperationService.class); mockWorkService = mock(WorkService.class); mockBioStateReplacer = mock(BioStateReplacer.class); @@ -50,7 +52,8 @@ void setup() { valFactory = mock(ValidationHelperFactory.class); mockVal = mock(ValidationHelper.class); - service = spy(new InPlaceOpServiceImp(mockLabwareValidatorFactory, mockOpService, mockWorkService, + service = spy(new InPlaceOpServiceImp(mockLabwareValidatorFactory, mockOpService, mockBioRiskService, + mockWorkService, mockBioStateReplacer, mockOpTypeRepo, mockLwRepo, valFactory)); } @@ -242,6 +245,7 @@ public void testCreateOperations(Collection labware, OperationType opTy } else { verify(mockWorkService).link(work, ops); } + verify(mockBioRiskService).copyOpSampleBioRisks(ops); } static Stream createOperationsArgs() { diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java index 4b84c7db..ac9454ca 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmOperationService.java @@ -10,8 +10,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.AddressCommentId; import uk.ac.sanger.sccp.stan.request.confirm.*; -import uk.ac.sanger.sccp.stan.service.OperationService; -import uk.ac.sanger.sccp.stan.service.ValidationException; +import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.operation.confirm.ConfirmOperationServiceImp.ConfirmLabwareResult; import javax.persistence.EntityManager; @@ -32,6 +31,7 @@ public class TestConfirmOperationService { private ConfirmOperationValidationFactory mockConfirmOperationValidationFactory; private EntityManager mockEntityManager; + private BioRiskService mockBioRiskService; private OperationService mockOperationService; private LabwareRepo mockLabwareRepo; private PlanOperationRepo mockPlanOperationRepo; @@ -47,6 +47,7 @@ public class TestConfirmOperationService { void setup() { mockConfirmOperationValidationFactory = mock(ConfirmOperationValidationFactory.class); mockEntityManager = mock(EntityManager.class); + mockBioRiskService = mock(BioRiskService.class); mockOperationService = mock(OperationService.class); mockLabwareRepo = mock(LabwareRepo.class); mockPlanOperationRepo = mock(PlanOperationRepo.class); @@ -57,8 +58,8 @@ void setup() { mockMeasurementRepo = mock(MeasurementRepo.class); service = spy(new ConfirmOperationServiceImp(mockConfirmOperationValidationFactory, mockEntityManager, - mockOperationService, mockLabwareRepo, mockPlanOperationRepo, mockSlotRepo, mockSampleRepo, - mockCommentRepo, mockOperationCommentRepo, mockMeasurementRepo)); + mockBioRiskService, mockOperationService, mockLabwareRepo, mockPlanOperationRepo, mockSlotRepo, + mockSampleRepo, mockCommentRepo, mockOperationCommentRepo, mockMeasurementRepo)); } @ParameterizedTest @@ -125,6 +126,7 @@ public void testRecordConfirmation() { assertThat(result.getLabware()).hasSameElementsAs(labware); assertThat(result.getOperations()).containsOnly(op); + verify(mockBioRiskService).copyOpSampleBioRisks(result.getOperations()); assertThat(assertThrows(IllegalArgumentException.class, () -> service.recordConfirmation(user, new ConfirmOperationRequest( diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java index 9b2ed566..c578b667 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/operation/confirm/TestConfirmSectionService.java @@ -11,8 +11,7 @@ import uk.ac.sanger.sccp.stan.request.OperationResult; import uk.ac.sanger.sccp.stan.request.confirm.*; import uk.ac.sanger.sccp.stan.request.confirm.ConfirmSectionLabware.AddressCommentId; -import uk.ac.sanger.sccp.stan.service.OperationService; -import uk.ac.sanger.sccp.stan.service.ValidationException; +import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.operation.confirm.ConfirmSectionServiceImp.ConfirmLabwareResult; import uk.ac.sanger.sccp.stan.service.operation.confirm.ConfirmSectionServiceImp.PlanActionKey; import uk.ac.sanger.sccp.stan.service.work.WorkService; @@ -35,6 +34,7 @@ */ public class TestConfirmSectionService { private ConfirmSectionValidationService mockValidationService; + private BioRiskService mockBioRiskService; private OperationService mockOpService; private WorkService mockWorkService; private LabwareRepo mockLwRepo; @@ -52,6 +52,7 @@ public class TestConfirmSectionService { @BeforeEach void setup() { mockValidationService = mock(ConfirmSectionValidationService.class); + mockBioRiskService = mock(BioRiskService.class); mockOpService = mock(OperationService.class); mockWorkService = mock(WorkService.class); mockLwRepo = mock(LabwareRepo.class); @@ -63,7 +64,7 @@ void setup() { mockLwNoteRepo = mock(LabwareNoteRepo.class); mockSamplePositionRepo = mock(SamplePositionRepo.class); mockEntityManager = mock(EntityManager.class); - service = spy(new ConfirmSectionServiceImp(mockValidationService, mockOpService, mockWorkService, + service = spy(new ConfirmSectionServiceImp(mockValidationService, mockBioRiskService, mockOpService, mockWorkService, mockLwRepo, mockSlotRepo, mockMeasurementRepo, mockSampleRepo, mockCommentRepo, mockOpCommentRepo, mockLwNoteRepo, mockSamplePositionRepo, mockEntityManager)); } @@ -185,6 +186,7 @@ public void testPerformSuccessful() { verify(service).confirmLabware(user, csl2, lw2, plan2, regionMap, commentMap); verify(service).recordAddressComments(csl1, op1.getId(), lw1B); verify(service).recordAddressComments(csl2, null, lw2B); + verify(mockBioRiskService).copyOpSampleBioRisks(result.getOperations()); verify(mockWorkService).link(request.getWorkNumber(), result.getOperations()); verify(service).updateSourceBlocks(result.getOperations()); verify(service).loadPlanNotes(Set.of(10,11)); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java index 71ead68b..8c3ea9af 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterService.java @@ -45,6 +45,8 @@ public class TestRegisterService { @Mock private SlotRepo mockSlotRepo; @Mock + private BioRiskRepo mockBioRiskRepo; + @Mock private OperationTypeRepo mockOpTypeRepo; @Mock private LabwareService mockLabwareService; @@ -75,7 +77,7 @@ void setup() { when(mockOpTypeRepo.getByName(opType.getName())).thenReturn(opType); registerService = spy(new RegisterServiceImp(mockEntityManager, mockValidationFactory, mockDonorRepo, mockTissueRepo, - mockSampleRepo, mockSlotRepo, mockOpTypeRepo, mockLabwareService, mockOpService, mockWorkService, mockClashChecker)); + mockSampleRepo, mockSlotRepo, mockBioRiskRepo, mockOpTypeRepo, mockLabwareService, mockOpService, mockWorkService, mockClashChecker)); } @AfterEach @@ -450,6 +452,7 @@ public void testCreateProblems(Species species, Object hmdmcObj, String expected hmdmc = null; hmdmcString = null; } + BioRisk br = new BioRisk(800, "biorisk"); BlockRegisterRequest block = new BlockRegisterRequest(); block.setDonorIdentifier(donor.getDonorName()); @@ -464,11 +467,18 @@ public void testCreateProblems(Species species, Object hmdmcObj, String expected block.setReplicateNumber("2"); block.setSpatialLocation(1); block.setSpecies(species.getName()); + block.setBioRiskCode(br.getCode()); Map donorMap = Map.of(donor.getDonorName().toUpperCase(), donor); doReturn(donorMap).when(registerService).createDonors(any(), any()); final SpatialLocation sl = new SpatialLocation(1, "SL0", 1, tissueType); + Operation op = new Operation(); + op.setId(700); + when(mockOpService.createOperationInPlace(any(), any(), any(), any())).thenReturn(op); + + when(mockValidation.getBioRisk(br.getCode())).thenReturn(br); + when(mockValidation.getSpatialLocation(eqCi(tissueType.getName()), eq(1))) .thenReturn(sl); when(mockValidation.getMedium(eqCi(medium.getName()))).thenReturn(medium); @@ -522,6 +532,7 @@ public void testCreateProblems(Species species, Object hmdmcObj, String expected assertEquals(slot.getBlockSampleId(), sample.getId()); verify(mockSlotRepo).save(slot); verify(mockOpService).createOperationInPlace(opType, user, slot, sample); + verify(mockBioRiskRepo).recordBioRisk(sample, br, op.getId()); } static Stream createArgs() { diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java index ed9f24fc..b264f595 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidation.java @@ -14,6 +14,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.register.BlockRegisterRequest; import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; +import uk.ac.sanger.sccp.stan.service.BioRiskService; import uk.ac.sanger.sccp.stan.service.Validator; import uk.ac.sanger.sccp.stan.service.register.RegisterValidationImp.StringIntKey; import uk.ac.sanger.sccp.stan.service.work.WorkService; @@ -63,6 +64,8 @@ public class TestRegisterValidation { @Mock private TissueFieldChecker mockFieldChecker; @Mock + private BioRiskService mockBioRiskService; + @Mock private WorkService mockWorkService; private AutoCloseable mocking; @@ -87,7 +90,8 @@ private void loadSpecies(final Collection specieses) { private RegisterValidationImp create(RegisterRequest request) { return spy(new RegisterValidationImp(request, mockDonorRepo, mockHmdmcRepo, mockTtRepo, mockLtRepo, mockMediumRepo, mockFixativeRepo, mockTissueRepo, mockSpeciesRepo, - mockDonorNameValidation, mockExternalNameValidation, mockReplicateValidator, mockFieldChecker, mockWorkService)); + mockDonorNameValidation, mockExternalNameValidation, mockReplicateValidator, mockFieldChecker, + mockBioRiskService, mockWorkService)); } private void stubValidationMethods(RegisterValidationImp validation) { @@ -100,6 +104,7 @@ private void stubValidationMethods(RegisterValidationImp validation) { doNothing().when(validation).validateNewTissues(); doNothing().when(validation).validateFixatives(); doNothing().when(validation).validateCollectionDates(); + doNothing().when(validation).validateBioRisks(); doNothing().when(validation).validateWorks(); } @@ -772,7 +777,7 @@ private static Stream newTissueData() { ); } - private static class ValidateTissueTestData { + static class ValidateTissueTestData { String externalName; String replicate = "1"; String donorName = "D"; diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java index e4e368f4..bc6ace22 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestRegisterValidationFactory.java @@ -5,8 +5,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.request.register.RegisterRequest; import uk.ac.sanger.sccp.stan.request.register.SectionRegisterRequest; -import uk.ac.sanger.sccp.stan.service.SlotRegionService; -import uk.ac.sanger.sccp.stan.service.Validator; +import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.work.WorkService; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -28,7 +27,8 @@ void setup() { mock(FixativeRepo.class), mock(TissueRepo.class), mock(SpeciesRepo.class), mock(LabwareRepo.class), mock(BioStateRepo.class), mockStringValidator, mockStringValidator, mockStringValidator, mockStringValidator, mockStringValidator, mockStringValidator, - mock(TissueFieldChecker.class), mock(SlotRegionService.class), mock(WorkService.class)); + mock(TissueFieldChecker.class), mock(SlotRegionService.class), mock(BioRiskService.class), + mock(WorkService.class)); } @Test diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java index 2b3f2f74..41d8887f 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestBlockRegisterFileReader.java @@ -119,7 +119,7 @@ void testRead(boolean error) { void testIndexColumns() { Row row = mockRow("All information is needed", "SGP number", "donor identifier", "life stage", "if then date of collection of stuff", - "species", "humfre", "tissue type", "external id", "spatial location", "replicate number", + "species", "biological risk assessment number", "humfre", "tissue type", "external id", "spatial location", "replicate number", "last known banana section custard", "labware type", "fixative", "medium", "information", "comment"); List problems = new ArrayList<>(); var result = reader.indexColumns(problems, row); @@ -137,7 +137,7 @@ void testIndexColumnsProblems() { Row row = mockRow( "SGP number", "work number", "donor identifier", "life stage", "if then date of collection of stuff", "bananas", - "species", "humfre", "spatial location", "replicate number", + "species", "bio risk", "humfre", "spatial location", "replicate number", "last known banana section custard", "labware type", "fixative", "medium", "information"); List problems = new ArrayList<>(3); reader.indexColumns(problems, row); @@ -325,7 +325,7 @@ void testCreateRequest_problems() { ); List srls = IntStream.range(1, 3) .mapToObj(i -> makeBlockRegisterRequest("X"+i)) - .collect(toList()); + .toList(); doReturn(srls.get(0)).when(reader).createBlockRequest(any(), same(rows.get(0))); Matchers.mayAddProblem("Bad stuff.", srls.get(1)).when(reader).createBlockRequest(any(), same(rows.get(1))); diff --git a/src/test/resources/graphql/register.graphql b/src/test/resources/graphql/register.graphql index 4b47d0ec..fceda1a2 100644 --- a/src/test/resources/graphql/register.graphql +++ b/src/test/resources/graphql/register.graphql @@ -15,6 +15,7 @@ mutation { fixative:"None", highestSection:0, sampleCollectionDate: "2021-02-03", + bioRiskCode: "biorisk1" } ] workNumbers: ["SGP1"] diff --git a/src/test/resources/testdata/block_reg.xlsx b/src/test/resources/testdata/block_reg.xlsx index ae89785f3832748d471b6a802ac1e08dad4fd98e..b3f04747c384018637b56f33fbdf4e38d82f389e 100644 GIT binary patch delta 6407 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 Gz4|X{irVJ@ delta 6139 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 P2;*P#Z`>+O|JwZ@cXNt4 diff --git a/src/test/resources/testdata/block_reg_existing.xlsx b/src/test/resources/testdata/block_reg_existing.xlsx index 2bd9ef77ed72e112301d873787f1f8484e00b9ca..a59007e99f6e089c075d29dee16c854b2ea06ae0 100644 GIT binary patch delta 3264 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 Y64)w&fYYAN2>{H0<8;7&p}*Px0B0q~3jhEB diff --git a/src/test/resources/testdata/reg_empty.xlsx b/src/test/resources/testdata/reg_empty.xlsx index bdf97b44ef10634874cd05346d0f335e6b35517d..bad4af49655037739b036b2f09eccf0998f83275 100644 GIT binary patch delta 6996 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