eco-helpers 2.0.25 → 2.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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