• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pulibrary / orangelight / b4033a01-cc31-437d-91fc-e98f0e1a7f31

22 Nov 2023 03:16PM UTC coverage: 95.408% (-0.07%) from 95.475%
b4033a01-cc31-437d-91fc-e98f0e1a7f31

Pull #3848

circleci

christinach
Allow Alma guest users to request all - In Library Use - from ReCAP
Pull Request #3848: Allow Alma guest users to request all - In Library Use - from ReCAP

5714 of 5989 relevant lines covered (95.41%)

1412.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.8
/app/models/requests/request.rb
1
# frozen_string_literal: true
2
require 'faraday'
3✔
3

4
module Requests
3✔
5
  class Request
3✔
6
    attr_accessor :email
3✔
7
    attr_accessor :user_name
3✔
8
    attr_reader :system_id
3✔
9
    attr_reader :source
3✔
10
    attr_reader :mfhd
3✔
11
    attr_reader :patron
3✔
12
    attr_reader :doc
3✔
13
    attr_reader :requestable
3✔
14
    attr_reader :requestable_unrouted
3✔
15
    attr_reader :holdings
3✔
16
    attr_reader :location
3✔
17
    attr_reader :location_code
3✔
18
    attr_reader :items
3✔
19
    attr_reader :pick_ups
3✔
20
    alias default_pick_ups pick_ups
3✔
21
    delegate :ctx, :openurl_ctx_kev, to: :@ctx_obj
3✔
22
    delegate :eligible_for_library_services?, to: :patron
3✔
23

24
    include Requests::Bibdata
3✔
25
    include Requests::Scsb
3✔
26

27
    # @option opts [String] :system_id A bib record id or a special collection ID value
28
    # @option opts [Fixnum] :mfhd alma holding id
29
    # @option opts [Patron] :patron current Patron object
30
    # @option opts [String] :source represents system that directed user to request form. i.e.
31
    def initialize(system_id:, mfhd:, patron: nil, source: nil)
3✔
32
      @system_id = system_id
386✔
33
      @doc = SolrDocument.new(solr_doc(system_id))
386✔
34
      @holdings = JSON.parse(doc[:holdings_1display] || '{}')
386✔
35
      # scsb items are the only ones that come in without a MFHD parameter from the catalog now
36
      # set it for them, because they only ever have one location
37
      @mfhd = mfhd || @holdings.keys.first
386✔
38
      @patron = patron
386✔
39
      @source = source
386✔
40
      ### These should be re-factored
41
      @location_code = @holdings[@mfhd]["location_code"] if @holdings[@mfhd].present?
386✔
42
      @location = load_location
386✔
43
      @items = load_items
386✔
44
      @pick_ups = build_pick_ups
386✔
45
      @requestable_unrouted = build_requestable
386✔
46
      @requestable = route_requests(@requestable_unrouted)
386✔
47
      @ctx_obj = Requests::SolrOpenUrlContext.new(solr_doc: doc)
386✔
48
    end
49

50
    delegate :user, to: :patron
3✔
51

52
    # Is this a partner system id
53
    def partner_system_id?
3✔
54
      return true if /^SCSB-\d+/.match?(system_id.to_s)
384✔
55
    end
56

57
    def requestable?
3✔
58
      requestable.size.positive?
96✔
59
    end
60

61
    def single_aeon_requestable?
3✔
62
      requestable.size == 1 && first_filtered_requestable&.services&.include?('aeon')
106✔
63
    end
64

65
    def first_filtered_requestable
3✔
66
      requestable&.first
187✔
67
    end
68

69
    # Does this request object have any pageable items?
70
    def any_pageable?
3✔
71
      services = requestable.map(&:services).flatten
4✔
72
      services.uniq!
4✔
73
      services.include? 'paging'
4✔
74
    end
75

76
    # Does this request object have any available copies?
77
    def any_loanable_copies?
