eco-helpers 2.0.25 → 2.0.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -6
  3. data/lib/eco/api/common.rb +0 -1
  4. data/lib/eco/api/common/loaders.rb +2 -0
  5. data/lib/eco/api/common/loaders/base.rb +58 -0
  6. data/lib/eco/api/common/loaders/case_base.rb +33 -0
  7. data/lib/eco/api/common/loaders/error_handler.rb +2 -2
  8. data/lib/eco/api/common/loaders/parser.rb +30 -5
  9. data/lib/eco/api/common/loaders/policy.rb +1 -1
  10. data/lib/eco/api/common/loaders/use_case.rb +1 -1
  11. data/lib/eco/api/common/people/default_parsers/csv_parser.rb +93 -1
  12. data/lib/eco/api/common/people/entries.rb +83 -14
  13. data/lib/eco/api/common/people/entry_factory.rb +10 -9
  14. data/lib/eco/api/common/people/person_attribute_parser.rb +8 -0
  15. data/lib/eco/api/common/people/person_factory.rb +4 -2
  16. data/lib/eco/api/common/people/person_parser.rb +7 -1
  17. data/lib/eco/api/common/people/supervisor_helpers.rb +1 -1
  18. data/lib/eco/api/common/version_patches/ecoportal_api/external_person.rb +0 -8
  19. data/lib/eco/api/common/version_patches/ecoportal_api/internal_person.rb +0 -8
  20. data/lib/eco/api/microcases/set_core_with_supervisor.rb +4 -2
  21. data/lib/eco/api/microcases/set_supervisor.rb +29 -8
  22. data/lib/eco/api/microcases/with_each.rb +7 -3
  23. data/lib/eco/api/microcases/with_each_starter.rb +3 -2
  24. data/lib/eco/api/organization/people.rb +1 -1
  25. data/lib/eco/api/session.rb +7 -2
  26. data/lib/eco/api/session/batch/job.rb +8 -0
  27. data/lib/eco/api/usecases/default_cases/create_case.rb +10 -1
  28. data/lib/eco/api/usecases/default_cases/create_details_case.rb +10 -1
  29. data/lib/eco/api/usecases/default_cases/create_details_with_supervisor_case.rb +10 -1
  30. data/lib/eco/api/usecases/default_cases/hris_case.rb +6 -2
  31. data/lib/eco/api/usecases/default_cases/upsert_case.rb +10 -1
  32. data/lib/eco/cli/config/default/input.rb +2 -2
  33. data/lib/eco/cli/config/default/options.rb +23 -7
  34. data/lib/eco/cli/config/default/usecases.rb +16 -0
  35. data/lib/eco/cli/config/default/workflow.rb +7 -4
  36. data/lib/eco/cli/config/filters.rb +6 -2
  37. data/lib/eco/cli/config/filters/input_filters.rb +3 -2
  38. data/lib/eco/cli/config/filters/people_filters.rb +3 -2
  39. data/lib/eco/cli/config/help.rb +1 -1
  40. data/lib/eco/cli/config/options_set.rb +6 -4
  41. data/lib/eco/cli/config/use_cases.rb +6 -3
  42. data/lib/eco/csv.rb +2 -0
  43. data/lib/eco/version.rb +1 -1
  44. metadata +3 -2
  45. data/lib/eco/api/common/base_loader.rb +0 -72
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06a58306abadf9b27421583990eb14960f7f30368515481b16aa474de1bc1b08
4
- data.tar.gz: 0eef93068fdb31bc6d1949f1022eac325403ac3dbb47c95b593a5b9623655773
3
+ metadata.gz: 722e1dc695f9d9fae5fbceca8a2269f4836393ee4dc50fc0f850e43fefc88789
4
+ data.tar.gz: bc34444cdf33bff51895fa5ed6bb7a89b89074e5bd11852c0a1a4f4bf6bcc574
5
5
  SHA512:
