#!/usr/bin/env ruby # frozen_string_literal: true require "alma/electronic" # Contains batch processing utils for Alma Electronic APIs. # # This class and its methods are used to iterate over Alma Electronic IDs to # process and fetch Alma electronic objects via the Alma Electronic APIs. The # https calls are logged and can be used to rerun the batch process without # making any further http calls or to just rerun parts of the full batch. module Alma class Electronic::BatchUtils attr_reader :notes, :type # @param [Hash] options The options to create a batch instance. # @option [true, false] :chain true indicates a new instance of self returned. # @option [Array] :ids List of collection ids. # @option ["collection", "service", "portfolio"] :type The Alma Electronic object type. # @option [String] :tag A string used to tag the batch session. # @option [Logger] :logger A logger used t def initialize(options = {}) options ||= options {} @chain = options.fetch(:chain, false) @ids = options.fetch(:ids, []) @type = options.fetch(:type, "collection") @tag = options.fetch(:tag, Time.now.to_s) @@logger = options[:logger] || Logger.new("log/electronic_batch_process.log") end def get_collection_notes(ids = nil, options = {}) ids ||= @ids get_notes(options.merge(ids: make_collection_ids(ids), type: "collection")) end def get_service_notes(ids = nil, options = {}) ids ||= @ids get_notes(options.merge(ids: get_service_ids(ids, options), type: "service")) end def get_portfolio_notes(ids = nil, options = {}) ids ||= @ids get_notes(options.merge(ids: get_portfolio_ids(ids, options, type: "portfolio"))) end def get_notes(options = {}) options ||= {} chain = options.fetch(:chain, @chain) ids = options[:ids] || (chain ? build_ids(options) : @ids) type = options.fetch(:type, @type) tag = options.fetch(:tag, @tag) @notes = ids.inject({}) do |notes, params| id = get_id(type, params) start = Time.now begin item = Alma::Electronic.get(params) rescue StandardError => e item = { "error" => e.message } end data = ["error", "authentication_note", "public_note"].reduce({}) do |acc, field| acc[field] = item[field] if item[field].present? acc end unavailable = item.dig("service_temporarily_unavailable", "value") if unavailable == "1" || unavailable == "true" data.merge!(item.slice("service_temporarily_unavailable", "service_unavailable_date", "service_unavailable_reason")) end if data.present? log(params.merge(data).merge(type: type, start: start, tag: tag)) notes[id] = data unless data["error"].present? end notes end self.class.new(options.merge( chain: chain, ids: ids, type: type, tag: tag, notes: notes, logger: @@logger, )) end def get_service_ids(ids = @ids, options = {}) tag = options.fetch(:tag, @tag) start = Time.now make_collection_ids(ids) .map { |id| id.merge(type: "services") } .inject([]) do |service_ids, params| params.merge!(tag: tag) begin item = Alma::Electronic.get(params) if item["errorList"] log params.merge(item["errorList"]) .merge(start: start) else item["electronic_service"].each { |service| service_id = { service_id: service["id"].to_s } service_ids << params.slice(:collection_id) .merge(service_id) log params.merge(service_id) .merge(start: start) } end rescue StandardError => e log params.merge("error" => e.message) .merge(start: start) end service_ids end end # Builds the notes object using the logs. def build_notes(options = {}) options ||= {} type ||= options.fetch(:type, "collection") get_logged_items(options) .select { |item| item.slice("authentication_note", "public_note").values.any?(&:present?) } .inject({}) do |nodes, item| id = item["#{type}_id"] nodes.merge(id => item.slice("authentication_note", "public_note")) end end # Builds list of ids from logs based on failed attempts. # Useful for rebuilding part of collection. def build_failed_ids(options = {}) successful_ids = build_successful_ids(options) get_logged_items(options) .select { |item| item.slice("authentication_note", "public_note").values.all?(&:nil?) } .map { |item| item["collection_id"] } .select { |id| !successful_ids.include? id } .uniq end # Builds list of ids from logs based on successful attempts. # Useful for verifying that failed ids have always failed. def build_successful_ids(options = {}) get_logged_items(options) .select { |item| item.slice("authentication_note", "public_note").values.present? } .map { |item| item["collection_id"] } .uniq end # Builds a list of all ids for a specific session. # Useful for analytics purpose or rebuilds. def build_ids(options = {}) build_failed_ids(options) + build_successful_ids(options) end def print_notes(options = {}) options ||= {} chain = options.fetch(:chain, @chain) notes = options[:notes] || chain ? build_notes(options) : @notes type = options.fetch(:type, @type) tag = options.fetch(:tag, @tag) filename = options.fetch(:filename, "spec/fixtures/#{type}_notes.json") File.open(filename, "w") do |file| file.write(JSON.pretty_generate(notes)) end self.class.new(options.merge( chain: chain, notes: notes, type: type, tag: tag, logger: @@logger, )) end private def log(params) if defined?(LogUtils) LogUtils.json_request_logger(@@logger, params) else @@logger.info(params) end end def get_id(type, params = {}) id = "#{type}_id".to_sym params[id] end def make_collection_ids(ids = @ids) ids.map { |id| id.class == Hash ? id : { collection_id: id.to_s } } end # Returns JSON parsed list of logged items def get_logged_items(options = {}) options ||= {} type ||= options.fetch(:type, "collection") tag ||= options.fetch(:tag, @tag) filename = (@@logger.instance_variable_get :@logdev).filename File.readlines(filename) .map { |log| log.match(/{.*}/).to_s } .select(&:present?) .map { |json| JSON.parse(json) } .select { |item| item["tag"] == tag } .select { |item| item["type"] == type } end end end