3✔
78
      requestable_unrouted.any? do |requestable|
378✔
79
        !(requestable.charged? || (requestable.aeon? || !requestable.circulates? || requestable.partner_holding? || requestable.on_reserve?))
817✔
80
      end
81
    end
82

83
    def any_enumerated?
3✔
84
      requestable_unrouted.any?(&:enumerated?)
1✔
85
    end
86

87
    def route_requests(requestable_items)
3✔
88
      routed_requests = []
386✔
89
      return [] if requestable_items.blank?
386✔
90
      any_loanable = any_loanable_copies?
374✔
91
      requestable_items.each do |requestable|
374✔
92
        router = Requests::Router.new(requestable:, user: patron.user, any_loanable:)
2,427✔
93
        routed_requests << router.routed_request
2,427✔
94
      end
95
      routed_requests
374✔
96
    end
97

98
    def serial?
3✔
99
      doc[:format].present? && doc[:format].include?('Journal')
341✔
100
    end
101

102
    def recap?
3✔
103
      return false if location.blank?
292✔
104
      location[:remote_storage] == "recap_rmt"
280✔
105
    end
106

107
    def all_items_online?
3✔
108
      requestable.map(&:online?).reduce(:&)
87✔
109
    end
110

111
    # returns nil if there are no attached items
112
    # if mfhd set returns only items associated with that mfhd
113
    # if no mfhd returns items sorted by mfhd
114
    def load_items
3✔
115
      return nil if thesis? || numismatics?
386✔
116

117
      return nil if too_many_items?
345✔
118

119
      mfhd_items = if @mfhd && serial?
342✔
120
                     load_serial_items
63✔
121
                   else
122
                     # load_items_by_bib_id
123
                     load_items_by_mfhd
279✔
124
                   end
125
      mfhd_items.empty? ? nil : mfhd_items.with_indifferent_access
342✔
126
    end
127

128
    def thesis?
3✔
129
      doc[:holdings_1display].present? && parse_json(doc[:holdings_1display]).key?('thesis')
549✔
130
    end
131

132
    def numismatics?
3✔
133
      doc[:holdings_1display].present? && parse_json(doc[:holdings_1display]).key?('numismatics')
529✔
134
    end
135

136
    # returns basic metadata for display on the request from via solr_doc values
137
    # Fields to return all keys are arrays
138
    ## Add more fields here as needed
139
    def display_metadata
3✔
140
      {
141
        title: doc["title_citation_display"],
191✔
142
        author: doc["author_citation_display"],
143
        isbn: doc["isbn_s"]&.values_at(0),
144
        date: doc["pub_date_display"]
145
      }
146
    end
147

148
    def language
3✔
149
      doc["language_iana_s"]&.first
269✔
150
    end
151

152
    # should probably happen in the initializer
153
    def build_pick_ups
3✔
154
      pick_up_locations = []
386✔
155
      Requests::BibdataService.delivery_locations.each_value do |pick_up|
386✔
156
        pick_up_locations << { label: pick_up["label"], gfa_pickup: pick_up["gfa_pickup"], pick_up_location_code: pick_up["library"]["code"] || 'firestone', staff_only: pick_up["staff_only"] } if pick_up["pickup_location"] == true
8,878✔
157
      end
158
      # pick_up_locations.sort_by! { |loc| loc[:label] }
159
      sort_pick_ups(pick_up_locations)
386✔
160
    end
161

162
    def ill_eligible?
3✔
163
      requestable.any? { |r| r.services.include? 'ill' }
×
164
    end
165

166
    def isbn_numbers?
3✔
167
      if doc.key? 'isbn_s'
2✔
168
        true
1✔
169
      else
170
        false
1✔
171
      end
172
    end
173

174
    def isbn_numbers
3✔
175
      doc['isbn_s']
×
176
    end
177

178
    def other_id
3✔
179
      doc['other_id_s'].first
36✔
180
    end
181