6
- metadata.gz: 80b0d2fc7bedb99deabae6d7d273cb4967eb0022db2e743078a82cace02d4f499fe8ad51ec02b7c5bcef549aac9fb03b0ea7ef5358fb602c65856654c7c20814
7
- data.tar.gz: 553e1342f38c244ab57bb259b639d55ddc4a4d5d6f72bd54ed9290111636f4dffb29834f69a5b7d2707ee3d44951fa52efccf81589194f11dfa1a709309ddb77
6
+ metadata.gz: 9a18aaa7abf012872f21209907909f1c309ddb530586bc9e27d595ace57377c3c4dba2b0a7bacc902e160cf2f51d4171a5de3e0536b6eda4dff7d018307744ca
7
+ data.tar.gz: efcaa1832677c0cf13d5dfb0ba6a3abaa8bff3fec4e1d2639b2b832358a1e51b849e594da64827bed096eb4f8f03a5664a6a8245c58128acf3478eaf0e94d38a
data/CHANGELOG.md CHANGED
@@ -1,11 +1,62 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
- ## [2.0.25] - 2021-06-xx
4
+ ## [2.0.26] - 2021-06-xx
5
+
6
+ ### Added
7
+ - `Eco::API::MicroCases#set_supervisor`, tries to keep in sync the `#subordinates` **count** of previous and new supervisor
8
+ - new **option** `-run-postlaunch` to run post launch cases, even when we run in `dry-run` mode
9
+ * when in `dry-run` it will **not** reload the people base of the session
10
+ - new **option** to append **new** entries to the `People` object
11
+ * **invokable** on **cli** via `-append-starters` (`{people: {append_created: true}}`)
12
+ * the following use cases include this option:
13
+ * `Eco::API::UseCases::DefaultCases::UpsertCase`
14
+ * `Eco::API::UseCases::DefaultCases::HrisCase`
15
+ * `Eco::API::UseCases::DefaultCases::CreateDetailsWithSupervisorCase`
16
+ * `Eco::API::UseCases::DefaultCases::CreateDetailsCase`
17
+ * `Eco::API::UseCases::DefaultCases::CreateCase`
18
+ * the option involves a new keyed argument `:append_created` in a couple of `MicroCases`
19
+ * `Eco::API::MicroCases#with_each`: where internally the search is performed against a copy of the `People` object.
20
+ * `Eco::API::MicroCases#with_each_starter`
21
+ * when `--help` is invoked option to filter the shown `-options`, `-usecases` and `filters` by a word contained in the option.
22
+ - **added** `csv` **header checks** for feed files, which entailed some changes:
23
+ * `Eco::API::Common::Loaders::Parser`
24
+ - new **subclass** `RequiredAttrs`, creatred when calling `.active_when_all` and `.active_when_any`
25
+ - **dependency** injection via `.dependencies` as `{required_attrs: RequiredAttrs}`
26
+ * **added** `Eco::API::Common::People::PersonParser#required_attrs` to offer all the `RequiredAttrs`, where defined
27
+ - the **new method** `#required_attrs` to expose the injected `RequiredAttrs`
28
+ * **new** keyed argument `check_headers:` in `Eco::API::Common::People::EntryFactory#entries`
29
+ - subsequent changes to accommodate the new param in `Eco::API::Session#csv_entries`
30
+ - `eco/cli/config/default/input` calls using this param to `true`
31
+ * `Eco::API::Common::People::DefaultParsers::CSVParser`
32
+ - added option `check_headers` via `dependencies` that enables the headers check
33
+ - it will now offer detailed warning messages on what can happen with the **missing headers**
34
+ * it will also list the **unknown header** names
35
+
36
+ ### Changed
37
+ - `Eco::API::MicroCases#set_supervisor`, the order of the 2 first parameters
38
+ - `Eco::API::Organization::People`: internally `@by_id` cache Hash included `nil` values => **not** any more.
39
+ - removed **unused** methods on **patches** for `Ecoportal::API::V1::Person` and `Ecoportal::API::Internal::Person`
40
+ * specifically `#reset_account!` and `#consolidate_account!` as well as `#reset_details!` and `#consolidate_details!`
41
+ - internal changes in `Eco::API::Common::People::Entries#entry`
42
+ * **added** option to trigger `MultipleSearchResults` StandardError when multiple candiates are found.
43
+ * **removed** `nil` values from the `caches` (the Hashes to optimize the search)
44
+ - slight structure refactor of `Eco::API::Common::Loaders`
45
+ * moved base class to subfolder/namespace
46
+ * decoupled pure `Loader` logics to `Loaders::Base` and use case inheriance chain loader to `Loaders::CaseBase`
47
+ - `Eco::API::Session::BatchJob` the `post_launch`:
48
+ * sets the `id` to the `person` if it was **created** successfully
49
+ * when in `dry-run` it fakes the `id` with a counter
50
+ - `Eco::API::Common::People::PersonFactory` gets `subordinates` initialized to `0` (when **creating** a `new` person)
51
+
52
+ ### Fixed
53
+
54
+
55
+ ## [2.0.25] - 2021-06-23
5
56
 
