diff --git a/app/models/labware.rb b/app/models/labware.rb index c08689f9f9..ce89f01210 100644 --- a/app/models/labware.rb +++ b/app/models/labware.rb @@ -369,7 +369,19 @@ def obtain_retention_instructions end def lookup_labwhere_location - lookup_labwhere(machine_barcode) || lookup_labwhere(human_barcode) + lookups = [lookup_labwhere(machine_barcode), lookup_labwhere(human_barcode)] + + return latest_location(lookups) if lookups.any? { |l| l&.dig(:location).present? } + return 'Not found - There is a problem with Labwhere' if lookups.any? { |l| l&.dig(:error) } + + nil + end + + def latest_location(lookups) + lookups.compact + .select { |l| l[:location].present? } + .max_by { |l| l[:updated_at] || Time.zone.at(0) } + .fetch(:location) end def lookup_labwhere(barcode) @@ -378,9 +390,23 @@ def lookup_labwhere(barcode) rescue StandardError => e # rescue LabWhereClient::LabwhereException => e Rails.logger.error { e } - return 'Not found - There is a problem with Labwhere' + return { error: true, message: e.message } end - info_from_labwhere.location.location_info if info_from_labwhere.present? && info_from_labwhere.location.present? + + return nil unless info_from_labwhere.present? && info_from_labwhere.location.present? + + { + location: info_from_labwhere.location.location_info, + updated_at: parse_labwhere_timestamp(info_from_labwhere.updated_at) + } + end + + def parse_labwhere_timestamp(timestamp) + return if timestamp.blank? + + Time.zone.parse(timestamp) + rescue ArgumentError + nil end end # rubocop:enable Metrics/ClassLength diff --git a/lib/lab_where_client.rb b/lib/lab_where_client.rb index 2e04600a4c..d679bbd22a 100644 --- a/lib/lab_where_client.rb +++ b/lib/lab_where_client.rb @@ -91,7 +91,7 @@ def self.included(base) class Labware < Endpoint endpoint_name 'labwares' - attr_reader :barcode, :location + attr_reader :barcode, :location, :updated_at def self.find_by_barcode(barcode) return nil if barcode.blank? @@ -102,7 +102,8 @@ def self.find_by_barcode(barcode) def initialize(params) @barcode = params['barcode'] - @location = Location.new(params['location']) + @updated_at = params['updated_at'] + @location = Location.new(params['location']) if params['location'].present? end end @@ -155,7 +156,7 @@ def valid? class Location < Endpoint endpoint_name 'locations' - attr_reader :name, :parentage, :barcode + attr_reader :name, :parentage, :barcode, :updated_at def self.find_by_barcode(barcode) return nil if barcode.blank? @@ -168,6 +169,7 @@ def initialize(params) @name = params['name'] @parentage = params['parentage'] @barcode = params['barcode'] + @updated_at = params['updated_at'] end def location_info diff --git a/spec/lib/lab_where_client_spec.rb b/spec/lib/lab_where_client_spec.rb index 436312d388..856ef630df 100644 --- a/spec/lib/lab_where_client_spec.rb +++ b/spec/lib/lab_where_client_spec.rb @@ -83,4 +83,26 @@ end end end + + describe LabWhereClient::Labware do + let(:params) do + { + 'barcode' => '123456', + 'updated_at' => '2026-05-06 10:46:00', + 'location' => { + 'name' => 'f1', + 'parentage' => 'Sanger / Ogilvie / AA216', + 'barcode' => 'lw-f1-26214', + 'updated_at' => '2023-06-06 16:26:00' + } + } + end + + it 'captures labware and location updated_at values from the API payload' do + labware = described_class.new(params) + + expect(labware.updated_at).to eq('2026-05-06 10:46:00') + expect(labware.location.updated_at).to eq('2023-06-06 16:26:00') + end + end end diff --git a/spec/models/labware_spec.rb b/spec/models/labware_spec.rb index 718e2b7b2b..061c679979 100644 --- a/spec/models/labware_spec.rb +++ b/spec/models/labware_spec.rb @@ -133,6 +133,66 @@ plate = create(:plate) expect(plate.storage_location).to eq('Not found - There is a problem with Labwhere') end + + it 'chooses the newer location when both machine and human barcode lookups return values' do + plate = create(:plate) + + allow(LabWhereClient::Labware).to receive(:find_by_barcode).with(plate.machine_barcode).and_return( + LabWhereClient::Labware.new( + { + 'barcode' => plate.machine_barcode, + 'updated_at' => '2026-05-06 10:46:00', + 'location' => { + 'name' => 'f1', + 'parentage' => 'Sanger / Ogilvie / AA216', + 'barcode' => 'lw-f1-26214', + 'updated_at' => '2026-05-06 10:46:00' + } + } + ) + ) + allow(LabWhereClient::Labware).to receive(:find_by_barcode).with(plate.human_barcode).and_return( + LabWhereClient::Labware.new( + { + 'barcode' => plate.human_barcode, + 'updated_at' => '2026-05-07 10:46:00', + 'location' => { + 'name' => 'f2', + 'parentage' => 'Sanger / Ogilvie / AA216', + 'barcode' => 'lw-f2-26214', + 'updated_at' => '2026-05-07 10:46:00' + } + } + ) + ) + + expect(plate.labwhere_location).to eq('Sanger / Ogilvie / AA216 - f2') + end + + it 'falls back to human barcode location when machine barcode lookup raises an error' do + plate = create(:plate) + + allow(LabWhereClient::Labware).to receive(:find_by_barcode).with(plate.machine_barcode).and_raise( + StandardError, + 'Timed out reading data from server' + ) + allow(LabWhereClient::Labware).to receive(:find_by_barcode).with(plate.human_barcode).and_return( + LabWhereClient::Labware.new( + { + 'barcode' => plate.human_barcode, + 'updated_at' => '2026-05-07 10:46:00', + 'location' => { + 'name' => 'f2', + 'parentage' => 'Sanger / Ogilvie / AA216', + 'barcode' => 'lw-f2-26214', + 'updated_at' => '2026-05-07 10:46:00' + } + } + ) + ) + + expect(plate.labwhere_location).to eq('Sanger / Ogilvie / AA216 - f2') + end end context 'when retrieving retention instructions' do diff --git a/spec/models/location_report_spec.rb b/spec/models/location_report_spec.rb index cec00c5b81..c8524c3308 100644 --- a/spec/models/location_report_spec.rb +++ b/spec/models/location_report_spec.rb @@ -247,14 +247,17 @@ before do [ - [plate_1.machine_barcode.to_s, 'Shelf 1', locn_prefix], - [plate_2.machine_barcode.to_s, 'Shelf 2', locn_prefix], - [plate_3.machine_barcode.to_s, 'Shelf 3', locn_prefix], - [tube_1.machine_barcode.to_s, 'Shelf 4', locn_prefix], + [plate_1.machine_barcode.to_s, plate_1.human_barcode.to_s, 'Shelf 1', locn_prefix], + [plate_2.machine_barcode.to_s, plate_2.human_barcode.to_s, 'Shelf 2', locn_prefix], + [plate_3.machine_barcode.to_s, plate_3.human_barcode.to_s, 'Shelf 3', locn_prefix], + [tube_1.machine_barcode.to_s, tube_1.human_barcode.to_s, 'Shelf 4', locn_prefix], # Tube racks are filtered out so this should not show up - [tube_rack.machine_barcode.to_s, 'Shelf 5', locn_prefix] - ].each do |lw_barcode, lw_locn_name, lw_locn_parentage| - stub_lwclient_labware_find_by_bc(lw_barcode:, lw_locn_name:, lw_locn_parentage:) + [tube_rack.machine_barcode.to_s, tube_rack.human_barcode.to_s, 'Shelf 5', locn_prefix] + ].each do |machine_bc, human_bc, lw_locn_name, lw_locn_parentage| + stub_lwclient_labware_find_by_bc(lw_barcode: machine_bc, lw_locn_name: lw_locn_name, + lw_locn_parentage: lw_locn_parentage) + stub_lwclient_labware_find_by_bc(lw_barcode: human_bc, lw_locn_name: lw_locn_name, + lw_locn_parentage: lw_locn_parentage) end plate_1_set_long_term_storage @@ -391,6 +394,11 @@ lw_locn_name: 'Shelf 1', lw_locn_parentage: locn_prefix ) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_4.human_barcode.to_s, + lw_locn_name: 'Shelf 1', + lw_locn_parentage: locn_prefix + ) end it_behaves_like 'a successful report' @@ -537,6 +545,11 @@ stub_lwclient_locn_children(location_barcode, []) stub_lwclient_locn_labwares(location_barcode, [p1]) stub_lwclient_labware_find_by_bc(p1) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_1.human_barcode, + lw_locn_name: 'Shelf 1', + lw_locn_parentage: locn_prefix + ) plate_1_set_long_term_storage end @@ -613,6 +626,21 @@ stub_lwclient_labware_find_by_bc(p1) stub_lwclient_labware_find_by_bc(p2) stub_lwclient_labware_find_by_bc(t1) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_1.human_barcode, + lw_locn_name: 'Box 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1" + ) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_2.human_barcode, + lw_locn_name: 'Box 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1" + ) + stub_lwclient_labware_find_by_bc( + lw_barcode: tube_1.human_barcode, + lw_locn_name: 'Box 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1" + ) plate_1_set_long_term_storage tube_1_set_long_term_storage @@ -670,6 +698,11 @@ stub_lwclient_locn_children(locn_lvl2_b1[:locn_barcode], []) stub_lwclient_locn_labwares(locn_lvl2_b1[:locn_barcode], [p1]) stub_lwclient_labware_find_by_bc(p1) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_1.human_barcode, + lw_locn_name: 'Box 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1" + ) # set up Shelf 1 - Box 2 with one labware and no sub-locations p2 = { @@ -681,6 +714,11 @@ stub_lwclient_locn_children(locn_lvl2_b2[:locn_barcode], []) stub_lwclient_locn_labwares(locn_lvl2_b2[:locn_barcode], [p2]) stub_lwclient_labware_find_by_bc(p2) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_2.human_barcode, + lw_locn_name: 'Box 2', + lw_locn_parentage: "#{locn_prefix} - Shelf 1" + ) plate_1_set_long_term_storage end @@ -728,6 +766,11 @@ stub_lwclient_locn_children(location_barcode, [locn_lvl2_t1]) stub_lwclient_locn_labwares(location_barcode, [p1]) stub_lwclient_labware_find_by_bc(p1) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_1.human_barcode, + lw_locn_name: 'Shelf 1', + lw_locn_parentage: locn_prefix + ) # set up Shelf 1 - Tray 1 with 1 labware and 1 sub-location locn_lvl3_b1 = { @@ -744,6 +787,11 @@ stub_lwclient_locn_children(locn_lvl2_t1[:locn_barcode], [locn_lvl3_b1]) stub_lwclient_locn_labwares(locn_lvl2_t1[:locn_barcode], [p2]) stub_lwclient_labware_find_by_bc(p2) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_2.human_barcode, + lw_locn_name: 'Tray 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1" + ) # set up Shelf 1 - Tray 1 - Box 1 with 2 labware and no sub-locations p3 = { @@ -761,6 +809,16 @@ stub_lwclient_locn_labwares(locn_lvl3_b1[:locn_barcode], [p3, t1]) stub_lwclient_labware_find_by_bc(p3) stub_lwclient_labware_find_by_bc(t1) + stub_lwclient_labware_find_by_bc( + lw_barcode: plate_3.human_barcode, + lw_locn_name: 'Box 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1 - Tray 1" + ) + stub_lwclient_labware_find_by_bc( + lw_barcode: tube_1.human_barcode, + lw_locn_name: 'Box 1', + lw_locn_parentage: "#{locn_prefix} - Shelf 1 - Tray 1" + ) plate_1_set_long_term_storage plate_3_return_after_two_years