182
    def scsb_location
3✔
183
      doc['location_code_s'].first
115✔
184
    end
185

186
    def off_site?
3✔
187
      return false if location['library'].nil? || location['library']['code'].nil?
×
188
      library_code = location[:library][:code]
×
189
      library_code == 'recap' || library_code == 'marquand' || library_code == 'annex'
×
190
    end
191

192
    def too_many_items?
3✔
193
      holding = holdings[@mfhd]
349✔
194
      items = holding.try(:[], "items")
349✔
195
      return false if items.nil?
349✔
196

197
      return true if items.count > 500
296✔
198

199
      false
293✔
200
    end
201

202
    private
3✔
203

204
      ### builds a list of possible requestable items
205
      # returns a collection of requestable objects or nil
206
      # @return [Array<Requests::Requestable>] array containing Requests::Requestables
207
      def build_requestable
3✔
208
        return [] if doc._source.blank?
386✔
209
        if partner_system_id?
384✔
210
          build_scsb_requestable
35✔
211
        elsif !items.nil?
349✔
212
          # for single aeon item, ends up in this statement
213
          build_requestable_with_items
292✔
214
        else
215
          # for too many aeon items, ends up in this statement
216
          build_requestable_from_data
57✔
217
        end
218
      end
219

220
      def availability_data(id)
3✔
221
        @availability_data ||= items_by_id(id, scsb_owning_institution(scsb_location))
115✔
222
      end
223

224
      # @return [Array<Requests::Requestable>] array containing Requests::Requestables
225
      def build_scsb_requestable
3✔
226
        requestable_items = []
35✔
227
        ## scsb processing
228
        ## If mfhd present look for only that
229
        ## sort items by keys
230
        ## send query for availability by barcode
231
        ## overlay availability to the 'status' field
232
        ## make sure other fields map to the current data model for item in requestable
233
        ## adjust router to understand SCSB status
234
        holdings.each do |id, values|
35✔
235
          requestable_items = build_holding_scsb_items(id:, values:, availability_data: availability_data(other_id), requestable_items:)
35✔
236
        end
237
        requestable_items
35✔
238
      end
239

240
      # @return [Array<Requests::Requestable>] array containing Requests::Requestables
241
      def build_holding_scsb_items(id:, values:, availability_data:, requestable_items:)
3✔
242
        return requestable_items if values['items'].nil?
35✔
243
        barcodesort = build_barcode_sort(items: values['items'], availability_data:)
35✔
244
        barcodesort.each_value do |item|
35✔
245
          item['location_code'] = location_code
67✔
246
          params = build_requestable_params(item: item.with_indifferent_access, holding: { id.to_sym.to_s => holdings[id] },
67✔
247
                                            location:)
248
          requestable_items << Requests::Requestable.new(**params)
67✔
249
        end
250
        requestable_items
35✔
251
      end
252

253
      def build_barcode_sort(items:, availability_data:)
3✔
254
        barcodesort = {}
115✔
255
        items.each do |item|
115✔
256
          item[:status_label] = status_label(item:, availability_data:)
434✔
257
          barcodesort[item['barcode']] = item
434✔
258
        end
259
        availability_data.each do |item|
115✔
260
          barcode_item = barcodesort[item['itemBarcode']]
75✔
261
          next if barcode_item.nil? || barcode_item["status_source"] == "work_order" || item['errorMessage'].present?
75✔
262
          barcode_item['status_label'] = item['itemAvailabilityStatus']
63✔
263
          barcode_item['status'] = nil
63✔
264
        end
265
        barcodesort
115✔
266
      end
267

268
      def status_label(item:, availability_data:)
3✔
269
        if item["status_source"] != "work_order" && availability_data.empty?
434✔
270
          "Unavailable"
320✔
271
        elsif item["status_source"] != "work_order" && item[:status_label] == 'Item in place' && availability_data.size == 1 && availability_data.first['errorMessage'] == "Bib Id doesn't exist in SCSB database."