6
57
  ### Added
7
58
  - `Eco::API::UseCases::DefaultCases::HrisCase` validation error to require `-schema-id` command line when there are people in schemas other than the active one
8
-
59
+
9
60
  ### Changed
10
61
  - `Eco::API::Session::Batch::Job`
11
62
  * for backwards compatibility `-include-only-excluded` should bring an options structure compatible with `-include-excluded`
@@ -19,7 +70,6 @@ All notable changes to this project will be documented in this file.
19
70
  - `Eco::API::Session::Batch::Job` made **native** `-include-excluded`
20
71
  * also added new option `-include-only-excluded` to be able to only target people HRIS excluded
21
72
 
22
-
23
73
  ## [2.0.23] - 2021-06-22
24
74
 
25
75
  ### Added
@@ -51,8 +101,6 @@ All notable changes to this project will be documented in this file.
51
101
  * we just kept `roo` and `roo-xls`
52
102
  - custom `Error` classes now all inherit from `StandardError` (rather than `Exception`)
53
103
 
54
-
55
-
56
104
  ## [2.0.21] - 2021-06-04
57
105
 
58
106
  ### Added
@@ -63,7 +111,6 @@ All notable changes to this project will be documented in this file.
63
111
  ### Changed
64
112
  - `Eco::API::Common::People::EntryFactory` slight **refactor** to boost better support for multiple input formats
65
113
 
66
-
67
114
  ## [2.0.20] - 2021-05-31
68
115
 
69
116
  ### Added
@@ -10,7 +10,6 @@ require_relative 'common/class_helpers'
10
10
  require_relative 'common/class_auto_loader'
11
11
  require_relative 'common/class_hierarchy'
12
12
  require_relative 'common/class_meta_basics'
13
- require_relative 'common/base_loader'
14
13
  require_relative 'common/loaders'
15
14
  require_relative 'common/session'
16
15
  require_relative 'common/people'
@@ -7,6 +7,8 @@ module Eco
7
7
  end
8
8
  end
9
9
 
10
+ require_relative 'loaders/base'
11
+ require_relative 'loaders/case_base'
10
12
  require_relative 'loaders/use_case'
11
13
  require_relative 'loaders/policy'
12
14
  require_relative 'loaders/error_handler'
