From e02b0f8b6fc72c8179a7eee4b945464cf0dbed18 Mon Sep 17 00:00:00 2001 From: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:54:55 +0100 Subject: [PATCH] x1236 Add sample prep reagent lot to probe operation --- .../sccp/stan/config/FieldValidation.java | 5 + .../stan/request/ProbeOperationRequest.java | 17 ++- .../sccp/stan/service/ProbeServiceImp.java | 52 ++++++++ .../uk/ac/sanger/sccp/utils/BasicUtils.java | 26 +++- .../java/uk/ac/sanger/sccp/utils/UCMap.java | 37 ++++++ src/main/resources/schema.graphqls | 2 + .../TestProbeOperationMutation.java | 11 +- .../sccp/stan/service/ProbeServiceTest.java | 111 ++++++++++++++---- .../graphql/recordprobeoperation.graphql | 1 + 9 files changed, 233 insertions(+), 29 deletions(-) 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 907fcbf9..54d59a12 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 @@ -290,6 +290,11 @@ public Validator reagentPlateBarcodeValidator() { return new StringValidator("Reagent plate barcode", 24, 24, charTypes); } + @Bean + public Validator samplePrepReagentLotValidator() { + return new StringValidator("Sample prep reagent lot", 6, 6, CharacterType.DIGIT); + } + @Bean public Validator cytAssistBarcodeValidator() { Set charTypes = EnumSet.of(CharacterType.DIGIT, CharacterType.ALPHA, CharacterType.HYPHEN); diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/ProbeOperationRequest.java b/src/main/java/uk/ac/sanger/sccp/stan/request/ProbeOperationRequest.java index b1759e86..99514d2c 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/ProbeOperationRequest.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/ProbeOperationRequest.java @@ -24,14 +24,16 @@ public static class ProbeOperationLabware { private String barcode; private String workNumber; private SlideCosting kitCosting; + private String samplePrepReagentLot; private List probes = List.of(); public ProbeOperationLabware() {} - public ProbeOperationLabware(String barcode, String workNumber, SlideCosting kitCosting, List probes) { + public ProbeOperationLabware(String barcode, String workNumber, SlideCosting kitCosting, String samplePrepReagentLot, List probes) { setBarcode(barcode); setWorkNumber(workNumber); setKitCosting(kitCosting); + setSamplePrepReagentLot(samplePrepReagentLot); setProbes(probes); } @@ -67,6 +69,15 @@ public void setKitCosting(SlideCosting kitCosting) { this.kitCosting = kitCosting; } + /** Sample prep reagent lot number. */ + public String getSamplePrepReagentLot() { + return this.samplePrepReagentLot; + } + + public void setSamplePrepReagentLot(String samplePrepReagentLot) { + this.samplePrepReagentLot = samplePrepReagentLot; + } + /** * The probes used on this labware. */ @@ -86,6 +97,7 @@ public boolean equals(Object o) { return (Objects.equals(this.barcode, that.barcode) && Objects.equals(this.workNumber, that.workNumber) && this.kitCosting==that.kitCosting + && Objects.equals(this.samplePrepReagentLot, that.samplePrepReagentLot) && Objects.equals(this.probes, that.probes) ); } @@ -97,7 +109,8 @@ public int hashCode() { @Override public String toString() { - return String.format("(%s, %s, workNumber: %s, probes: %s)", repr(barcode), kitCosting, repr(workNumber), probes); + return String.format("(%s, %s, workNumber: %s, samplePrepReagentLot: %s, probes: %s)", + repr(barcode), kitCosting, repr(workNumber), repr(samplePrepReagentLot), probes); } } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/ProbeServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/ProbeServiceImp.java index a9ad8409..99656ced 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/ProbeServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/ProbeServiceImp.java @@ -14,6 +14,7 @@ import java.time.*; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Stream; import static uk.ac.sanger.sccp.utils.BasicUtils.*; @@ -24,6 +25,7 @@ @Service public class ProbeServiceImp implements ProbeService { public static final String KIT_COSTING_NAME = "kit costing"; + public static final String SAMPLE_PREP_REAGENT_LOT_NAME = "sample prep reagent lot"; private final LabwareValidatorFactory lwValFac; private final LabwareRepo lwRepo; @@ -35,6 +37,7 @@ public class ProbeServiceImp implements ProbeService { private final OperationService opService; private final WorkService workService; private final Validator probeLotValidator; + private final Validator samplePrepReagentLotValidator; private final Clock clock; @Autowired @@ -43,6 +46,7 @@ public ProbeServiceImp(LabwareValidatorFactory lwValFac, ProbePanelRepo probePanelRepo, LabwareProbeRepo lwProbeRepo, LabwareNoteRepo noteRepo, OperationService opService, WorkService workService, @Qualifier("probeLotNumberValidator") Validator probeLotValidator, + @Qualifier("samplePrepReagentLotValidator") Validator samplePrepReagentLotValidator, Clock clock) { this.lwValFac = lwValFac; this.lwRepo = lwRepo; @@ -54,6 +58,7 @@ public ProbeServiceImp(LabwareValidatorFactory lwValFac, this.opService = opService; this.workService = workService; this.probeLotValidator = probeLotValidator; + this.samplePrepReagentLotValidator = samplePrepReagentLotValidator; this.clock = clock; } @@ -79,6 +84,7 @@ public OperationResult recordProbeOperation(User user, ProbeOperationRequest req .toList(); UCMap work = workService.validateUsableWorks(problems, workNumbers); checkKitCostings(problems, request.getLabware()); + checkSamplePrepReagentLots(problems, request.getLabware()); UCMap probes = validateProbes(problems, request.getLabware()); if (request.getPerformed()!=null) { validateTimestamp(problems, request.getPerformed(), labware); @@ -160,6 +166,28 @@ public void checkKitCostings(Collection problems, Collection problems, Collection pols) { + if (pols != null) { + final Consumer addProblem = problems::add; + for (ProbeOperationLabware pol : pols) { + String lot = pol.getSamplePrepReagentLot(); + if (lot != null) { + lot = emptyToNull(lot.trim()); + pol.setSamplePrepReagentLot(lot); + if (lot != null) { + samplePrepReagentLotValidator.validate(lot, addProblem); + } + } + } + } + } + /** * Loads and checks the probes indicated in the request, their lot numbers and plexes. * @param problems receptacle for problems @@ -240,6 +268,7 @@ public OperationResult perform(User user, Collection pols UCMap workMap) { UCMap lwOps = makeOps(user, opType, pols, lwMap, time); saveKitCostings(pols, lwMap, lwOps); + saveSamplePrepReagentLots(pols, lwMap, lwOps); linkWork(pols, lwOps, workMap); saveProbes(pols, lwOps, lwMap, probeMap); return assembleResult(pols, lwMap, lwOps); @@ -324,6 +353,29 @@ public void saveProbes(Collection pols, UCMap lwProbeRepo.saveAll(lwProbes); } + /** + * Saves the indicated sample prep reagent lots against the indicated labware and ops + * @param pols request details + * @param lwMap map to look up labware by barcode + * @param lwOps map to look up operation by labware barcode + */ + public void saveSamplePrepReagentLots(Collection pols, + UCMap lwMap, UCMap lwOps) { + List notes = new ArrayList<>(); + for (ProbeOperationLabware pol : pols) { + String lot = pol.getSamplePrepReagentLot(); + if (!nullOrEmpty(lot)) { + String barcode = pol.getBarcode(); + Labware lw = lwMap.get(barcode); + Operation op = lwOps.get(barcode); + notes.add(new LabwareNote(null, lw.getId(), op.getId(), SAMPLE_PREP_REAGENT_LOT_NAME, lot)); + } + } + if (!notes.isEmpty()) { + noteRepo.saveAll(notes); + } + } + /** * Assembles an operation result whose order matches the order given in the request. * @param pols the request specifying labware barcodes diff --git a/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java b/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java index ba2cc67f..90b78eed 100644 --- a/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java +++ b/src/main/java/uk/ac/sanger/sccp/utils/BasicUtils.java @@ -231,20 +231,36 @@ public static ArrayList newArrayList(Iterable items) { } /** - * Collector to a map where the values are the input objects + * Collector to a map whose values are the items in the stream * @param keyMapper a mapping function to produce keys * @param mapFactory a supplier providing a new empty {@code Map} * into which the results will be inserted * @param the type of the input elements - * @param the output type of the key mapping function + * @param the key type of the resulting {@code Map} * @param the type of the resulting {@code Map} * @return a {@code Collector} which collects elements into a {@code Map} - * whose keys are the result of applying a key mapping function to the input - * elements, and whose values are input elements */ public static > Collector inMap(Function keyMapper, Supplier mapFactory) { - return Collectors.toMap(keyMapper, Function.identity(), illegalStateMerge(), mapFactory); + return toMap(keyMapper, Function.identity(), mapFactory); + } + + /** + * Collector to a map + * @param keyMapper a mapping function to produce keys + * @param valueMapper a mapping function to produce values + * @param mapFactory a supplier providing a new empty {@code Map} + * into which the results will be inserted + * @param the type of the input elements + * @param the key type of the resulting {@code Map} + * @param the value type of the resulting {@code Map} + * @param the type of the resulting {@code Map} + * @return a {@code Collector} which collects elements into a {@code Map} + */ + public static > Collector toMap(Function keyMapper, + Function valueMapper, + Supplier mapFactory) { + return Collectors.toMap(keyMapper, valueMapper, illegalStateMerge(), mapFactory); } /** diff --git a/src/main/java/uk/ac/sanger/sccp/utils/UCMap.java b/src/main/java/uk/ac/sanger/sccp/utils/UCMap.java index 80bce17b..6668febc 100644 --- a/src/main/java/uk/ac/sanger/sccp/utils/UCMap.java +++ b/src/main/java/uk/ac/sanger/sccp/utils/UCMap.java @@ -16,14 +16,17 @@ public class UCMap implements Map { private final Map inner; + /** Creates a new UCMap backed by a hashmap with the given initial capacity */ public UCMap(int initialCapacity) { inner = new HashMap<>(initialCapacity); } + /** Creates a new UCMap backed by a hashmap of the default capacity */ public UCMap() { inner = new HashMap<>(); } + /** Creates a new UCMap containing the contents of the given map */ public UCMap(Map contents) { this(contents.size()); this.putAll(contents); @@ -162,6 +165,13 @@ public int hashCode() { return inner.hashCode(); } + /** + * Converts a given key to upper case. If it's not a string, + * returns the key. + * @param key the key to upcase + * @return the upcased key + * @param the type of the key, typically a string + */ private static K upcase(K key) { if (key instanceof String) { //noinspection unchecked @@ -185,6 +195,26 @@ public String toString() { return BasicUtils.inMap(keyMapper, UCMap::new); } + /** + * Collector to collect a {@code UCMap} from strings to values extracted from the input objects + * @param keyMapper mapper from input objects to strings + * @param valueMapper mapper from input objects to values in map + * @return a collector to collect to a {@code UCMap} + * @param the type of input object + * @param the type of value in the map + */ + public static Collector> toUCMap(Function keyMapper, + Function valueMapper) { + return BasicUtils.toMap(keyMapper, valueMapper, UCMap::new); + } + + /** + * Creates a UCMap containing a single item + * @param keyMapper function to extract key from the item + * @param value the item to put into the map as a value + * @return a UCMap containing one item + * @param the type of value to put in the map + */ public static UCMap from(Function keyMapper, T value) { UCMap map = new UCMap<>(1); map.put(keyMapper.apply(value), value); @@ -200,6 +230,13 @@ public static UCMap from(Function keyMapper, T... values) { return map; } + /** + * Creates a UCMap containing the items from a stream, using the keys from the given function + * @param items a stream or items to put into a map + * @param keyMapper function to get keys for items + * @return map containing the given items as values + * @param type of items to put into the map + */ public static UCMap from(Collection items, Function keyMapper) { UCMap map = new UCMap<>(items.size()); for (T item : items) { diff --git a/src/main/resources/schema.graphqls b/src/main/resources/schema.graphqls index fe9534a4..f5e974d6 100644 --- a/src/main/resources/schema.graphqls +++ b/src/main/resources/schema.graphqls @@ -1738,6 +1738,8 @@ input ProbeOperationLabware { workNumber: String! """The costing for the kit used on this labware.""" kitCosting: SlideCosting! + """Sample prep reagent lot number.""" + samplePrepReagentLot: String """The probes used on this labware.""" probes: [ProbeLot!]! } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestProbeOperationMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestProbeOperationMutation.java index 944e601d..47fc4e37 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestProbeOperationMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestProbeOperationMutation.java @@ -12,6 +12,7 @@ import uk.ac.sanger.sccp.stan.repo.*; import uk.ac.sanger.sccp.stan.service.CompletionServiceImp; import uk.ac.sanger.sccp.stan.service.operation.AnalyserServiceImp; +import uk.ac.sanger.sccp.utils.UCMap; import javax.transaction.Transactional; import java.util.*; @@ -79,7 +80,7 @@ public void testRecordProbeOperation() throws Exception { List lwProbes = lwProbeRepo.findAllByOperationIdIn(List.of(opId)); assertThat(lwProbes).hasSize(2); if (lwProbes.getFirst().getProbePanel().getName().equals("probe2")) { - lwProbes = IntStream.of(lwProbes.size()-1,-1,-1).mapToObj(lwProbes::get).collect(toList()); + lwProbes = IntStream.of(lwProbes.size() - 1, -1, -1).mapToObj(lwProbes::get).collect(toList()); } for (LabwareProbe lwp : lwProbes) { assertEquals(opId, lwp.getOperationId()); @@ -95,7 +96,13 @@ public void testRecordProbeOperation() throws Exception { assertEquals(2, lwp.getPlex()); assertEquals("LOT2", lwp.getLotNumber()); assertEquals(SlideCosting.SGP, lwp.getCosting()); - + List notes = lwNoteRepo.findAllByOperationIdIn(List.of(opId)); + assertThat(notes).hasSize(2); + notes.forEach(note -> assertEquals(lw.getId(), note.getLabwareId())); + UCMap noteValues = notes.stream() + .collect(UCMap.toUCMap(LabwareNote::getName, LabwareNote::getValue)); + assertEquals("Faculty", noteValues.get("kit costing")); + assertEquals("123456", noteValues.get("sample prep reagent lot")); testCompletion(lw, work, sample); testAnalyser(lw, work, sample); testSampleMetrics(lw, work); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/ProbeServiceTest.java b/src/test/java/uk/ac/sanger/sccp/stan/service/ProbeServiceTest.java index 6b5497b3..13b93cf9 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/ProbeServiceTest.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/ProbeServiceTest.java @@ -14,6 +14,7 @@ import uk.ac.sanger.sccp.stan.request.ProbeOperationRequest.ProbeOperationLabware; import uk.ac.sanger.sccp.stan.service.work.WorkService; import uk.ac.sanger.sccp.utils.UCMap; +import uk.ac.sanger.sccp.utils.Zip; import java.time.*; import java.util.*; @@ -27,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.*; import static uk.ac.sanger.sccp.stan.Matchers.*; +import static uk.ac.sanger.sccp.stan.service.ProbeServiceImp.SAMPLE_PREP_REAGENT_LOT_NAME; import static uk.ac.sanger.sccp.utils.BasicUtils.concat; import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty; @@ -56,6 +58,8 @@ public class ProbeServiceTest { @Mock private Validator mockProbeLotValidator; @Mock + private Validator mockSamplePrepReagentLotValidator; + @Mock private Clock mockClock; @InjectMocks @@ -67,7 +71,9 @@ public class ProbeServiceTest { @BeforeEach void setUp() { mocking = MockitoAnnotations.openMocks(this); - service = spy(service); + service = spy(new ProbeServiceImp(mockLwValFac, mockLwRepo, mockOpTypeRepo, mockOpRepo, + mockProbePanelRepo, mockLwProbeRepo, mockNoteRepo, mockOpService, mockWorkService, + mockProbeLotValidator, mockSamplePrepReagentLotValidator, mockClock)); } @AfterEach @@ -89,6 +95,7 @@ public void testRecordProbeOperation(User user, ProbeOperationRequest request, UCMap ppMap = UCMap.from(ProbePanel::getName, new ProbePanel("bananas")); doReturn(ppMap).when(service).validateProbes(any(), any()); doNothing().when(service).validateTimestamp(any(), any(), any()); + doNothing().when(service).checkSamplePrepReagentLots(any(), any()); doNothing().when(service).checkKitCostings(any(), any()); if (nullOrEmpty(expectedProblems)) { @@ -105,6 +112,8 @@ public void testRecordProbeOperation(User user, ProbeOperationRequest request, verifyNoInteractions(mockWorkService); verify(service, never()).validateProbes(any(), any()); verify(service, never()).validateTimestamp(any(), any(), any()); + verify(service, never()).checkSamplePrepReagentLots(any(), any()); + verify(service, never()).checkKitCostings(any(), any()); return; } final List pols = request.getLabware(); @@ -120,6 +129,7 @@ public void testRecordProbeOperation(User user, ProbeOperationRequest request, .map(ProbeOperationLabware::getWorkNumber) .collect(toList()))); verify(service).validateProbes(any(), eq(pols)); + verify(service).checkSamplePrepReagentLots(any(), eq(pols)); verify(service).checkKitCostings(any(), eq(pols)); if (request.getPerformed()==null) { verify(service, never()).validateTimestamp(any(), any(), any()); @@ -142,7 +152,7 @@ public void testRecordProbeOperationProblems() { doAnswer(addProblem("Bad costing")).when(service).checkKitCostings(any(), any()); User user = EntityFactory.getUser(); ProbeOperationRequest request = new ProbeOperationRequest("optype", LocalDateTime.now(), - List.of(new ProbeOperationLabware("Alpha", "Beta", SlideCosting.SGP, List.of()))); + List.of(new ProbeOperationLabware("Alpha", "Beta", SlideCosting.SGP, null, List.of()))); Matchers.assertValidationException(() -> service.recordProbeOperation(user, request), List.of("Bad labware", "Bad op type", "Bad work", "Bad probe", "Bad time", "Bad costing")); ArgumentCaptor> streamCaptor = Matchers.streamCaptor(); @@ -159,7 +169,7 @@ public void testRecordProbeOperationProblems() { static Stream recordProbeOperationArgs() { User user = EntityFactory.getUser(); ProbeOperationRequest emptyRequest = new ProbeOperationRequest(); - ProbeOperationLabware pol = new ProbeOperationLabware("STAN-A1", "SGP-1", SlideCosting.SGP, List.of()); + ProbeOperationLabware pol = new ProbeOperationLabware("STAN-A1", "SGP-1", SlideCosting.SGP, null, List.of()); LocalDateTime time = LocalDateTime.now(); ProbeOperationRequest completeRequest = new ProbeOperationRequest("optype", time, List.of(pol)); return Arrays.stream(new Object[][] { @@ -237,6 +247,36 @@ public void testValidateOpType(String opName, boolean exists, boolean inplace, b assertProblem(problems, expectedProblem); } + @Test + public void testCheckSamplePrepReagentLots() { + when(mockSamplePrepReagentLotValidator.validate(any(), any())).then(invocation -> { + String string = invocation.getArgument(0); + if (string.indexOf('!') < 0) { + return true; + } + Consumer consumer = invocation.getArgument(1); + consumer.accept("Bad lot: " + string); + return false; + }); + String[] lots = {null, "", " ", " Good ", "Bad!"}; + String[] sanitisedLots = {null, null, null, "Good", "Bad!"}; + List pols = Arrays.stream(lots) + .map(lot -> new ProbeOperationLabware("bc", null, null, lot, null)) + .toList(); + List problems = new ArrayList<>(1); + service.checkSamplePrepReagentLots(problems, pols); + assertProblem(problems, "Bad lot: Bad!"); + for (int i = 0; i < pols.size(); i++) { + ProbeOperationLabware pol = pols.get(i); + String lot = sanitisedLots[i]; + assertEquals(lot, pol.getSamplePrepReagentLot()); + if (lot!=null) { + verify(mockSamplePrepReagentLotValidator).validate(eq(lot), any()); + } + } + verify(mockSamplePrepReagentLotValidator, never()).validate(isNull(), any()); + } + @ParameterizedTest @ValueSource(booleans={false,true}) public void testCheckKitCostings(boolean anyMissing) { @@ -250,7 +290,7 @@ public void testCheckKitCostings(boolean anyMissing) { expectedProblem = null; } List pols = costings.stream() - .map(costing -> new ProbeOperationLabware(null, null, costing, null)) + .map(costing -> new ProbeOperationLabware(null, null, costing, null, null)) .toList(); List problems = new ArrayList<>(anyMissing ? 1 : 0); service.checkKitCostings(problems, pols); @@ -261,10 +301,10 @@ public void testCheckKitCostings(boolean anyMissing) { public void testValidateProbes() { List pols = List.of( new ProbeOperationLabware("BC1", "SGP1", SlideCosting.SGP, - List.of(new ProbeLot("p1", "lot1", 1, SlideCosting.SGP), + null, List.of(new ProbeLot("p1", "lot1", 1, SlideCosting.SGP), new ProbeLot("p2", "lot2", 2, SlideCosting.SGP))), new ProbeOperationLabware("BC2", "SGP2", SlideCosting.Faculty, - List.of(new ProbeLot("p2", "lot3", 3, SlideCosting.Faculty), + null, List.of(new ProbeLot("p2", "lot3", 3, SlideCosting.Faculty), new ProbeLot("p3", "lot4", 4, SlideCosting.SGP))) ); List probes = IntStream.range(1, 4) @@ -285,9 +325,9 @@ public void testValidateProbes() { public void testValidateNoProbes() { List pols = List.of( new ProbeOperationLabware("BC1", "SGP1", SlideCosting.SGP, - List.of(new ProbeLot("p1", "lot1", 1, SlideCosting.Faculty))), + null, List.of(new ProbeLot("p1", "lot1", 1, SlideCosting.Faculty))), new ProbeOperationLabware("BC2", "SGP2", SlideCosting.Faculty, - List.of()) + null, List.of()) ); List probes = List.of(new ProbePanel(1, "p1")); when(mockProbePanelRepo.findAllByNameIn(any())).thenReturn(probes); @@ -312,7 +352,7 @@ public void testValidateNoProbes() { }) public void testValidateProbes_problems(String probeName, String lot, Integer plex, SlideCosting costing, String expectedProblem) { ProbeOperationLabware pol = new ProbeOperationLabware("BC", "SGP1", SlideCosting.SGP, - List.of(new ProbeLot(probeName, lot, plex, costing))); + null, List.of(new ProbeLot(probeName, lot, plex, costing))); when(mockProbeLotValidator.validate(any(), any())).then(invocation -> { String lotArg = invocation.getArgument(0); if (lotArg.indexOf('!')<0) { @@ -376,6 +416,7 @@ public void testPerform() { doReturn(lwOps).when(service).makeOps(any(), any(), any(), any(), any()); doNothing().when(service).linkWork(any(), any(), any()); doNothing().when(service).saveProbes(any(), any(), any(), any()); + doNothing().when(service).saveSamplePrepReagentLots(any(), any(), any()); doNothing().when(service).saveKitCostings(any(), any(), any()); OperationResult opres = new OperationResult(List.of(), List.of()); doReturn(opres).when(service).assembleResult(any(), any(), any()); @@ -384,6 +425,7 @@ public void testPerform() { verify(service).makeOps(user, opType, pols, lwMap, time); verify(service).linkWork(pols, lwOps, workMap); verify(service).saveProbes(pols, lwOps, lwMap, ppMap); + verify(service).saveSamplePrepReagentLots(pols, lwMap, lwOps); verify(service).saveKitCostings(pols, lwMap, lwOps); verify(service).assembleResult(pols, lwMap, lwOps); } @@ -394,8 +436,8 @@ public void testMakeOps(boolean hasTime) { User user = EntityFactory.getUser(); OperationType optype = EntityFactory.makeOperationType("opname", null); List pols = List.of( - new ProbeOperationLabware("STAN-1", null, SlideCosting.SGP, null), - new ProbeOperationLabware("STAN-2", null, SlideCosting.Faculty, null) + new ProbeOperationLabware("STAN-1", null, SlideCosting.SGP, null, null), + new ProbeOperationLabware("STAN-2", null, SlideCosting.Faculty, null, null) ); LabwareType lt = EntityFactory.getTubeType(); UCMap lwMap = UCMap.from(Labware::getBarcode, EntityFactory.makeEmptyLabware(lt, "STAN-1"), @@ -426,9 +468,9 @@ public void testMakeOps(boolean hasTime) { @Test public void testLinkWork() { List pols = List.of( - new ProbeOperationLabware("STAN-1", "SGP1", SlideCosting.SGP, null), - new ProbeOperationLabware("STAN-2", "SGP1", SlideCosting.SGP, null), - new ProbeOperationLabware("STAN-3", "SGP2", SlideCosting.Faculty, null) + new ProbeOperationLabware("STAN-1", "SGP1", SlideCosting.SGP, null, null), + new ProbeOperationLabware("STAN-2", "SGP1", SlideCosting.SGP, null, null), + new ProbeOperationLabware("STAN-3", "SGP2", SlideCosting.Faculty, null, null) ); Operation[] ops = IntStream.range(1, 4) .mapToObj(i -> { @@ -467,8 +509,8 @@ public void testSaveKitCostings() { final String noteName = ProbeServiceImp.KIT_COSTING_NAME; List pols = List.of( - new ProbeOperationLabware(lw1.getBarcode(), null, SlideCosting.SGP, null), - new ProbeOperationLabware(lw2.getBarcode(), null, SlideCosting.Faculty, null) + new ProbeOperationLabware(lw1.getBarcode(), null, SlideCosting.SGP, null, null), + new ProbeOperationLabware(lw2.getBarcode(), null, SlideCosting.Faculty, null, null) ); service.saveKitCostings(pols, lwMap, opMap); @@ -483,11 +525,11 @@ public void testSaveKitCostings() { @Test public void testSaveProbes() { List pols = List.of( - new ProbeOperationLabware("STAN-1", null, SlideCosting.Faculty, List.of( + new ProbeOperationLabware("STAN-1", null, SlideCosting.Faculty, null, List.of( new ProbeLot("probe1", "lot1", 1, SlideCosting.SGP), new ProbeLot("probe2", "lot2", 2, SlideCosting.SGP) )), - new ProbeOperationLabware("STAN-2", null, SlideCosting.SGP, List.of( + new ProbeOperationLabware("STAN-2", null, SlideCosting.SGP, null, List.of( new ProbeLot("probe1", "lot3", 3, SlideCosting.Faculty) )) ); @@ -517,14 +559,43 @@ public void testSaveProbes() { )); } + @ParameterizedTest + @ValueSource(booleans={false,true}) + public void testSaveSamplePrepReagentLots(boolean anyLots) { + String[] lots = anyLots ? new String[] {null, "Alpha", "Beta", null, "Alpha"} : new String[] { null, null }; + LabwareType lt = EntityFactory.getTubeType(); + Labware[] lws = Arrays.stream(lots).map(unused -> EntityFactory.makeEmptyLabware(lt)).toArray(Labware[]::new); + Operation[] ops = Arrays.stream(lots).map(unused -> new Operation()).toArray(Operation[]::new); + UCMap lwMap = UCMap.from(Labware::getBarcode, lws); + UCMap opMap = new UCMap<>(ops.length); + Zip.enumerateForEach(Arrays.stream(ops), (i, op) -> { + op.setId(100+i); + opMap.put(lws[i].getBarcode(), op); + }); + + List pols = Zip.map(Arrays.stream(lws), Arrays.stream(lots), + (lw, lot) -> new ProbeOperationLabware(lw.getBarcode(), null, null, lot, null) + ).toList(); + service.saveSamplePrepReagentLots(pols, lwMap, opMap); + if (anyLots) { + List expectedNotes = IntStream.range(0, lots.length) + .filter(i -> lots[i] != null) + .mapToObj(i -> new LabwareNote(null, lws[i].getId(), ops[i].getId(), SAMPLE_PREP_REAGENT_LOT_NAME, lots[i])) + .toList(); + verify(mockNoteRepo).saveAll(expectedNotes); + } else { + verifyNoInteractions(mockNoteRepo); + } + } + @Test public void testAssembleResult() { LabwareType lt = EntityFactory.getTubeType(); Labware[] labware = { EntityFactory.makeEmptyLabware(lt, "STAN-1"), EntityFactory.makeEmptyLabware(lt, "STAN-2")}; UCMap lwMap = UCMap.from(Labware::getBarcode, labware); List pols = List.of( - new ProbeOperationLabware("STAN-1", null, SlideCosting.SGP, null), - new ProbeOperationLabware("STAN-2", null, SlideCosting.Faculty, null) + new ProbeOperationLabware("STAN-1", null, SlideCosting.SGP, null, null), + new ProbeOperationLabware("STAN-2", null, SlideCosting.Faculty, null, null) ); Operation[] ops = { new Operation(), new Operation()}; ops[0].setId(1); diff --git a/src/test/resources/graphql/recordprobeoperation.graphql b/src/test/resources/graphql/recordprobeoperation.graphql index f6e9ff89..a66cadcf 100644 --- a/src/test/resources/graphql/recordprobeoperation.graphql +++ b/src/test/resources/graphql/recordprobeoperation.graphql @@ -16,6 +16,7 @@ mutation { costing: SGP }] workNumber: "SGP1" + samplePrepReagentLot: "123456" }] }) { labware { barcode }