From a47badfa87d9d6e9d6e3086c783ed0452267fc2e Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Tue, 31 Mar 2026 15:21:26 +0100 Subject: [PATCH 1/4] feat(aviti): adds custom primer kit used field to batch task --- .../descriptors/002_element_aviti_descriptors.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/default_records/descriptors/002_element_aviti_descriptors.yml b/config/default_records/descriptors/002_element_aviti_descriptors.yml index a19cc4f61e..38adc63a0a 100644 --- a/config/default_records/descriptors/002_element_aviti_descriptors.yml +++ b/config/default_records/descriptors/002_element_aviti_descriptors.yml @@ -12,6 +12,17 @@ Final Loading Concentration (pM): kind: Text required: false sorter: 1 +Custom primer kit used: + name: Custom primer kit used + task: Loading + workflow: Element Aviti + kind: Selection + value: Yes + selection: + Yes: Yes + No: No + required: true + sorter: 1 "200mM Tris PH7 lot#": name: "200mM Tris PH7 lot#" task: Loading From 820bbb1fb35697a9d09fa26f683b90319c4cf728 Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Tue, 31 Mar 2026 15:25:58 +0100 Subject: [PATCH 2/4] feat(aviti): adds quant method used field to batch task --- .../descriptors/002_element_aviti_descriptors.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/default_records/descriptors/002_element_aviti_descriptors.yml b/config/default_records/descriptors/002_element_aviti_descriptors.yml index 38adc63a0a..2baedf9188 100644 --- a/config/default_records/descriptors/002_element_aviti_descriptors.yml +++ b/config/default_records/descriptors/002_element_aviti_descriptors.yml @@ -1,3 +1,14 @@ +Quant method used: + name: Quant method used + task: Loading + workflow: Element Aviti + kind: Selection + value: Tapestation + selection: + Tapestation: Tapestation + Tapestation & qPCR: Tapestation & qPCR + required: true + sorter: -1 Pipette Carousel: name: Pipette Carousel task: Loading From 438386f833c11907c6a7d0d4101a97e041c5a2ee Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Thu, 2 Apr 2026 15:24:06 +0100 Subject: [PATCH 3/4] feat(eseq_flowcell): adds custom eseq flowcell class --- app/models/api/messages/eseq_flowcell_io.rb | 65 ++++++++ .../element_aviti_sequencing_pipeline.rb | 2 +- app/models/sequencing_request.rb | 1 + .../api/messages/eseq_flowcell_io_spec.rb | 143 ++++++++++++++++++ .../element_aviti_sequencing_pipeline_spec.rb | 2 +- 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 app/models/api/messages/eseq_flowcell_io.rb create mode 100644 spec/models/api/messages/eseq_flowcell_io_spec.rb diff --git a/app/models/api/messages/eseq_flowcell_io.rb b/app/models/api/messages/eseq_flowcell_io.rb new file mode 100644 index 0000000000..85388bc333 --- /dev/null +++ b/app/models/api/messages/eseq_flowcell_io.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true +# Generates warehouse messages describing an Element Aviti flowcell. +# This is a subset of FlowcellIo containing only fields required by Eseq. +class Api::Messages::EseqFlowcellIo < Api::Base + self.includes = { + requests: [ + { + target_asset: { + aliquots: [ + :library, + :bait_library, + :primer_panel, + { tag: :tag_group, tag2: :tag_group, sample: :uuid_object, study: :uuid_object } + ] + } + }, + :batch_request + ] + } + + # The following modules add methods onto the relevant models, which are used below in generation of the + # eseq flowcel MLWH message. + # Included in SequencingRequest model + module LaneExtensions + def self.included(base) + base.class_eval do + def quant_method_used + detect_descriptor('Quant method used') + end + + def custom_primer_kit_used + detect_descriptor('Custom primer kit used') + end + end + end + end + + renders_model(::Batch) + + map_attribute_to_json_attribute(:id, 'flowcell_id') + map_attribute_to_json_attribute(:updated_at) + + with_nested_has_many_association(:requests, as: :lanes) do + map_attribute_to_json_attribute(:position, 'lane') + map_attribute_to_json_attribute(:mx_library, 'id_pool_lims') + map_attribute_to_json_attribute(:lane_identifier, 'entity_id_lims') + map_attribute_to_json_attribute(:request_purpose, 'purpose') + map_attribute_to_json_attribute(:quant_method_used) + map_attribute_to_json_attribute(:custom_primer_kit_used) + + with_nested_has_many_association(:lane_samples, as: :samples) do + with_association(:tag) { map_attribute_to_json_attribute(:oligo, 'tag_sequence') } + with_association(:tag2) { map_attribute_to_json_attribute(:oligo, 'tag2_sequence') } + map_attribute_to_json_attribute(:library_type, 'pipeline_id_lims') + with_association(:bait_library) { map_attribute_to_json_attribute(:name, 'bait_name') } + map_attribute_to_json_attribute(:insert_size_from, 'requested_insert_size_from') + map_attribute_to_json_attribute(:insert_size_to, 'requested_insert_size_to') + with_association(:sample) { map_attribute_to_json_attribute(:uuid, 'sample_uuid') } + with_association(:study) { map_attribute_to_json_attribute(:uuid, 'study_uuid') } + with_association(:primer_panel) { map_attribute_to_json_attribute(:name, 'primer_panel') } + with_association(:library) { map_attribute_to_json_attribute(:external_identifier, 'id_library_lims') } + map_attribute_to_json_attribute(:aliquot_type, 'entity_type') + end + end +end diff --git a/app/models/element_aviti_sequencing_pipeline.rb b/app/models/element_aviti_sequencing_pipeline.rb index 3408e0f875..d5270d1eee 100644 --- a/app/models/element_aviti_sequencing_pipeline.rb +++ b/app/models/element_aviti_sequencing_pipeline.rb @@ -5,6 +5,6 @@ class ElementAvitiSequencingPipeline < SequencingPipeline def post_release_batch(batch, _user) # Same logic as the superclass, but with a different Messenger root batch.assets.compact.uniq.each(&:index_aliquots) - Messenger.create!(target: batch, template: 'FlowcellIo', root: 'eseq_flowcell') + Messenger.create!(target: batch, template: 'EseqFlowcellIo', root: 'eseq_flowcell') end end diff --git a/app/models/sequencing_request.rb b/app/models/sequencing_request.rb index 6ed23cd350..d6d1fc42f0 100644 --- a/app/models/sequencing_request.rb +++ b/app/models/sequencing_request.rb @@ -3,6 +3,7 @@ class SequencingRequest < CustomerRequest include Api::Messages::FlowcellIo::LaneExtensions + include Api::Messages::EseqFlowcellIo::LaneExtensions class_attribute :flowcell_identifier diff --git a/spec/models/api/messages/eseq_flowcell_io_spec.rb b/spec/models/api/messages/eseq_flowcell_io_spec.rb new file mode 100644 index 0000000000..7fcbd32480 --- /dev/null +++ b/spec/models/api/messages/eseq_flowcell_io_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::Messages::EseqFlowcellIo do + let(:message) { described_class.to_hash(sequencing_batch.reload) } + + context 'with a batch' do + let(:sequencing_pipeline) { create(:element_aviti_sequencing_pipeline) } + let(:sequencing_batch) { create(:sequencing_batch, pipeline: sequencing_pipeline) } + + let!(:request) do + create( + :complete_sequencing_request, + asset: mx_tube1, + batch: sequencing_batch, + target_asset: lane1, + request_type: request_type, + event_descriptors: request_data + ) + end + + let(:mx_tube1) { create(:multiplexed_library_tube, sample_count: 1) } + let(:request_type) { sequencing_pipeline.request_types.first } + + let(:lane1) do + create(:lane, aliquots: mx_tube1.aliquots.map(&:dup)).tap(&:index_aliquots) + end + + let(:tags) { lane1.aliquots.map(&:tag) } + let(:tag2s) { lane1.aliquots.map(&:tag2) } + let(:aliquots) { lane1.aliquots } + + let(:expected_json) do + { + 'flowcell_id' => sequencing_batch.id, + 'lanes' => [ + { + 'lane' => 1, + 'id_pool_lims' => mx_tube1.human_barcode, + 'entity_id_lims' => lane1.id, + 'purpose' => 'standard', + 'quant_method_used' => 'Tapestation', + 'custom_primer_kit_used' => 'No', + 'samples' => [ + { + 'tag_sequence' => tags[0].oligo, + 'tag2_sequence' => tag2s[0].oligo, + 'pipeline_id_lims' => 'Standard', + 'bait_name' => aliquots[0].bait_library.name, + 'requested_insert_size_from' => 100, + 'requested_insert_size_to' => 200, + 'sample_uuid' => aliquots[0].sample.uuid, + 'study_uuid' => aliquots[0].study.uuid, + 'primer_panel' => aliquots[0].primer_panel.name, + 'id_library_lims' => aliquots[0].library.human_barcode, + 'entity_type' => 'library_indexed' + } + ] + } + ] + } + end + + let(:request_data) do + { + 'Quant method used' => 'Tapestation', + 'Custom primer kit used' => 'No' + } + end + + context 'with all request data' do + it 'generates valid json' do + expect(message.as_json).to include_json(expected_json) + end + end + + context 'with some missing request data' do + let(:request_data) do + { + 'Quant method used' => 'Tapestation', + 'Custom primer kit used' => nil + } + end + + it 'generates valid json' do + # Should have an empty primer kit used + expected_json['lanes'][0]['custom_primer_kit_used'] = nil + + expect(message.as_json).to include_json(expected_json) + end + end + + context 'with updated events' do + before do + create( + :lab_event, + eventful: request, + batch: request.batch, + descriptors: { + 'Quant method used' => 'Tapestation & qPCR', + 'Custom primer kit used' => 'Yes' + } + ) + end + + let(:expected_json) do + { + 'flowcell_id' => sequencing_batch.id, + 'lanes' => [ + { + 'lane' => 1, + 'id_pool_lims' => mx_tube1.human_barcode, + 'entity_id_lims' => lane1.id, + 'purpose' => 'standard', + 'quant_method_used' => 'Tapestation & qPCR', + 'custom_primer_kit_used' => 'Yes', + 'samples' => [ + { + 'tag_sequence' => tags[0].oligo, + 'tag2_sequence' => tag2s[0].oligo, + 'pipeline_id_lims' => 'Standard', + 'bait_name' => aliquots[0].bait_library.name, + 'requested_insert_size_from' => 100, + 'requested_insert_size_to' => 200, + 'sample_uuid' => aliquots[0].sample.uuid, + 'study_uuid' => aliquots[0].study.uuid, + 'primer_panel' => aliquots[0].primer_panel.name, + 'id_library_lims' => aliquots[0].library.human_barcode, + 'entity_type' => 'library_indexed' + } + ] + } + ] + } + end + + it 'generates valid json' do + expect(message.as_json).to include_json(expected_json) + end + end + end +end diff --git a/spec/models/element_aviti_sequencing_pipeline_spec.rb b/spec/models/element_aviti_sequencing_pipeline_spec.rb index 09a997ed52..5031bffdc3 100644 --- a/spec/models/element_aviti_sequencing_pipeline_spec.rb +++ b/spec/models/element_aviti_sequencing_pipeline_spec.rb @@ -11,7 +11,7 @@ pipeline.post_release_batch(batch, create(:user)) expect(Messenger).to have_received(:create!).with( - hash_including(target: batch, template: 'FlowcellIo', root: 'eseq_flowcell') + hash_including(target: batch, template: 'EseqFlowcellIo', root: 'eseq_flowcell') ) end end From 9eb3c7625417e94965ec718e500e9302c1b52c0c Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Thu, 2 Apr 2026 15:45:52 +0100 Subject: [PATCH 4/4] feat(eseq_flowcell): adds controls --- app/models/api/messages/eseq_flowcell_io.rb | 12 ++++++++++++ .../api/messages/eseq_flowcell_io_spec.rb | 17 ++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/models/api/messages/eseq_flowcell_io.rb b/app/models/api/messages/eseq_flowcell_io.rb index 85388bc333..84fb6847d4 100644 --- a/app/models/api/messages/eseq_flowcell_io.rb +++ b/app/models/api/messages/eseq_flowcell_io.rb @@ -61,5 +61,17 @@ def custom_primer_kit_used with_association(:library) { map_attribute_to_json_attribute(:external_identifier, 'id_library_lims') } map_attribute_to_json_attribute(:aliquot_type, 'entity_type') end + + # The following methods come from the Aliquot model or the relevant module above. + # They are included in the MLWH message under 'controls'. + with_nested_has_many_association(:controls) do + with_association(:tag) { map_attribute_to_json_attribute(:oligo, 'tag_sequence') } + with_association(:tag2) { map_attribute_to_json_attribute(:oligo, 'tag2_sequence') } + map_attribute_to_json_attribute(:library_type, 'pipeline_id_lims') + with_association(:sample) { map_attribute_to_json_attribute(:uuid, 'sample_uuid') } + with_association(:study) { map_attribute_to_json_attribute(:uuid, 'study_uuid') } + with_association(:library) { map_attribute_to_json_attribute(:external_identifier, 'id_library_lims') } + map_attribute_to_json_attribute(:control_aliquot_type, 'entity_type') + end end end diff --git a/spec/models/api/messages/eseq_flowcell_io_spec.rb b/spec/models/api/messages/eseq_flowcell_io_spec.rb index 7fcbd32480..0688954bd9 100644 --- a/spec/models/api/messages/eseq_flowcell_io_spec.rb +++ b/spec/models/api/messages/eseq_flowcell_io_spec.rb @@ -24,9 +24,14 @@ let(:request_type) { sequencing_pipeline.request_types.first } let(:lane1) do - create(:lane, aliquots: mx_tube1.aliquots.map(&:dup)).tap(&:index_aliquots) + create(:lane, aliquots: mx_tube1.aliquots.map(&:dup)).tap do |lane| + lane.labware.parents << phix + lane.index_aliquots + end end + let(:phix) { create(:spiked_buffer, :tube_barcode, tag_option: 'Dual') } + let(:tags) { lane1.aliquots.map(&:tag) } let(:tag2s) { lane1.aliquots.map(&:tag2) } let(:aliquots) { lane1.aliquots } @@ -56,6 +61,16 @@ 'id_library_lims' => aliquots[0].library.human_barcode, 'entity_type' => 'library_indexed' } + ], + 'controls' => [ + { + 'tag_sequence' => 'TGTGCAGC', + 'tag2_sequence' => 'ACTGATGT', + 'pipeline_id_lims' => nil, + 'sample_uuid' => phix.aliquots[0].sample.uuid, + 'id_library_lims' => phix.human_barcode, + 'entity_type' => 'library_indexed_spike' + } ] } ]