@@ -0,0 +1,58 @@
1
+ module Eco
2
+ module API
3
+ module Common
4
+ module Loaders
5
+ class Base
6
+ extend Eco::API::Common::ClassHelpers
7
+
8
+ class << self
9
+ # Sort order
10
+ def <=>(other)
11
+ created_at <=> other.created_at
12
+ end
13
+
14
+ # If still not set, it sets the `created_at` class timestamp.
15
+ def set_created_at!
16
+ @created_at = Time.now unless @created_at
17
+ end
18
+
19
+ # Class creation timestamp, to be able to load them in the order they were declared.
20
+ def created_at
21
+ @created_at ||= Time.now
22
+ end
23
+ end
24
+
25
+ # This method will be called when the BaseLoader is created
26
+ # @note
27
+ # - this method should implement the loading logics for the given `Children` class.
28
+ def initialize
29
+ raise "You should implement this method"
30
+ end
31
+
32
+ def name
33
+ self.class.name
34
+ end
35
+
36
+ private
37
+
38
+ def session
39
+ ASSETS.session
40
+ end
41
+
42
+ def config
43
+ session.config
44
+ end
45
+
46
+ def logger
47
+ session.logger
48
+ end
49
+
50
+ def micro
51
+ session.micro
52
+ end
53
+
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,33 @@
1
+ module Eco
2
+ module API
3
+ module Common
4
+ module Loaders
5
+ class CaseBase < Loaders::Base
6
+
7
+ class << self
8
+ attr_writer :name, :type
9
+
10
+ # The name that this case, policy or error handler will have.
11
+ def name(value = nil)
12
+ name_only_once! if value
13
+ set_created_at!
14
+ return @name ||= self.to_s unless value
15
+ @name = value
16
+ end
17
+
18
+ # Prevent the same class to be re-opened/re-named
19
+ def name_only_once!
20
+ raise "You have already declared #{self} or you are trying to give it a name twice" if @name
21
+ end
22
+
23
+ end
24
+
25
+ def name
26
+ self.class.name
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,7 +2,7 @@ module Eco
2
2
  module API
3
3
  module Common
4
4
  module Loaders
5
- class ErrorHandler < Eco::API::Common::BaseLoader
5
+ class ErrorHandler < Eco::API::Common::Loaders::CaseBase
6
6
 
7
7
  class << self
8
8
  attr_writer :error
@@ -17,7 +17,7 @@ module Eco
17
17
  end
18
18
 
19
19
  inheritable_class_vars :error
20
-
20
+
21
21
  def initialize(handlers)
22
22
  raise "Expected Eco::API::Policies. Given #{handlers.class}" unless handlers.is_a?(Eco::API::Error::Handlers)
23
23
  handlers.on(self.error, &self.method(:main))
@@ -2,7 +2,28 @@ module Eco
2
2
  module API
3
3
  module Common
4
4
  module Loaders
5
- class Parser < Eco::API::Common::BaseLoader
5
+ class Parser < Eco::API::Common::Loaders::CaseBase
6
+
7
+ # Helper class to scope what required attributes it depends on
8
+ class RequiredAttrs < Struct.new(:attr, :type, :attrs)
9
+ def active?(*input_attrs)
10
+ missing(*input_attrs).empty?
11
+ end
12
+
13
+ def dependant?(attr)
14
+ attrs.include?(attr)
15
+ end
16
+
17
+ def missing(*input_attrs)
18
+ return [] if input_attrs.include?(attr)
19
+ match = input_attrs & attrs
20
+ miss = attrs - match
21
+ return [] if miss.empty?
22
+ return attrs if match.empty?
23
+ return miss if type == :all
24
+ []
25
+ end
26
+ end
6
27
 
7
28
  class << self
8
29
  attr_reader :active_when
@@ -19,12 +40,14 @@ module Eco
19
40
  @attribute = value
20
41
  end
21
42
 
22
- # TODO: it migh rather merge?
23
43
  # Some parsers require dependencies to do their job.
24
- def dependencies(value = nil)
44
+ def dependencies(**value)
25
45
  @dependencies ||= {}
26
- return @dependencies unless value
27
- @dependencies = value
46
+ return @dependencies.merge({
47
+ required_attrs: @active_when_attrs
48
+ }) unless !value.empty?
49
+ raise "Expected Hash. Given: '#{value.class}'" unless value.is_a?(Hash)
50
+ @dependencies.merge!(value)
28
51
  end
29
52
 
30
53
  # Define or get the `phase` that the `parser` kicks in.
@@ -47,6 +70,7 @@ module Eco
47
70
 