114✔
272
          "In Process"
2✔
273
        else
274
          item[:status_label]
112✔
275
        end
276
      end
277

278
      # @return [Array<Requests::Requestable>] array containing Requests::Requestables
279
      def build_requestable_with_items
3✔
280
        requestable_items = []
292✔
281
        barcodesort = {}
292✔
282
        barcodesort = build_barcode_sort(items: items[mfhd], availability_data: availability_data(system_id)) if recap?
292✔
283
        items.each do |holding_id, mfhd_items|
292✔
284
          next if mfhd != holding_id
292✔
285
          requestable_items = build_requestable_from_mfhd_items(requestable_items:, holding_id:, mfhd_items:, barcodesort:)
292✔
286
        end
287
        requestable_items.compact
292✔
288
      end
289

290
      # @return [Array<Requests::Requestable>] array containing Requests::Requestables or empty array
291
      def build_requestable_from_data
3✔
292
        return if doc[:holdings_1display].nil?
57✔
293
        @mfhd ||= 'thesis' if thesis?
57✔
294
        @mfhd ||= 'numismatics' if numismatics?
57✔
295
        return [] if holdings[@mfhd].blank?
57✔
296

297
        [build_requestable_from_holding(@mfhd, holdings[@mfhd].with_indifferent_access)]
56✔
298
      end
299

300
      def build_requestable_from_mfhd_items(requestable_items:, holding_id:, mfhd_items:, barcodesort:)
3✔
301
        if !mfhd_items.empty?
292✔
302
          mfhd_items.each do |item|
259✔
303
            requestable_items << build_requestable_mfhd_item(requestable_items, holding_id, item, barcodesort)
2,286✔
304
          end
305
        else
306
          requestable_items << build_requestable_from_holding(holding_id, holdings[holding_id])
33✔
307
        end
308
        requestable_items.compact
292✔
309
      end
310

311
      def build_requestable_mfhd_item(_requestable_items, holding_id, item, barcodesort)
3✔
312
        return if item['on_reserve'] == 'Y'
2,286✔
313

314
        item_loc = item_current_location(item)
2,279✔
315
        current_location = get_current_location(item_loc:)
2,279✔
316
        item['status_label'] = barcodesort[item['barcode']][:status_label] unless barcodesort.empty?
2,279✔
317
        calculate_holding = if item["in_temp_library"] && item["temp_location_code"] != "RES_SHARE$IN_RS_REQ"
2,279✔
318
                              { holding_id.to_sym.to_s => holdings[item_loc] }
9✔
319
                            else
320
                              { holding_id.to_sym.to_s => holdings[holding_id] }
2,270✔
321
                            end
322
        params = build_requestable_params(
2,279✔
323
          item: item.with_indifferent_access,
324
          holding: calculate_holding,
325
          location: current_location
326
        )
327
        Requests::Requestable.new(**params)
2,279✔
328
      end
329

330
      def get_current_location(item_loc:)
3✔
331
        if item_loc != location_code
2,279✔
332
          @temp_locations ||= {}
11✔
333
          @temp_locations[item_loc] = get_location_data(item_loc) if @temp_locations[item_loc].blank?
11✔
334
          @temp_locations[item_loc]
11✔
335
        else
336
          location
2,268✔
337
        end
338
      end
339

340
      # This method will always return a Requestable object where .item is a NullItem, because we don't pass an item in
341
      def build_requestable_from_holding(holding_id, holding)
3✔
342
        return if holding.blank?
89✔
343
        params = build_requestable_params(holding: { holding_id.to_sym.to_s => holding }, location:)
81✔
344
        Requests::Requestable.new(**params)
81✔
345
      end
346

347
      def load_location
3✔
348
        return if location_code.nil?
386✔
349
        location = get_location_data(location_code)
