app/models/labware_creators/plate_split_to_tube_racks.rb
# frozen_string_literal: true
require_dependency 'form'
require_dependency 'labware_creators/base'
module LabwareCreators
# Handles the creation of up to 2 child racks of tubes from a single parent 96-well plate.
# Intended for use in a PBMC cell extraction pipeline.
#
# There will typically be one rack of tubes for 'contingency' and one for 'sequencing' (this rack is
# optional).
#
# The parent plate contains multiple copies of material prepared from the same sample. One well instance
# of each sample will go into a tube in the 'sequencing' rack (if present), and any remaining copies
# will go into tubes in the 'contingency' rack.
#
# If after the initial preparation the users feel they need more contingency tubes, then they will go
# back to prepare more material for that sample from an earlier step in the pipeline and create just
# 'contingency' tubes at this step.
#
# Inputs:
# 1) The parent plate - This plate contains multiple groups of wells containing the same samples e.g.
# there may be 3 wells with sample 1, 3 with sample 2, 3 with sample 3 etc. The number of copies of
# each sample is not known in advance.
# The first of these parent wells will be transferred into a tube in the 'sequencing' rack if it is
# present, and any remaining parent wells for the same sample will be transferred into tubes in the
# 'contingency' rack.
# 2) Child tube rack scan CSV files - these are scans of racks of 2D tube barcodes from the 'contingency'
# and 'sequencing' tube racks, to allow us to know the position and barcode of each available tube.
# On the upload screen displays counts of how many tubes are needed (e.g. To perform this transfer you
# will either need 20 sequencing and 40 contingency tubes, or 60 contingency tubes.)
#
# Validations - Error message to users if any of these are not met:
# 1) The user must always upload a scan file for the 'contingency' rack tube barcodes, whereas the
# 'sequencing' rack file is optional.
# 2) The scanned child tube barcodes must be unique and must not already exist in the system i.e. they are
# new unused empty tubes. List any that do already exist with rack type and location.
# 3) The number of tubes available in the racks must be correct for the number of parent wells being
# transferred. e.g. if there are 20 distinct samples in the parent and 40 additional copies (60 wells
# total), then there must be 20 tubes in the first rack and 40 in the second rack.
#
# rubocop:disable Metrics/ClassLength
class PlateSplitToTubeRacks < Base
include LabwareCreators::CustomPage
include SupportParent::PlateOnly
self.page = 'plate_split_to_tube_racks'
self.attributes += %i[sequencing_file contingency_file]
attr_accessor :sequencing_file, :contingency_file
attr_reader :child_sequencing_tubes, :child_contingency_tubes
validates_nested :well_filter
# Don't create the tubes until at least the contingency file has been uploaded
validate :validate_file_presence
# N.B. contingency file is required, sequencing file is optional
validates_nested :sequencing_csv_file, if: :sequencing_file
validates_nested :contingency_csv_file, if: :contingency_file
# validations for duplications between the two tube rack files
validate :check_tube_rack_barcodes_differ_between_files
validate :check_tube_barcodes_differ_between_files
# validate there are sufficient tubes in the racks for the number of parent wells
validate :must_have_correct_number_of_tubes_in_rack_files, if: :contingency_file
# validate that the tube barcodes do not already exist in the system
validate :tube_barcodes_are_unique?
PARENT_PLATE_INCLUDES =
'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection'
def validate_file_presence
if sequencing_file.blank?
errors.add(:base, "Sequencing file can't be blank")
elsif contingency_file.present? && sequencing_file.blank?
errors.add(:base, 'If contingency_file is present, sequencing_file must also be present.')
end
end
def save
# NB. need the && true!!
super && upload_tube_rack_files && true
end
# v2 api is used to select the parent plate
def parent
@parent ||= Sequencescape::Api::V2.plate_with_custom_includes(PARENT_PLATE_INCLUDES, uuid: parent_uuid)
end
# v1 api is used to upload the tube rack scan files and create the tubes
def parent_v1
@parent_v1 ||= api.plate.find(parent_uuid)
end
# Returns the list of wells of the parent labware.
# In column order (A1, B1, C1 etc.)
# Used in WellFilter.
#
# @return [Array<Well>] The wells of the parent labware.
def labware_wells
parent.wells_in_columns
end
# Creates child sequencing and contingency tubes, performs transfers.
#
# @return [Boolean] true if the child tubes were created successfully.
def create_labware!
@child_sequencing_tubes = create_child_sequencing_tubes
@child_contingency_tubes = create_child_contingency_tubes
add_child_tube_metadata
perform_transfers
true
end
# Creates a single child sequencing tube for each parent well containing a unique sample.
#
# @return [Array<Tube>] The child sequencing tubes.
def create_child_sequencing_tubes
create_tubes(sequencing_tube_purpose_uuid, parent_wells_for_sequencing.length, sequencing_tube_attributes)
end
# Creates a child contingency tube for each parent well not already assigned to a sequencing tube.
#
# @return [Array<Tube>] The child contingency tubes.
def create_child_contingency_tubes
return [] if require_sequencing_tubes_only?
create_tubes(contingency_tube_purpose_uuid, parent_wells_for_contingency.length, contingency_tube_attributes)
end
# Creates transfer requests for the given transfer request attributes and performs the transfers.
#
# @return [void]
def perform_transfers
api.transfer_request_collection.create!(user: user_uuid, transfer_requests: transfer_request_attributes)
end
# We will create multiple child tubes, so redirect to the parent plate
def redirection_target
parent
end
# Display the children tab in the plate view so we see the child tubes listed.
def anchor
'relatives_tab'
end
# Returns a CsvFile object for the sequencing tube rack scan CSV file, or nil if the file doesn't exist.
#
# @return [CsvFile, nil] A CsvFile object for the sequencing tube rack scan CSV file, or nil if the file
# doesn't exist.
def sequencing_csv_file
@sequencing_csv_file ||=
CommonFileHandling::CsvFileForTubeRackWithRackBarcode.new(sequencing_file) if sequencing_file
end
# Returns a CsvFile object for the contingency tube rack scan CSV file, or nil if the file doesn't exist.
#
# @return [CsvFile, nil] A CsvFile object for the contingency tube rack scan CSV file, or nil if the file
# doesn't exist.
def contingency_csv_file
@contingency_csv_file ||=
CommonFileHandling::CsvFileForTubeRackWithRackBarcode.new(contingency_file) if contingency_file
end
# Returns the number of unique sample UUIDs for the parent wells after applying the current well filter.
#
# @return [Integer] The number of unique sample UUIDs.
def num_parent_unique_samples
@num_parent_unique_samples ||= parent_uniq_sample_uuids.length
end
# Returns the number of parent wells after applying the current well filter.
#
# @return [Integer] The number of filtered parent wells.
def num_parent_wells
@num_parent_wells ||= well_filter.filtered.length
end
# Validation to compare the tube rack barcodes in the two files to check for duplication
#
# Sets errors if the tube rack barcodes are the same
def check_tube_rack_barcodes_differ_between_files
return unless contingency_file_valid? && sequencing_file_valid?
return unless same_tube_rack_barcode?
errors.add(
:contingency_csv_file,
'The tube rack barcodes within the contingency and sequencing files must be different'
)
end
def same_tube_rack_barcode?
seq_tube_rack = extract_tube_rack_barcode(sequencing_csv_file)
cont_tube_rack = extract_tube_rack_barcode(contingency_csv_file)
seq_tube_rack == cont_tube_rack
end
def extract_tube_rack_barcode(file)
file.position_details.values.first['tube_rack_barcode']
end
# Validation to compare the tube barcodes in the two files to check for duplication
#
# Sets errors if the tube rack barcodes are the same
def check_tube_barcodes_differ_between_files
return unless sequencing_file_valid? && contingency_file_valid?
seq_barcodes = extract_barcodes(sequencing_csv_file)
cont_barcodes = extract_barcodes(contingency_csv_file)
duplicate_barcodes = seq_barcodes & cont_barcodes
return if duplicate_barcodes.empty?
errors.add(
:contingency_csv_file,
"Tube barcodes are duplicated across contingency and sequencing files (#{duplicate_barcodes.join(', ')})"
)
end
def extract_barcodes(file)
file.position_details.values.pluck('tube_barcode')
end
# Validation that checks if there are the correct number of tubes in the child tube racks for all
# the parent wells.
# This depends on the number of unique samples in the parent plate, and the number of parent wells,
# as well as whether they are using both sequencing tubes and contingency tubes or just contingency.
#
# Sets errors if there are insufficient or too many tubes.
def must_have_correct_number_of_tubes_in_rack_files
return unless files_valid?
unless require_sequencing_tubes_only?
add_error_if_wrong_number_of_tubes(
:contingency_csv_file,
num_contingency_tubes,
num_parent_wells - num_parent_unique_samples
)
end
add_error_if_wrong_number_of_tubes(:sequencing_csv_file, num_sequencing_tubes, num_parent_unique_samples)
end
# Checks the files passed their validations
def files_valid?
return sequencing_file_valid? if require_sequencing_tubes_only?
contingency_file_valid? && sequencing_file_valid?
end
def sequencing_file_valid?
sequencing_file.present? && sequencing_csv_file&.valid?
end
def contingency_file_valid?
contingency_file.present? && contingency_csv_file&.valid?
end
# Adds an error when there are insufficient or too many tubes in the given file
def add_error_if_wrong_number_of_tubes(file, num_tubes, required_tubes)
# msg if too many tubes
errors.add(file, 'contains more tubes than needed') if num_tubes > required_tubes
# msg if not enough tubes
errors.add(file, 'contains insufficient tubes') unless num_tubes >= required_tubes
end
# Validation that the tube barcodes are unique and do not already exist in the system.
# NB. this checks all the tube barcodes in the uploaded tube rack scan files, not just the
# ones that will be used.
#
# @return [void]
def tube_barcodes_are_unique?
check_tube_rack_scan_file(sequencing_csv_file, 'Sequencing') if sequencing_file
check_tube_rack_scan_file(contingency_csv_file, 'Contingency') if contingency_file
end
# Checks if the tube barcodes in the given tube rack file already exist in the LIMS.
#
# @param tube_rack_file [CsvFile] The tube rack file to check.
# @param msg_prefix [String] The prefix to use for error messages.
# @return [void] Adds errors to the model if the tube barcodes are not unique.
def check_tube_rack_scan_file(tube_rack_file, msg_prefix)
tube_rack_file.position_details.each do |tube_posn, tube_details|
foreign_barcode = tube_details['tube_barcode']
tube_in_db = Sequencescape::Api::V2::Tube.find_by(barcode: foreign_barcode)
next if tube_in_db.blank?
msg = "#{msg_prefix} tube barcode #{foreign_barcode} (at rack position #{tube_posn}) already exists in the LIMS"
errors.add(:tube_rack_file, msg)
end
end
private
# Returns a new instance of WellFilter with the current object as the creator.
#
# @return [WellFilter] A new instance of WellFilter.
def well_filter
@well_filter ||= WellFilter.new(creator: self)
end
# Returns the ancestor stock tubes for the parent wells.
#
# @return [Array<Sequencescape::Api::V2::Tube>] The ancestor stock tubes.
def ancestor_stock_tubes
@ancestor_stock_tubes ||= locate_ancestor_tubes
end
# Locates the ancestor stock tubes for the parent wells.
#
# @return [Hash{String => Sequencescape::Api::V2::Tube}] A hash of ancestor stock tubes, keyed by sample UUID.
def locate_ancestor_tubes
purpose_name = purpose_config[:ancestor_stock_tube_purpose_name]
ancestor_results = parent.ancestors.where(purpose_name: purpose_name)
return {} if ancestor_results.blank?
ancestor_results.each_with_object({}) do |ancestor_result, tube_list|
tube = Sequencescape::Api::V2::Tube.find_by(uuid: ancestor_result.uuid)
tube_sample_uuid = tube.aliquots.first.sample.uuid
tube_list[tube_sample_uuid] = tube if tube_sample_uuid.present?
end
end
# Returns an array of unique sample UUIDs for the parent wells after applying the current well filter.
#
# @return [Array<String>] An array of unique sample UUIDs.
def parent_uniq_sample_uuids
well_filter.filtered.map { |well, _ignore| well.aliquots.first.sample.uuid }.uniq
end
# Returns the number of sequencing tubes in the sequencing CSV file.
#
# @return [Integer] The number of sequencing tubes.
def num_sequencing_tubes
@num_sequencing_tubes ||= sequencing_csv_file&.position_details&.length || 0
end
# Returns the number of contingency tubes in the contingency CSV file.
#
# @return [Integer] The number of contingency tubes.
def num_contingency_tubes
@num_contingency_tubes ||= contingency_csv_file&.position_details&.length || 0
end
# Uploads the sequencing and contingency tube rack scan CSV files to the parent plate using api v1.
#
# @return [void]
def upload_tube_rack_files
unless require_sequencing_tubes_only?
parent_v1.qc_files.create_from_file!(contingency_file, 'scrna_core_contingency_tube_rack_scan.csv')
end
parent_v1.qc_files.create_from_file!(sequencing_file, 'scrna_core_sequencing_tube_rack_scan.csv')
end
# Returns true if only contingency tubes are required for the parent plate, false otherwise.
#
# @return [Boolean]
def require_sequencing_tubes_only?
contingency_file.blank?
end
# Returns an array of parent wells that should be transferred to sequencing tubes based on the current well filter.
#
# @return [Array<Well>] An array of parent wells.
def parent_wells_for_sequencing
@parent_wells_for_sequencing ||= find_parent_wells_for_sequencing
end
# Returns an array of parent wells that should be transferred to sequencing tubes based on the current well filter,
# or an empty array if only contingency tubes are required for the parent plate.
#
# @return [Array<Well>] An array of parent wells.
def find_parent_wells_for_sequencing
unique_sample_uuids = []
parent_wells_for_seq = []
well_filter.filtered.each do |(well, _ignore)|
sample_uuid = well.aliquots.first.sample.uuid
next if sample_uuid.in?(unique_sample_uuids)
unique_sample_uuids << sample_uuid
parent_wells_for_seq << well
end
parent_wells_for_seq
end
# Returns an array of parent wells that should be transferred to contingency tubes based on the current well filter
# and the wells that will be transferred to sequencing tubes.
#
# @return [Array<Well>] An array of parent wells that should be used for contingency tubes.
def parent_wells_for_contingency
@parent_wells_for_contingency ||=
well_filter.filtered.filter_map { |well, _ignore| well unless parent_wells_for_sequencing.include?(well) }
end
# Creates a specified number of tubes with the given attributes and returns a hash of the created tubes indexed
# by name.
#
# @param tube_purpose_uuid [String] The UUID of the tube purpose to use for the created tubes.
# @param number_of_tubes [Integer] The number of tubes to create.
# @param tube_attributes [Hash] A hash of attributes to use for the created tubes.
# @return [Hash<String, Tube>] A hash of the created tubes indexed by name.
def create_tubes(tube_purpose_uuid, number_of_tubes, tube_attributes)
api
.specific_tube_creation
.create!(
user: user_uuid,
parent: parent_uuid,
child_purposes: [tube_purpose_uuid] * number_of_tubes,
tube_attributes: tube_attributes
)
.children
.index_by(&:name)
end
# Returns the name of the sequencing tube purpose based on the current purpose configuration.
#
# @return [String] The name of the sequencing tube purpose.
def sequencing_tube_purpose_name
@sequencing_tube_purpose_name ||= purpose_config.dig(:creator_class, :args, :child_seq_tube_purpose_name)
end
# Returns the prefix to use for the name of a sequencing tube from the purpose config.
#
# @return [String] The sequencing tube name prefix.
def sequencing_tube_name_prefix
@sequencing_tube_name_prefix ||= purpose_config.dig(:creator_class, :args, :child_seq_tube_name_prefix)
end
# Returns the UUID of the sequencing tube purpose based on the current purpose configuration.
#
# @return [String] The UUID of the sequencing tube purpose.
def sequencing_tube_purpose_uuid
raise "Missing purpose configuration argument 'child_seq_tube_purpose_name'" unless sequencing_tube_purpose_name
Settings.purpose_uuids[sequencing_tube_purpose_name]
end
# Returns the name of the contingency tube purpose based on the current purpose configuration.
#
# @return [String] The name of the contingency tube purpose.
def contingency_tube_purpose_name
@contingency_tube_purpose_name ||= purpose_config.dig(:creator_class, :args, :child_spare_tube_purpose_name)
end
# Returns the prefix to use for the name of a contingency tube from the purpose config.
#
# @return [String] The contingency tube name prefix.
def contingency_tube_name_prefix
@contingency_tube_name_prefix ||= purpose_config.dig(:creator_class, :args, :child_spare_tube_name_prefix)
end
# Returns the UUID of the contingency tube purpose based on the current purpose configuration.
#
# @return [String] The UUID of the contingency tube purpose.
def contingency_tube_purpose_uuid
unless contingency_tube_purpose_name
raise "Missing purpose configuration argument 'child_spare_tube_purpose_name'"
end
Settings.purpose_uuids[contingency_tube_purpose_name]
end
# Returns the human-readable barcode of the ancestor (supplier) source tube for the given sample UUID.
#
# @param sample_uuid [String] The UUID of the sample to find the ancestor tube for.
# @return [String] The human-readable barcode of the ancestor tube.
# @raise [StandardError] If the ancestor tube cannot be found for the given sample UUID.
def ancestor_tube_barcode(sample_uuid)
sample_ancestor_tube = ancestor_stock_tubes[sample_uuid]
if sample_ancestor_tube.blank?
raise StandardError, "Failed to identify ancestor (supplier) source tube for sample uuid #{sample_uuid}"
end
sample_ancestor_tube.human_barcode
end
# Returns a hash of attributes to use for the sequencing tubes.
#
# @return [Hash] A hash of attributes to use for the sequencing tubes.
def sequencing_tube_attributes
@sequencing_tube_attributes ||=
generate_tube_attributes('sequencing', sequencing_csv_file, parent_wells_for_sequencing)
end
# Returns a hash of attributes to use for the contingency tubes.
#
# @return [Hash] A hash of attributes to use for the contingency tubes.
def contingency_tube_attributes
@contingency_tube_attributes ||=
generate_tube_attributes('contingency', contingency_csv_file, parent_wells_for_contingency)
end
# Returns the name prefix for child tubes based on the tube type.
# This method looks up the name prefix in the configuration file based on the tube type.
# If the name prefix is not found, this method raises an error.
# @param tube_type [String] The type of tube to generate attributes for ('sequencing' or 'contingency').
#
# @return [String] The name prefix for the child tubes.
def tube_name_prefix(tube_type)
config_arg = ''
name_prefix =
if tube_type == 'sequencing'
config_arg = 'child_seq_tube_name_prefix'
sequencing_tube_name_prefix
else
config_arg = 'child_spare_tube_name_prefix'
contingency_tube_name_prefix
end
raise "Missing purpose configuration argument '#{config_arg}'" unless name_prefix
name_prefix
end
# Adds a mapping between a well and a tube name to the appropriate hash based on the tube type.
# @param tube_type [String] The type of tube to generate attributes for ('sequencing' or 'contingency').
# @param well [Well] The well to add the mapping for.
# @param tube_name [String] The name of the tube to add the mapping for.
#
# This method adds the mapping to either the `@sequencing_wells_to_tube_names` or to the
# `@contingency_wells_to_tube_names` hash, depending on the tube type.
# If the hash does not exist, this method creates it.
# @return [void]
def add_to_well_to_tube_hash(tube_type, well, tube_name)
if tube_type == 'sequencing'
@sequencing_wells_to_tube_names ||= {}
@sequencing_wells_to_tube_names[well] = tube_name
else
@contingency_wells_to_tube_names ||= {}
@contingency_wells_to_tube_names[well] = tube_name
end
end
# Generates a hash of attributes to use for the tubes based on the
# current purpose configuration and the available tube positions.
# Passes the name for each tube.
# Passes the foreign barcode extracted from the tube rack scan upload for each tube,
# which on the Sequencescape side sets that barcode as the primary.
# @param tube_type [String] The type of tube to generate attributes for.
# @param csv_file [CsvFile] The CSV file containing the tube rack scan data.
# @param wells [Array<Well>] The parent wells to generate attributes for.
#
# @return [Hash] A hash of attributes to use for the contingency tubes.
# rubocop:disable Metrics/AbcSize
def generate_tube_attributes(tube_type, csv_file, wells)
# fetch the available tube positions (i.e. locations of scanned tubes for which we
# have the barcodes) e.g. ["A1", "B1", "D1"]
available_tube_posns = csv_file.position_details.keys
name_prefix = tube_name_prefix(tube_type)
wells
.zip(available_tube_posns)
.map do |well, tube_posn|
sample_uuid = well.aliquots.first.sample.uuid
name_for_details = name_for_details_hash(name_prefix, ancestor_tube_barcode(sample_uuid), tube_posn)
tube_name = name_for(name_for_details)
add_to_well_to_tube_hash(tube_type, well, tube_name)
{ name: tube_name, foreign_barcode: csv_file.position_details[tube_posn]['tube_barcode'] }
end
end
# rubocop:enable Metrics/AbcSize
# Returns a hash of details to use for generating a tube name based on the given prefix,
# stock tube barcode, and destination tube position.
#
# @param prefix [String] The prefix to use for the tube name.
# @param stock_tube_bc [String] The barcode of the stock tube.
# @param dest_tube_posn [String] The position of the destination tube.
# @return [Hash] A hash of details to use for generating a tube name.
def name_for_details_hash(prefix, stock_tube_bc, dest_tube_posn)
{ prefix: prefix, stock_tube_bc: stock_tube_bc, dest_tube_posn: dest_tube_posn }
end
# Generates a human-readable name for a tube based on the given details hash.
# Comprises a prefix, the ancestor source (stock) tube barcode and the destination tube position
#
# @param details [Hash] A hash of details to use for generating the tube name.
# @return [String] A human-readable name for the tube. e.g. 'SEQ:NT12345678:A1'
def name_for(details)
"#{details[:prefix]}:#{details[:stock_tube_bc]}:#{details[:dest_tube_posn]}"
end
# Returns an array of transfer request hashes for the filtered wells and their corresponding child tubes.
#
# @return [Array<Hash>] An array of transfer request hashes.
def transfer_request_attributes
well_filter.filtered.filter_map do |well, additional_parameters|
child_tube = find_child_tube(well)
next unless child_tube
request_hash(well.uuid, child_tube.uuid, additional_parameters)
end
end
# Finds the child tube corresponding to the given well.
#
# @param well [Well] The well to find the child tube for.
# @return [Tube, nil] The child tube corresponding to the given well, or nil if no child tube was found.
def find_child_tube(well)
if require_sequencing_tubes_only?
@child_sequencing_tubes[@sequencing_wells_to_tube_names[well]]
else
@child_sequencing_tubes[@sequencing_wells_to_tube_names[well]] ||
@child_contingency_tubes[@contingency_wells_to_tube_names[well]]
end
end
# Adds metadata to child tubes using details from the parsed sequencing and contingency CSV files.
#
# @return [void]
def add_child_tube_metadata
add_sequencing_tube_metadata
add_contingency_tube_metadata unless require_sequencing_tubes_only?
end
# Adds tube rack barcode and position metadata to child sequencing tubes.
#
# @return [void]
def add_sequencing_tube_metadata
child_sequencing_tubes.each do |child_tube_name, child_tube|
tube_posn = child_tube_name.split(':').last
add_tube_metadata(child_tube, tube_posn, sequencing_csv_file.position_details[tube_posn])
end
end
# Adds tube rack barcode and position metadata to child contingency tubes.
#
# @return [void]
def add_contingency_tube_metadata
child_contingency_tubes.each do |child_tube_name, child_tube|
tube_posn = child_tube_name.split(':').last
add_tube_metadata(child_tube, tube_posn, contingency_csv_file.position_details[tube_posn])
end
end
# Shared method for adding tube rack barcode and position metadata to child tubes.
#
# @param child_tube [Tube] The child tube to add metadata to.
# @param tube_posn [String] The position of the child tube in the tube rack.
# @param tube_details [Hash] The tube details hash from the tube rack scan file.
# @return [void]
def add_tube_metadata(child_tube, tube_posn, tube_details)
LabwareMetadata
.new(user_uuid: user_uuid, barcode: child_tube.barcode.machine)
.update!(tube_rack_barcode: tube_details['tube_rack_barcode'], tube_rack_position: tube_posn)
end
# Generates a transfer request hash for the given source well UUID, target tube UUID, and additional parameters.
#
# @param source_well_uuid [String] The UUID of the source well.
# @param target_tube_uuid [String] The UUID of the target tube.
# @param additional_parameters [Hash] Additional parameters to include in the transfer request hash.
# @return [Hash] A transfer request hash.
def request_hash(source_well_uuid, target_tube_uuid, additional_parameters)
{ 'source_asset' => source_well_uuid, 'target_asset' => target_tube_uuid }.merge(additional_parameters)
end
end
# rubocop:enable Metrics/ClassLength
end