48
71
  # Helper to build the `active_when` condition.
49
72
  def active_when_any(*attrs)
73
+ @active_when_attrs = RequiredAttrs.new(attribute, :any, attrs)
50
74
  @active_when = Proc.new do |source_data|
51
75
  keys = data_keys(source_data)
52
76
  attrs.any? {|key| keys.include?(key)}
@@ -55,6 +79,7 @@ module Eco
55
79
 
56
80
  # Helper to build the `active_when` condition.
57
81
  def active_when_all(*attrs)
82
+ @active_when_attrs = RequiredAttrs.new(attribute, :all, attrs)
58
83
  @active_when = Proc.new do |source_data|
59
84
  keys = data_keys(source_data)
60
85
  attrs.all? {|key| keys.include?(key)}
@@ -2,7 +2,7 @@ module Eco
2
2
  module API
3
3
  module Common
4
4
  module Loaders
5
- class Policy < Eco::API::Common::BaseLoader
5
+ class Policy < Eco::API::Common::Loaders::CaseBase
6
6
 
7
7
  def initialize(policies)
8
8
  raise "Expected Eco::API::Policies. Given #{policies.class}" unless policies.is_a?(Eco::API::Policies)
@@ -2,7 +2,7 @@ module Eco
2
2
  module API
3
3
  module Common
4
4
  module Loaders
5
- class UseCase < Eco::API::Common::BaseLoader
5
+ class UseCase < Eco::API::Common::Loaders::CaseBase
6
6
 
7
7
  class << self
8
8
  # @return [Symbol] the `type` of usecase (i.e. `:sync`, `:transform`, `:import`, `:other`)
@@ -2,7 +2,9 @@ class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Lo
2
2
  attribute :csv
3
3
 
4
4
  def parser(data, deps)
5
- Eco::CSV.parse(data, headers: true, skip_blanks: true).each_with_object([]) do |row, arr_hash|
5
+ Eco::CSV.parse(data, headers: true, skip_blanks: true).tap do |table|
6
+ check_headers(table) if deps[:check_headers]
7
+ end.each_with_object([]) do |row, arr_hash|
6
8
  row_hash = row.headers.uniq.each_with_object({}) do |attr, hash|
7
9
  next if attr.to_s.strip.empty?
8
10
  hash[attr.strip] = parse_string(row[attr])
@@ -36,4 +38,94 @@ class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Lo
36
38
  ["NULL"].any? {|token| str == token}
37
39
  end
38
40
 
41
+ def check_headers(table)
42
+ headers = table.headers
43
+ missing = missing_headers(headers)
44
+ unknown = unknown_headers(headers)
45
+ unless missing.empty? && unknown.empty?
46
+ msg = "Detected possible HEADER ISSUES !!!\n"
47
+ msg << "There might be Missing or Wrong HEADER names in the CSV file:\n"
48
+ msg << " * UNKNOWN (or not used?): #{unknown}\n" unless unknown.empty?
49
+ msg << " * MISSING DIRECT: #{missing[:direct]}\n" unless (missing[:direct] || []).empty?
50
+ unless (data = missing[:indirect] || []).empty?
51
+ msg << " * MISSING INDIRECT:\n"
52
+ data.each do |ext, info|
53
+ msg << " - '#{ext}' => "
54
+ msg << (info[:attrs] || {}).map do |status, attrs|
55
+ if status == :inactive
56
+ "makes inactive: #{attrs}"
57
+ elsif status == :active
58
+ "there could be missing info in: #{attrs}"
59
+ end
60
+ end.compact.join("; ") + "\n"
61
+ end
62
+ end
63
+ logger.warn(msg)
64
+ sleep(2)
65
+ end
66
+ end
67
+
68
+ def unknown_headers(headers)
69
+ (headers - known_headers) - all_internal_attrs
70
+ end
71
+
72
+ def missing_headers(headers)
73
+ hint = headers & all_internal_attrs
74
+ hext = headers - hint
75
+ int_head = hint + hext.map {|e| fields_mapper.to_internal(e)}.compact
76
+ known_as_int = known_headers.select do |e|
77
+ i = fields_mapper.to_internal(e)
78
+ int_head.include?(i)
79
+ end
80
+ ext = headers.select do |e|
81
+ i = fields_mapper.to_internal(e)
82
+ int_head.include?(i)
83
+ end
84
+ ext_present = known_as_int | ext
85
+ ext_miss = known_headers - ext_present
86
+ #int_miss = ext_miss.map {|ext| fields_mapper.to_internal(ext)}
87
+ ext_miss.each_with_object({}) do |ext, missing|
88
+ next unless int = fields_mapper.to_internal(ext)
89
+ if all_internal_attrs.include?(int)
90
+ missing[:direct] ||= []
91
+ missing[:direct] << ext
92
+ end
93
+ related_attrs_requirements = required_attrs.values.select do |req|
94
+ req.dependant?(int) && !int_head.include?(req.attr)
95
+ end
96
+ next if related_attrs_requirements.empty?
97
+ missing[:indirect] ||= {}
98
+ data = missing[:indirect][ext] = {}
99
+ data[:int] = int
100
+ data[:attrs] = {}
101
+ related_attrs_requirements.each_with_object(data[:attrs]) do |req, attrs|
102
+ status = req.active?(*int_head) ? :active : :inactive
103
+ attrs[status] ||= []
104
+ attrs[status] << req.attr
105
+ end
106
+ end
107
+ end
108
+
109
+ def known_headers
110
+ @known_headers ||= fields_mapper.list(:external).compact
111
+ end
112
+
113
+ def fields_mapper
114
+ session.fields_mapper
115
+ end
116
+
117
+ def required_attrs
118
+ @required_attrs ||= person_parser.required_attrs.each_with_object({}) do |ra, out|
119
+ out[ra.attr] = ra
120
+ end
121
+ end
122
+
123
+ def all_internal_attrs
124
+ person_parser.all_attrs(include_defined_parsers: true)
125
+ end
126
+
127
+ def person_parser
128
+ session.entry_factory.person_parser
129
+ end
130
+
39
131
  end
@@ -5,6 +5,42 @@ module Eco
5
5
  # Class meant to offer a _collection_ of entries, normally used to get parsed input data.
6
6
  # @attr_reader entries [Array<Eco::API::Common::PeopleEntry] a pure `Array` object.
7
7
  class Entries < Eco::Language::Models::Collection
8
+ # Error class that allows to handle cases where multiple entries were found for the same criterion.
9
+ # @note its main purpose to prevent the false pairing of duplicates or override information between different people.
10
+ class MultipleSearchResults < StandardError
11
+ attr_reader :candidates, :property
12
+ # @param msg [String] the basic message error.
13
+ # @param candiates [Array<PersonEntry>] the entries that match the same search criterion.
14
+ # @param property [String] the property of the entry model that triggered the error (base of the search criterion).
15
+ def initialize(msg, candidates: [], property: "email")
16
+ @candidates = candidates
17
+ @property = property
18
+ super(msg + " " + candidates_summary)
19
+ end
20
+
21
+ # @param with_index [Boolean] to add an index to each candidate description.
22
+ # @return [Array<String>] the `candidates` identified
23
+ def identify_candidates(with_index: false)
24
+ candidates.map.each_with_index do |entry, i|
25
+ index = with_index ? "#{i}. " : ""
26
+ "#{index} #{entry.identify}"
27
+ end
28
+ end
29
+
30
+ # @return [Person] the `candidate` in the `index` position
31
+ def candidate(index)
32
+ candidates[index]
33
+ end
34
+
35
+ private
36
+
37
+ def candidates_summary
38
+ lines = ["The following entries have the same '#{property}':"]
39
+ lines.concat(identify_candidates(with_index: true)).join("\n ")
40
+ end
41
+
42
+ end
43
+
8
44
  # build the shortcuts of Collection
9
45
  attr_collection :id, :external_id, :email, :name, :supervisor_id
10
46
 
@@ -54,19 +90,34 @@ module Eco
54
90
  # @!group Searchers
55
91
 
56
92
  # Search function to find an `entry` based on one of different options
93
+ # It searches an entry using the parameters given.
94
+ # @note This is how the search function actually works:
95
+ # 1. if eP `id` is given, returns the entry (if found), otherwise...
96
+ # 2. if `external_id` is given, returns the entry (if found), otherwise...
97
+ # 3. if `strict` is `false` and `email` is given:
98
+ # - if there is only 1 entry with that email, returns that entry, otherwise...
99
+ # - if found but, there are many candidate entries, it raises MultipleSearchResults error
100
+ # - if entry `external_id` matches `email`, returns that entry
101
+ # @raise MultipleSearchResults if there are multiple entries with the same `email`
102
+ # and there's no other criteria to find the entry. It only gets to this point if
103
+ # `external_id` was **not** provided and we are **not** in 'strict' search mode.
104
+ # However, it could be we were in `strict` mode and `external_id` was not provided.
105
+ # @param id [String] the `internal id` of the person
106
+ # @param external_id [String] the `exernal_id` of the person
107
+ # @param email [String] the `email` of the person
108
+ # @param strict [Boolean] if should perform a `:soft` or a `:strict` search. `strict` will avoid repeated email addresses.
109
+ # @return [Entry, nil] the entry we were searching, or `nil` if not found.
57
110
  def entry(id: nil, external_id: nil, email: nil, strict: false)
58
111
  init_caches
59
- pers = nil
60
- pers = @by_id[id]&.first if id
61
- pers = @by_external_id[external_id&.strip]&.first if !pers && !external_id.to_s.strip.empty?
62
-
63
- # strict prevents taking existing user for searched person with same email
64
- # specially useful if the organisation ensures all have external id (no need for email search)
65
- if !pers && (!strict || external_id.to_s.strip.empty?)
66
- pers = @by_email[email&.downcase.strip]&.first if !pers && !email.to_s.strip.empty?
67
- pers = @by_external_id[email&.downcase.strip]&.first if !pers && !email.to_s.strip.empty?
68
- end
69
- pers
112
+ # normalize values
113
+ ext_id = !external_id.to_s.strip.empty? && external_id.strip
114
+ email = !email.to_s.strip.empty? && email.downcase.strip
115
+
116
+ e = nil
117
+ e ||= @by_id[id]&.first
118
+ e ||= @by_external_id[ext_id]&.first
119
+ e ||= entry_by_email(email) unless strict && ext_id
120
+ e
70
121
  end
71
122
 
72
123
  # Search function to find an `entry` based on one of different options
@@ -136,15 +187,33 @@ module Eco
136
187
 
137
188
  private
138
189
 
190
+ def entry_by_email(email, prevent_multiple_match: false)
191
+ return nil unless email
192
+
193
+ candidates = @by_email[email] || []
194
+ return candidates.first if candidates.length == 1
195
+
196
+ if prevent_multiple_match && !candidates.empty?
197
+ msg = "Multiple search results match the criteria."
198
+ raise MultipleSearchResults.new(msg, candidates: candidates, property: "email")
199
+ end
200
+
201
+ @by_external_id[email]&.first
202
+ end
203
+
139
204
  def init_caches
140
205
  return if @caches_init
141
- @by_id = to_h
142
- @by_external_id = to_h('external_id')
143
- @by_email = to_h('email')
206
+ @by_id = no_nil_key(to_h)
207
+ @by_external_id = no_nil_key(to_h('external_id'))
208
+ @by_email = no_nil_key(to_h('email'))
144
209
  @array_supers = sort_by_supervisors(@items)
145
210
  @caches_init = true
146
211
  end
147
212
 
213
+ def no_nil_key(hash)
214
+ hash.tap {|h| h.delete(nil)}
215
+ end
216
+
148
217
  end
149
218
  end
150
219
  end