371✔
350
        location[:delivery_locations] = sort_pick_ups(location[:delivery_locations]) if location[:delivery_locations]&.present?
371✔
351
        location
371✔
352
      end
353

354
      def build_requestable_params(params)
3✔
355
        {
356
          bib: doc,
2,427✔
357
          holding: params[:holding],
358
          item: params[:item],
359
          location: build_requestable_location(params),
360
          patron:
361
        }
362
      end
363

364
      def build_requestable_location(params)
3✔
365
        location = params[:location]
2,427✔
366
        location["delivery_locations"] = build_delivery_locations(location["delivery_locations"]) if location["delivery_locations"].present?
2,427✔
367
        location
2,427✔
368
      end
369

370
      def build_delivery_locations(delivery_locations)
3✔
371
        delivery_locations.map do |loc|
2,287✔
372
          pick_up_code = loc["library"]["code"] if loc["library"].present?
7,703✔
373
          pick_up_code ||= 'firestone'
7,703✔
374
          loc.merge("pick_up_location_code" => pick_up_code) { |_key, v1, _v2| v1 }
14,432✔
375
        end
376
      end
377

378
      # Not sure why this method exists
379
      def load_serial_items
3✔
380
        mfhd_items = {}
63✔
381
        items_as_json = items_by_mfhd(@system_id, @mfhd)
63✔
382
        unless items_as_json.empty?
63✔
383
          items_with_symbols = items_to_symbols(items_as_json)
50✔
384
          mfhd_items[@mfhd] = items_with_symbols
50✔
385
        end
386
        # else
387
        #   empty_mfhd = items_by_bib(@system_id)
388
        #   mfhd_items[@mfhd] = [empty_mfhd[@mfhd]]
389
        # end
390
        mfhd_items
63✔
391
      end
392

393
      ## this method should be the only place we load item availability
394
      def load_items_by_mfhd
3✔
395
        mfhd_items = {}
279✔
396
        mfhd_items[@mfhd] = items_by_mfhd(@system_id, @mfhd)
279✔
397
        # items_by_mfhd(@system_id, @mfhd).each do |item_info|
398
        #  mfhd_items[item_info['id']] = load_item_for_holding(holding_id: @mfhd, item_info: item_info)
399
        # end
400
        mfhd_items
279✔
401
      end
402

403
      # def load_items_by_bib_id
404
      #   mfhd_items = {}
405
      #   items_by_bib(@system_id).each do |holding_id, item_info|
406
      #     next if @mfhd != holding_id
407
      #     mfhd_items[holding_id] = load_item_for_holding(holding_id: holding_id, item_info: item_info)
408
      #   end
409
      #   mfhd_items
410
      # end
411

412
      # def load_item_for_holding(holding_id:, item_info:)
413
      #   # new check needed here
414
      #   if item_info[:more_items] == false
415
      #     if item_info[:status].starts_with?('On-Order') || item_info[:status].starts_with?('Pending Order')
416
      #       [item_info]
417
      #     elsif item_info[:status].starts_with?('Online')
418
      #       [item_info]
419
      #     else
420
      #       ## we don't need to call this again
421
      #       items_to_symbols(items_by_mfhd(@system_id, holding_id))
422
      #     end
423
      #   else
424
      #     ## we don't need to call this again
425
      #     # items_to_symbols(items_by_mfhd(@system_id, holding_id))
426
      #     items_to_symbols([item_info])
427
      #   end
428
      # end
429

430
      def items_to_symbols(items = [])
3✔
431
        items_with_symbols = []
50✔
432
        items.each do |item|
50✔
433
          items_with_symbols << item.with_indifferent_access
1,842✔
434
        end
435
        items_with_symbols
50✔
436
      end
437

438
      def item_current_location(item)
3✔
439
        if item['in_temp_library']
2,279✔
440
          item['temp_location_code']
11✔
441
        else
442
          item['location']
2,268✔
443
        end
444
      end
445
  end
446
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc