peoplegroup-connectors 1.0.5 → 2.0.0
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.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/lib/peoplegroup/connectors/gitlab.rb +129 -5
- data/lib/peoplegroup/connectors/google_sheets.rb +7 -0
- data/lib/peoplegroup/connectors/greenhouse.rb +88 -12
- data/lib/peoplegroup/connectors/hris.rb +89 -107
- data/lib/peoplegroup/connectors/version.rb +1 -1
- data/lib/peoplegroup/connectors/workday/xml/request_one_time_payment.rb +3 -3
- metadata +4 -19
- data/lib/peoplegroup/connectors/bamboo.rb +0 -355
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 561d0cf562f003fd466dd9b9bec4494308228b046ea7e6d1af0a65f44c296acb
|
4
|
+
data.tar.gz: c1faf7bfa314dfb8bbbfe36aad665140aa6f357e97efa01a8e378d68e8ef6be8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba0f3f958eabdc082033844c7e3341298606d901333bdd46f6e452e04195ccde1809bc8b388453c4400b60b41febb1ab7e2763ab57c3208ba16b14b86a404fdf
|
7
|
+
data.tar.gz: 331d056805178a2335b47a9b4768295da89c4ff403c019edaf5c3bea452cbf6aa5d415a5701350401ff5dacdd0b5057b66ed0ac6d2260461e589092accdccec0
|
data/README.md
CHANGED
@@ -37,6 +37,16 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
37
37
|
|
38
38
|
For local development and manual testing, copy the contents of the `.env.example` file to a new file called `.env` and fill-in the environment variable values. You will then be able to run a REPL by running `bundle exec bin/console` from the root of the repository.
|
39
39
|
|
40
|
+
### Documentation
|
41
|
+
|
42
|
+
To view documentation for the current release. You can navigate to https://gemdocs.org/gems/peoplegroup-connectors/latest to view the latest documentation or https://gemdocs.org/gems/peoplegroup-connectors to select documentation for particular tags.
|
43
|
+
|
44
|
+
If you are developing locally, you can preview documentation changes by starting a yard server in the background:
|
45
|
+
|
46
|
+
```console
|
47
|
+
yard server --reload
|
48
|
+
```
|
49
|
+
|
40
50
|
## Contributing and Code of Conduct
|
41
51
|
|
42
52
|
Bug reports and pull requests are welcome on GitLab at <https://gitlab.com/gitlab-com/people-group/peopleops-eng/connectors-gem>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
@@ -5,18 +5,28 @@ require 'gitlab'
|
|
5
5
|
module PeopleGroup
|
6
6
|
module Connectors
|
7
7
|
class GitLab
|
8
|
+
# The Gitlab.client instance we wrap around.
|
8
9
|
attr_accessor :client
|
9
10
|
|
11
|
+
# The GitLab instance API url.
|
10
12
|
API_URL = ENV['GITLAB_API_V4_URL'] || 'https://gitlab.com/api/v4'
|
11
13
|
|
14
|
+
# Create a new client instance with the provided token for authentication.
|
15
|
+
# @param token [String] The personal access token to use for authentication.
|
12
16
|
def initialize(token: ENV['GITLAB_API_TOKEN'])
|
13
17
|
@client = Gitlab.client(endpoint: API_URL, private_token: token)
|
14
18
|
end
|
15
19
|
|
20
|
+
# Serach for a GitLab user under the gitlab-com group.
|
16
21
|
def find_gitlabber(field, query)
|
17
22
|
find_gitlabber_on(field, query, 'gitlab-com')
|
18
23
|
end
|
19
24
|
|
25
|
+
# Finds a GitLab user by the field.
|
26
|
+
# @param field [Symbol] The field to search for.
|
27
|
+
# @param query [String] The query to search for.
|
28
|
+
# @param group [String|Integer] The group to search in.
|
29
|
+
# @return [Gitlab::ObjectifiedHash] The found GitLab user if any.
|
20
30
|
def find_gitlabber_on(field, query, group)
|
21
31
|
return if !query || query.empty?
|
22
32
|
|
@@ -28,55 +38,117 @@ module PeopleGroup
|
|
28
38
|
end
|
29
39
|
end
|
30
40
|
|
41
|
+
# Creates a GitLab group.
|
42
|
+
# @param name [String] The name of the group.
|
43
|
+
# @param path [String] The path of the group.
|
44
|
+
# @param options [Hash] The options to create the group with.
|
45
|
+
# @return [Gitlab::ObjectifiedHash] The created group.
|
46
|
+
# @see https://docs.gitlab.com/ee/api/groups.html#create-a-group
|
31
47
|
def create_group(name, path, options = {})
|
32
48
|
retry_on_error { @client.create_group(name, path, options) }
|
33
49
|
end
|
34
50
|
|
51
|
+
# Get subgroups of a group.
|
52
|
+
# @param group_id [Integer|String] the id of the group to get the subgroups of.
|
53
|
+
# @param options [Hash] the options to get the subgroups with.
|
54
|
+
# @return [Array<Gitlab::ObjectifiedHash>] the subgroups of the group.
|
55
|
+
# @see https://docs.gitlab.com/ee/api/groups.html#list-subgroups
|
35
56
|
def get_subgroups(group_id, options = {})
|
36
57
|
retry_on_error { @client.group_subgroups(group_id, options).auto_paginate }
|
37
58
|
end
|
38
59
|
|
60
|
+
# Get members of a group.
|
61
|
+
# @param group_id [Integer|String] the id of the group to get the members of.
|
62
|
+
# @param options [Hash] the options to get the members with.
|
63
|
+
# @return [Array<Gitlab::ObjectifiedHash>] the members of the group.
|
64
|
+
# @see https://docs.gitlab.com/ee/api/groups.html#list-users
|
39
65
|
def get_group_members(group_id, options = {})
|
40
66
|
retry_on_error { @client.group_members(group_id, options).auto_paginate }
|
41
67
|
end
|
42
68
|
|
69
|
+
# Add a group member with a set access level.
|
70
|
+
# @param group_id [Integer|String] the id of the group to add the member to.
|
71
|
+
# @param user_id [Integer|String] the id of the user to add to the group.
|
72
|
+
# @param access_level [Integer] the access level to add the user with.
|
73
|
+
# @return [Gitlab::ObjectifiedHash] the created group member.
|
74
|
+
# @see https://docs.gitlab.com/ee/api/members.html#add-a-member-to-a-group-or-project
|
43
75
|
def add_group_member(group_id, user_id, access_level)
|
44
76
|
retry_on_error { @client.add_group_member(group_id, user_id, access_level) }
|
45
77
|
end
|
46
78
|
|
79
|
+
# Remove a group member
|
80
|
+
# @param group_id [Integer|String] the id of the group to remove the member from.
|
81
|
+
# @param user_id [Integer|String] the id of the user to remove from the group.
|
47
82
|
def remove_group_member(group_id, user_id)
|
48
83
|
retry_on_error { @client.remove_group_member(group_id, user_id) }
|
49
84
|
end
|
50
85
|
|
86
|
+
# Get a list of a groups user ids
|
87
|
+
# @param group_id [Integer|String] the id of the group to get the members of.
|
88
|
+
# @return [Array<Integer>] the user ids of the members of the group.
|
51
89
|
def group_member_ids(group_id)
|
52
90
|
members = retry_on_error { @client.group_members(group_id) }
|
53
91
|
members.map(&:id)
|
54
92
|
end
|
55
93
|
|
56
|
-
|
94
|
+
# Create a new issue.
|
95
|
+
# @param project [Integer|String] the id of the project to create the issue in.
|
96
|
+
# @param title [String] the title of the issue.
|
97
|
+
# @param options [Hash] the options to create the issue with.
|
98
|
+
# @return [Gitlab::ObjectifiedHash] the created issue.
|
99
|
+
# @see https://docs.gitlab.com/ee/api/issues.html#new-issue
|
100
|
+
# @example
|
101
|
+
# gitlab = PeopleGroup::Connectors::GitLab.new
|
102
|
+
# options = { description: 'This is a new issue', labels: ['bug'] }
|
103
|
+
# gitlab.create_issue(123, 'New issue', options)
|
104
|
+
def create_issue(project, title, options)
|
57
105
|
retry_on_error { @client.create_issue(project, title, options) }
|
58
106
|
end
|
59
107
|
|
108
|
+
# Create a note on an issue
|
109
|
+
# @param project [Integer|String] the id of the project to create the note in.
|
110
|
+
# @param id [Integer|String] the id of the issue to create the note on.
|
111
|
+
# @param text [String] the text of the note.
|
112
|
+
# @return [Gitlab::ObjectifiedHash] the created issue note.
|
113
|
+
# @example
|
114
|
+
# gitlab = PeopleGroup::Connectors::GitLab.new
|
115
|
+
# gitlab.create_issue_note(123, 456, 'This is a new note')
|
60
116
|
def create_issue_note(project, id, text)
|
61
117
|
retry_on_error { @client.create_issue_note(project, id, text) }
|
62
118
|
end
|
63
119
|
|
120
|
+
# List notes on an issue.
|
121
|
+
# @param project [Integer|String] the id of the project to list the notes from.
|
122
|
+
# @param id [Integer|String] the id of the issue to list the notes from.
|
123
|
+
# @param options [Hash] the options to list the notes with.
|
124
|
+
# @return [Array<Gitlab::ObjectifiedHash>] a list of notes associated to the issue.
|
64
125
|
def issue_notes(project, id, options = {})
|
65
126
|
retry_on_error { @client.issue_notes(project, id, options) }
|
66
127
|
end
|
67
128
|
|
129
|
+
# Edit an issue.
|
130
|
+
# @param project [Integer|String] the id of the project to edit the issue in.
|
131
|
+
# @param id [Integer|String] the id of the issue to edit.
|
132
|
+
# @param options [Hash] the options to edit the issue with.
|
133
|
+
# @return [Gitlab::ObjectifiedHash] the updated issue.
|
68
134
|
def edit_issue(project, id, options)
|
69
135
|
retry_on_error { @client.edit_issue(project, id, options) }
|
70
136
|
end
|
71
137
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
138
|
+
# Get a list of issues from a project.
|
139
|
+
# @param project [Integer|String] the id of the project to search for the issue in.
|
140
|
+
# @param args [Hash] the options to search for the issue with.
|
141
|
+
# @return [Array<Gitlab::ObjectifiedHash>] the list of issues matching the search criteria.
|
142
|
+
# @see https://docs.gitlab.com/ee/api/issues.html#list-project-issues
|
76
143
|
def get_issues(project, args)
|
77
144
|
retry_on_error { @client.issues(project, args).auto_paginate }
|
78
145
|
end
|
146
|
+
alias_method :get_onboarding_issues, :get_issues
|
79
147
|
|
148
|
+
# Gets an issue from the associated project.
|
149
|
+
# @param project [Integer|String] the id of the project to search for the issue in.
|
150
|
+
# @param issue_id [Integer|String] the id of the issue to get.
|
151
|
+
# @return [Gitlab::ObjectifiedHash] the issue object.
|
80
152
|
def get_issue(project, issue_id)
|
81
153
|
retry_on_error { @client.issue(project, issue_id) }
|
82
154
|
end
|
@@ -85,6 +157,11 @@ module PeopleGroup
|
|
85
157
|
retry_on_error { @client.update_variable(project, key, value, **opts) }
|
86
158
|
end
|
87
159
|
|
160
|
+
# Find or create an epic by title.
|
161
|
+
# @param group_id [Integer|String] the id of the group to search for the epic in.
|
162
|
+
# @param title [String] the title of the epic.
|
163
|
+
# @param options [Hash] the options to create the epic with.
|
164
|
+
# @return [Gitlab::ObjectifiedHash] the created or found epic.
|
88
165
|
def find_or_create_epic(group_id, title, options = {})
|
89
166
|
epic = find_epic(group_id, title)
|
90
167
|
reopen_epic(epic) if epic && epic.state == 'closed'
|
@@ -216,28 +293,75 @@ module PeopleGroup
|
|
216
293
|
create_merge_request(project_id, commit_message, options)
|
217
294
|
end
|
218
295
|
|
296
|
+
# Create an epic for a group.
|
297
|
+
# @param group_id [Integer|String] the id of the group to create the epic for.
|
298
|
+
# @param title [String] the title of the epic.
|
299
|
+
# @param options [Hash] the options to create the epic with.
|
300
|
+
# @return [Gitlab::ObjectifiedHash] the created epic.
|
301
|
+
# @see https://docs.gitlab.com/ee/api/epics.html#new-epic
|
219
302
|
def create_epic(group_id, title, options)
|
220
303
|
retry_on_error { @client.create_epic(group_id, title, options) }
|
221
304
|
end
|
222
305
|
|
306
|
+
# Create a note on an epic.
|
307
|
+
# @param group_id [Integer|String] the id of the group to search for the epic in.
|
308
|
+
# @param epic_id [Integer|String] the id of the epic to create the note for.
|
309
|
+
# @param text [String] the text of the note.
|
223
310
|
def create_epic_note(group_id, epic_id, text)
|
224
311
|
retry_on_error { @client.create_epic_note(group_id, epic_id, text) }
|
225
312
|
end
|
226
313
|
|
314
|
+
# Get all epics of a specific group.
|
315
|
+
# @param group_id [Integer|String] the id of the group to search for the epics in.
|
316
|
+
# @param options [Hash] the options to search for the epics with.
|
317
|
+
# @return [Array<Gitlab::ObjectifiedHash>] the list of epics associated
|
318
|
+
# @see https://docs.gitlab.com/ee/api/epics.html#list-epics-for-a-group
|
227
319
|
def get_epics(group_id, options = {})
|
228
320
|
retry_on_error { @client.epics(group_id, options).auto_paginate }
|
229
321
|
end
|
230
322
|
|
323
|
+
# Get the the issues for a specific epic.
|
324
|
+
# @param group_id [Integer|String] the id of the group to search for the epic in.
|
325
|
+
# @param epic_iid [Integer|String] the iid of the epic to search for the issues of.
|
326
|
+
# @return [Array<Gitlab::ObjectifiedHash>] the list of issues associated with the epic.
|
327
|
+
# @see https://docs.gitlab.com/ee/api/epics.html#get-a-list-of-epic-issues
|
231
328
|
def get_issue_epics(group_id, epic_iid)
|
232
329
|
retry_on_error { @client.epic_issues(group_id, epic_iid) }
|
233
330
|
end
|
234
331
|
|
332
|
+
# Get the file at a specific path in a specific project.
|
333
|
+
# @param project_id [Integer|String] the id of the project to search for the file in.
|
334
|
+
# @param file_path [String] the files path relative to the project root.
|
335
|
+
# @param ref [String] the branch or tag name, or the commit SHA, to retrieve the file from.
|
336
|
+
# @return [Gitlab::ObjectifiedHash] The get repository file response.
|
337
|
+
# @see https://docs.gitlab.com/ee/api/repository_files.html#get-file-from-repository
|
338
|
+
# @example
|
339
|
+
# client = PeopleGroup::Connectors::GitLab.new
|
340
|
+
# client.get_file(00000, 'path/to/file.txt')
|
341
|
+
def get_file(project_id, file_path, ref = 'main')
|
342
|
+
retry_on_error { @client.get_file(project_id, file_path, ref) }
|
343
|
+
end
|
344
|
+
|
345
|
+
# Get the contents of the specified file at a specific path in a project.
|
346
|
+
# @param project_id [Integer|String] the id of the project to search for the file in.
|
347
|
+
# @param file_path [String] the file path relative to the project root.
|
348
|
+
# @param ref [String] the branch or tag name, or the commit SHA, to retrieve the file from.
|
349
|
+
# @return [String] The contents of the file.
|
350
|
+
# @example
|
351
|
+
# client = PeopleGroup::Connectors::GitLab.new
|
352
|
+
# client.get_file_contents(00000, 'path/to/file.txt')
|
353
|
+
def get_file_contents(project_id, file_path, ref = 'main')
|
354
|
+
response = get_file(project_id, file_path, ref)
|
355
|
+
Base64.decode64(response&.content)
|
356
|
+
end
|
357
|
+
|
235
358
|
private
|
236
359
|
|
237
360
|
def retry_on_error(&block)
|
238
361
|
Utils.retry_on_error(errors: retry_errors, delay: 3, &block)
|
239
362
|
end
|
240
363
|
|
364
|
+
# Common errors that we can retry.
|
241
365
|
def retry_errors
|
242
366
|
[Gitlab::Error::InternalServerError, Gitlab::Error::BadGateway, Gitlab::Error::ServiceUnavailable]
|
243
367
|
end
|
@@ -5,10 +5,17 @@ require 'google_drive'
|
|
5
5
|
module PeopleGroup
|
6
6
|
module Connectors
|
7
7
|
class GoogleSheets
|
8
|
+
# Find a spreadsheet by title.
|
9
|
+
# @param title [String] the title of the spreadsheet, must be exact and shared with the service account.
|
10
|
+
# @return [GoogleDrive::Spreadsheet|nil] the found spreadsheet or nil if no matches were found.
|
8
11
|
def spreadsheet(title)
|
9
12
|
retry_on_error { session.spreadsheet_by_title(title) }
|
10
13
|
end
|
11
14
|
|
15
|
+
# Find a worksheet by title within a given spreadsheet.
|
16
|
+
# @param spreadsheet_title [String] the title of the spreadsheet, must be exact and shared with the service account.
|
17
|
+
# @param worksheet_title [String] the title of the worksheet inside the spreadsheet, must be an exact match.
|
18
|
+
# @return [GoogleDrive::Worksheet|nil] the found worksheet or nil if no matches were found.
|
12
19
|
def worksheet(spreadsheet_title, worksheet_title)
|
13
20
|
worksheets = retry_on_error { spreadsheet(spreadsheet_title).worksheets }
|
14
21
|
worksheets.find { |worksheet| worksheet.title == worksheet_title }
|
@@ -5,28 +5,60 @@ require 'greenhouse_io'
|
|
5
5
|
module PeopleGroup
|
6
6
|
module Connectors
|
7
7
|
class Greenhouse
|
8
|
+
# Maximum amount of retries for requests.
|
8
9
|
MAX_RETRIES = 3
|
9
10
|
|
11
|
+
# Error to retry on.
|
12
|
+
RETRY_ERRORS = [
|
13
|
+
GreenhouseIo::Error,
|
14
|
+
Net::OpenTimeout,
|
15
|
+
RestClient::InternalServerError
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
# Time to delay inbetween retries.
|
19
|
+
RETRY_DELAY = 3
|
20
|
+
|
21
|
+
# Create a new Greenhouse client instance.
|
22
|
+
# @param use_users_api_key [Boolean] Whether to use the users API key or the default one.
|
23
|
+
# @return [PeopleGroup::Connectors::Greenhouse]
|
24
|
+
# @example
|
25
|
+
# client = PeopleGroup::Connectors::Greenhouse.new
|
10
26
|
def initialize(use_users_api_key: false)
|
11
27
|
api_key = use_users_api_key ? ENV['GREENHOUSE_API_KEY_USERS'] : ENV['GREENHOUSE_API_KEY']
|
12
28
|
@client = GreenhouseIo::Client.new(api_key)
|
13
29
|
end
|
14
30
|
|
31
|
+
# List all offers for an application.
|
32
|
+
# @param application_id [Integer|String] The id of the application to get the offers for.
|
33
|
+
# @return [Array<Hash>] The offers for the application.
|
34
|
+
# @see https://developers.greenhouse.io/harvest.html#get-list-offers-for-application
|
15
35
|
def offer_for_application(application_id)
|
16
|
-
@client.offers_for_application(application_id)
|
36
|
+
retry_on_error { @client.offers_for_application(application_id) }
|
17
37
|
end
|
18
|
-
|
38
|
+
alias_method :offers_for_application, :offer_for_application
|
39
|
+
|
40
|
+
# Get the current offer of an application.
|
41
|
+
# @param application_id [Integer|String] The id of the application to get the offer for.
|
42
|
+
# @return [Hash] The current offer for the application.
|
43
|
+
# @example
|
44
|
+
# client = PeopleGroup::Connectors::Greenhouse.new
|
45
|
+
# client.current_offer_for_application(123)
|
46
|
+
# #=> { "id" => 123, "status" => "accepted", "application_id" => 456, ... }
|
47
|
+
# @see https://developers.greenhouse.io/harvest.html#get-retrieve-current-offer-for-application
|
19
48
|
def current_offer_for_application(application_id)
|
20
49
|
@client.current_offer_for_application(application_id)
|
21
50
|
end
|
22
51
|
|
52
|
+
# Check for hired candidates based on when they have been updated.
|
53
|
+
# @param updated_since [String|DateTime] YYYY-MM-DD format with optional time stamp.
|
54
|
+
# @return [Array<Hash>] The hired candidates.
|
55
|
+
# @see PeopleGroup::Connectors::Greenhouse#hired_non_active
|
23
56
|
def hired_candidates(updated_since)
|
24
57
|
page = 1
|
25
58
|
candidates = []
|
26
|
-
on_error = -> { p [updated_since, page] }
|
27
59
|
|
28
60
|
loop do
|
29
|
-
results = Utils.retry_on_error(
|
61
|
+
results = Utils.retry_on_error(on_error: -> { p [updated_since, page] }) do
|
30
62
|
@client.candidates(nil, updated_after: updated_since, page: page)
|
31
63
|
end
|
32
64
|
|
@@ -42,25 +74,43 @@ module PeopleGroup
|
|
42
74
|
candidates
|
43
75
|
end
|
44
76
|
|
77
|
+
# Search for a canidate by their candidate id.
|
78
|
+
# @param candidate_id [String|Integer] the candidates id.
|
79
|
+
# @return [Hash] the candidates details.
|
80
|
+
# @see https://developers.greenhouse.io/harvest.html#get-retrieve-candidate
|
45
81
|
def candidate(candidate_id)
|
46
|
-
@client.candidates(candidate_id)
|
82
|
+
retry_on_error { @client.candidates(candidate_id) }
|
47
83
|
end
|
48
84
|
|
85
|
+
# Add a note to the candidates profile in Greenhouse.
|
86
|
+
# @param candidate_id [String|Integer] the candidates ID that is being synced.
|
87
|
+
# @return [Boolean] whether or not the request was successful.
|
49
88
|
def add_sync_note_to_candidate(candidate_id)
|
50
89
|
note = {
|
51
90
|
user_id: ENV['GREENHOUSE_AUTHOR_ID'],
|
52
91
|
body: "This person was synced at #{Time.now} by the Employee Bot",
|
53
92
|
visibility: 'public'
|
54
93
|
}
|
55
|
-
|
94
|
+
|
95
|
+
retry_on_error { @client.create_candidate_note(candidate_id, note, ENV['GREENHOUSE_AUTHOR_ID']) }
|
56
96
|
end
|
57
97
|
|
98
|
+
# List candidates by the specified options.
|
99
|
+
# @param id [String|Integer] The ID of the candidate to fetch.
|
100
|
+
# @param options [Hash] The options to filter the candidates by.
|
101
|
+
# @return [Array<Hash>] candidates matching the specified filter.
|
102
|
+
# @example
|
103
|
+
# greenhouse = PeopleGroup::Connectors::Greenhouse.new
|
104
|
+
# greenhouse.candidates(nil, { email: 'john@example.com' })
|
105
|
+
# greenhouse.candiates(12345)
|
106
|
+
# @see https://developers.greenhouse.io/harvest.html#get-list-candidates
|
58
107
|
def candidates(id = nil, options = {})
|
59
|
-
|
60
|
-
@client.candidates(id, options)
|
61
|
-
end
|
108
|
+
retry_on_error { @client.candidates(id, options) }
|
62
109
|
end
|
63
110
|
|
111
|
+
# Paginate through a list of users in Greenhouse.
|
112
|
+
# @return [Array<Hash>] The users in Greenhouse.
|
113
|
+
# @see https://developers.greenhouse.io/harvest.html#get-list-users
|
64
114
|
def users
|
65
115
|
page = 1
|
66
116
|
users = []
|
@@ -76,18 +126,40 @@ module PeopleGroup
|
|
76
126
|
users
|
77
127
|
end
|
78
128
|
|
129
|
+
# Find an application by id.
|
130
|
+
# @param application_id [String|Integer] The ID of the application to fetch.
|
131
|
+
# @return [Hash] The application data.
|
132
|
+
# @see https://developers.greenhouse.io/harvest.html#get-retrieve-application
|
133
|
+
# @example
|
134
|
+
# greenhouse = PeopleGroup::Connectors::Greenhouse.new
|
135
|
+
# greenhouse.application(12345)
|
79
136
|
def application(application_id)
|
80
|
-
@client.applications(application_id)
|
137
|
+
retry_on_error { @client.applications(application_id) }
|
81
138
|
end
|
82
139
|
|
140
|
+
# Check if the individual has any active applications.
|
141
|
+
# @param work_email [String] the individuals email address.
|
142
|
+
# @return [Boolean] true if the individual has any active applications, false if not.
|
83
143
|
def has_active_application?(work_email)
|
84
|
-
|
144
|
+
# Find the canidate by email.
|
145
|
+
candidate = candidates(nil, { email: work_email })
|
146
|
+
|
147
|
+
# Return false unless they could be found or have no applications.
|
148
|
+
return false unless candidate && candidate['applications'].size.positive?
|
149
|
+
|
150
|
+
# Check all applications for any with a status of 'active'
|
151
|
+
candidate['applications'].any? { |application| application['status'] == 'active' }
|
85
152
|
rescue RestClient::NotFound
|
86
|
-
|
153
|
+
false # return false if candidate could not be found
|
87
154
|
end
|
88
155
|
|
89
156
|
private
|
90
157
|
|
158
|
+
# Check if the candidate is hired and inactive.
|
159
|
+
# - The candidate has any application that is active, we don't sync.
|
160
|
+
# - Check if candidate is hired for at least one of their applications
|
161
|
+
# @param candidate [Hash] The candidate data.
|
162
|
+
# @return [Boolean] whether or not the candidate is hired but not active.
|
91
163
|
def hired_non_active?(candidate)
|
92
164
|
# If the candidate has any application that is active, we don't sync.
|
93
165
|
return false if candidate['applications'].any? { |application| application['status'] == 'active' }
|
@@ -95,6 +167,10 @@ module PeopleGroup
|
|
95
167
|
# Check if candidate is hired for at least one of their applications
|
96
168
|
candidate['applications'].any? { |application| application['status'] == 'hired' }
|
97
169
|
end
|
170
|
+
|
171
|
+
def retry_on_error(on_error: -> {}, &block)
|
172
|
+
Utils.retry_on_error(errors: RETRY_ERRORS, delay: RETRY_DELAY, max_attempts: MAX_RETRIES, on_error: on_error, &block)
|
173
|
+
end
|
98
174
|
end
|
99
175
|
end
|
100
176
|
end
|
@@ -8,9 +8,13 @@ module PeopleGroup
|
|
8
8
|
|
9
9
|
def initialize
|
10
10
|
@workday = Workday::Client.new unless ENV.fetch('WORKDAY_SERVICE_URL', nil).nil?
|
11
|
-
@bamboo = Bamboo.new
|
12
11
|
end
|
13
12
|
|
13
|
+
# List all team members from the WORKDAY_WORKERS_REPORT.
|
14
|
+
# @return [Array<Hash>]
|
15
|
+
# hris = PeopleGroup::Connectors::Hris.new
|
16
|
+
# hris.team_members
|
17
|
+
# #=> [{ "workEmail"=>"tanuki@gitlab.com" }, ...]
|
14
18
|
def employees
|
15
19
|
@employees ||= Workday::Report.call(
|
16
20
|
url: ENV.fetch('WORKDAY_WORKERS_REPORT', nil),
|
@@ -19,11 +23,15 @@ module PeopleGroup
|
|
19
23
|
end
|
20
24
|
alias_method :team_members, :employees
|
21
25
|
|
26
|
+
# List team members with the satus of 'Active'.
|
27
|
+
# @return [Array<Hash] team members with a status of 'Active'.
|
22
28
|
def active_employees
|
23
29
|
employees.select { |employee| employee['status'] == 'Active' }
|
24
30
|
end
|
25
31
|
alias_method :active_team_members, :active_employees
|
26
32
|
|
33
|
+
# List team members with the satus of 'Active' and a hireDate that is less than or equal to today.
|
34
|
+
# @return [Array<Hash>] team members with a status of 'Active' and a hireDate that is less than or equal to today.
|
27
35
|
def active_and_current_employees
|
28
36
|
today = Date.current
|
29
37
|
employees.select do |employee|
|
@@ -32,11 +40,22 @@ module PeopleGroup
|
|
32
40
|
end
|
33
41
|
alias_method :active_and_current_team_members, :active_and_current_employees
|
34
42
|
|
43
|
+
# Get the details of a team member from the WORKDAY_WORKERS_REPORT.
|
44
|
+
# @param employee_number [String|Integer] the team member's employee number.
|
45
|
+
# @return [Hash] the team member's details.
|
46
|
+
# @example
|
47
|
+
# hris = PeopleGroup::Connectors::Hris.new
|
48
|
+
# hris.get_employee_details(12345)
|
49
|
+
# #=> { "workEmail"=>"tanuki@gitlab.com", ... }
|
35
50
|
def get_employee_details(employee_number)
|
36
51
|
employees.find { |emp| emp['employeeNumber'] == employee_number.to_s }
|
37
52
|
end
|
38
53
|
alias_method :get_team_member_details, :get_employee_details
|
39
54
|
|
55
|
+
# Get the details of a team member from the WORKDAY_WORKERS_REPORT.
|
56
|
+
# @param employee_number [String|Integer]
|
57
|
+
# @raise [EmployeeNotFoundError] if the team member could not be found.
|
58
|
+
# @see [PeopleGroup::Connectors::Hris#get_employee_details]
|
40
59
|
def get_employee_details!(employee_number)
|
41
60
|
employee_details = get_employee_details(employee_number)
|
42
61
|
raise EmployeeNotFoundError, "No team member found with employee number #{employee_number}" if employee_details.nil?
|
@@ -45,6 +64,12 @@ module PeopleGroup
|
|
45
64
|
end
|
46
65
|
alias_method :get_team_member_details!, :get_employee_details!
|
47
66
|
|
67
|
+
# Search a team member by name.
|
68
|
+
# @param name [String] the team member's name.
|
69
|
+
# @return [Hash|nil] the team member's details if found or nil.
|
70
|
+
# @example
|
71
|
+
# hris = PeopleGroup::Connectors::Hris.new
|
72
|
+
# hris.search_employee('Jane Doe')
|
48
73
|
def search_employee(name)
|
49
74
|
return if name.empty?
|
50
75
|
|
@@ -61,6 +86,10 @@ module PeopleGroup
|
|
61
86
|
end
|
62
87
|
alias_method :search_team_member, :search_employee
|
63
88
|
|
89
|
+
# Search team member by name.
|
90
|
+
# @param name [String] the team member's name or display name.
|
91
|
+
# @raise [EmployeeNotFoundError] if the team member could not be found.
|
92
|
+
# @see [PeopleGroup::Connectors::Hris#search_employee]
|
64
93
|
def search_employee!(name)
|
65
94
|
employee = search_employee(name)
|
66
95
|
raise EmployeeNotFoundError, "No employee found with name #{name}" if employee.nil?
|
@@ -69,11 +98,24 @@ module PeopleGroup
|
|
69
98
|
end
|
70
99
|
alias_method :search_team_member!, :search_employee!
|
71
100
|
|
101
|
+
# Search a team meber by field.
|
102
|
+
# @param field [String] the field to search on.
|
103
|
+
# @param value [String|Integer] the value to search for.
|
104
|
+
# @return [Hash|nil] the team member's details if found or nil.
|
105
|
+
# @example
|
106
|
+
# hris = PeopleGroup::Connectors::Hris.new
|
107
|
+
# hris.search_employee_by_field('workEmail', 'tanuki@gitlab.com')
|
108
|
+
# #=> { "workEmail"=>"tanuki@gitlab.com", ... }
|
72
109
|
def search_employee_by_field(field:, value:)
|
73
110
|
employees.find { |employee| employee[field] == value.to_s }
|
74
111
|
end
|
75
112
|
alias_method :search_team_member_by_field, :search_employee_by_field
|
76
113
|
|
114
|
+
# Search a team member by field, and raise error if not found.
|
115
|
+
# @param field [String] the field to search on.
|
116
|
+
# @param value [String|Integer] the value to search for.
|
117
|
+
# @raise [EmployeeNotFoundError] if the team member could not be found.
|
118
|
+
# @see [PeopleGroup::Connectors::Hris#search_employee_by_field]
|
77
119
|
def search_employee_by_field!(field:, value:)
|
78
120
|
employee = search_employee_by_field(field: field, value: value)
|
79
121
|
raise EmployeeNotFoundError, "No employee found with #{field}: #{value}" if employee.nil?
|
@@ -83,6 +125,11 @@ module PeopleGroup
|
|
83
125
|
alias_method :search_team_member_by_field!, :search_employee_by_field!
|
84
126
|
|
85
127
|
# Find the associated team member without checking case on their e-mail fields.
|
128
|
+
# 1. Look for a match on their workEmail field.
|
129
|
+
# 2. Look for a match on their bestEmail field.
|
130
|
+
# 3. Look for a match on their homeEmail field.
|
131
|
+
# @param email [String] the team member's e-mail address.
|
132
|
+
# @return [Hash|nil] the team member's details if found or nil.
|
86
133
|
def search_team_member_by_email(email)
|
87
134
|
return nil unless email
|
88
135
|
|
@@ -94,6 +141,8 @@ module PeopleGroup
|
|
94
141
|
alias_method :slack_email_lookup_with_fallback, :search_team_member_by_email
|
95
142
|
|
96
143
|
# Find the team member by email and raise error if not found.
|
144
|
+
# @param email [String] the team members e-mail address.
|
145
|
+
# @see [PeopleGroup::Connectors::Hris#search_team_member_by_email]
|
97
146
|
def search_team_member_by_email!(email)
|
98
147
|
team_member = search_team_member_by_email(email)
|
99
148
|
|
@@ -103,18 +152,38 @@ module PeopleGroup
|
|
103
152
|
end
|
104
153
|
alias_method :slack_email_lookup_with_fallback!, :search_team_member_by_email!
|
105
154
|
|
155
|
+
# Team members filtered by department
|
156
|
+
# @param department [String] the department name.
|
157
|
+
# @return [Array<Hash>] the team members that belong to that department
|
158
|
+
# @example
|
159
|
+
# hris = PeopleGroup::Connectors::Hris.new
|
160
|
+
# hris.team_members_by_department('People Operations')
|
161
|
+
# #=> [{ "workEmail"=>"tanuki@gitlab.com", ... }, ...]
|
106
162
|
def team_members_by_department(department)
|
107
163
|
active_and_current_team_members.select { |team_member| team_member['department'] == department }
|
108
164
|
end
|
109
165
|
|
166
|
+
# Team members filtered by division
|
167
|
+
# @param division [String] the division name.
|
168
|
+
# @return [Array<Hash>] the team members that belong to that division
|
169
|
+
# @example
|
170
|
+
# hris = PeopleGroup::Connectors::Hris.new
|
171
|
+
# hris.team_members_by_division('People Group')
|
110
172
|
def team_members_by_division(division)
|
111
173
|
active_and_current_team_members.select { |team_member| team_member['division'] == division }
|
112
174
|
end
|
113
175
|
|
176
|
+
# Find the manager of a team member.
|
177
|
+
# @param team_member [Hash] the team member's details.
|
178
|
+
# @return [Hash|nil] the manager's details
|
114
179
|
def fetch_manager(team_member)
|
115
180
|
active_team_members.find { |tm| tm['employeeNumber'] == team_member['supervisorId'] }
|
116
181
|
end
|
117
182
|
|
183
|
+
# Find the manager of a team member and raise error if not found.
|
184
|
+
# @param team_member [Hash] the team member's details.
|
185
|
+
# @raise [EmployeeNotFoundError] if the manager could not be found.
|
186
|
+
# @see [PeopleGroup::Connectors::Hris#fetch_manager]
|
118
187
|
def fetch_manager!(team_member)
|
119
188
|
manager = fetch_manager(team_member)
|
120
189
|
raise EmployeeNotFoundError, "Manager not found for employee #{team_member['employeeNumber']}" if manager.nil?
|
@@ -122,10 +191,20 @@ module PeopleGroup
|
|
122
191
|
manager
|
123
192
|
end
|
124
193
|
|
194
|
+
# Find the second level manager of a team member.
|
195
|
+
# 1. Find the team member's manager.
|
196
|
+
# 2. Find the manager's manager.
|
197
|
+
# @param team_member [Hash] the team member's details.
|
198
|
+
# @return [Hash|nil] the second level manager's details or nil.
|
125
199
|
def fetch_second_level_manager(team_member)
|
126
200
|
fetch_manager(fetch_manager(team_member))
|
127
201
|
end
|
128
202
|
|
203
|
+
# Find the second level manager of a team member, raise error if not found.
|
204
|
+
# @param team_member [Hash] the team member's details.
|
205
|
+
# @return [Hash|nil] the second level manager's details or nil.
|
206
|
+
# @raise [EmployeeNotFoundError] if the second level manager could not be found.
|
207
|
+
# @see [PeopleGroup::Connectors::Hris#fetch_second_level_manager]
|
129
208
|
def fetch_second_level_manager!(team_member)
|
130
209
|
manager = fetch_second_level_manager(team_member)
|
131
210
|
raise EmployeeNotFoundError, "No second level manager found for employee #{team_member['employeeNumber']}" if manager.nil?
|
@@ -133,37 +212,12 @@ module PeopleGroup
|
|
133
212
|
manager
|
134
213
|
end
|
135
214
|
|
136
|
-
|
137
|
-
|
138
|
-
end
|
139
|
-
alias_method :create_team_member, :create_employee
|
140
|
-
|
141
|
-
def update_employee(employee_number, employee_details_hash)
|
142
|
-
# Only used in activate_team_members.rb, which is not needed in Workday
|
143
|
-
@bamboo.update_employee(employee_number, employee_details_hash)
|
144
|
-
end
|
145
|
-
alias_method :update_team_member, :update_employee
|
146
|
-
|
215
|
+
# List entities from Workday.
|
216
|
+
# @return [Array<Hash>] list of entities from Workday.
|
147
217
|
def locations
|
148
218
|
@workday.locations
|
149
219
|
end
|
150
220
|
|
151
|
-
def add_stock_options(employee_number, data)
|
152
|
-
@bamboo.add_stock_options(employee_number, data)
|
153
|
-
end
|
154
|
-
|
155
|
-
def add_job_details(employee_number, data)
|
156
|
-
@bamboo.add_job_details(employee_number, data)
|
157
|
-
end
|
158
|
-
|
159
|
-
def update_job_details(employee_number, data)
|
160
|
-
@bamboo.update_job_details(employee_number, data)
|
161
|
-
end
|
162
|
-
|
163
|
-
def add_compensation_details(employee_number, data)
|
164
|
-
@bamboo.add_compensation_details(employee_number, data)
|
165
|
-
end
|
166
|
-
|
167
221
|
def employment_statuses(employee_number)
|
168
222
|
# Terminated/Active in one field already. Those are the statuses we need to care about
|
169
223
|
# Look for a hire date that is after the start date (not original hire date) to account for rehires
|
@@ -181,98 +235,26 @@ module PeopleGroup
|
|
181
235
|
raise NotImplementedError
|
182
236
|
end
|
183
237
|
|
184
|
-
def time_off_type(name)
|
185
|
-
# Staying in bamboo for workday first release
|
186
|
-
# Will need to have talks with pto by roots admin
|
187
|
-
@bamboo.time_off_type(name)
|
188
|
-
end
|
189
|
-
|
190
|
-
def time_off_adjustment(employee_number, options)
|
191
|
-
# Staying in bamboo for workday first release
|
192
|
-
# Will need to have talks with pto by roots admin
|
193
|
-
@bamboo.time_off_adjustment(employee_number, options)
|
194
|
-
end
|
195
|
-
|
196
|
-
def accrued_days(employee_number)
|
197
|
-
# Staying in bamboo for workday first release
|
198
|
-
# Will need to have talks with pto by roots admin
|
199
|
-
@bamboo.accrued_days(employee_number)
|
200
|
-
end
|
201
|
-
|
202
|
-
def time_off_policies
|
203
|
-
@bamboo.time_off_policies
|
204
|
-
end
|
205
|
-
|
206
|
-
def employee_time_off_policies(employee_number)
|
207
|
-
@bamboo.employee_time_off_policies(employee_number)
|
208
|
-
end
|
209
|
-
alias_method :team_member_time_off_policies, :employee_time_off_policies
|
210
|
-
|
211
|
-
def add_time_off_policy(employee_number, time_off_policy_id, accrual_start_date)
|
212
|
-
@bamboo.add_time_off_policy(employee_number, time_off_policy_id, accrual_start_date)
|
213
|
-
end
|
214
|
-
|
215
|
-
def remove_time_off_policy(employee_number, time_off_policy_id)
|
216
|
-
@bamboo.remove_time_off_policy(employee_number, time_off_policy_id)
|
217
|
-
end
|
218
|
-
|
219
238
|
def job_details(employee_number)
|
220
239
|
warn '[DEPRECATED] PeopleGroup::Connectors::Hris#job_details, use a custom workday report instead.'
|
221
240
|
Workday::Report.call(url: ENV.fetch('WORKDAY_MANAGER_REPORT', nil))
|
222
241
|
end
|
223
242
|
|
224
|
-
def resumes_folder_id(employee_number)
|
225
|
-
@bamboo.resumes_folder_id(employee_number)
|
226
|
-
end
|
227
|
-
|
228
|
-
def contract_folder_id(employee_number)
|
229
|
-
@bamboo.contract_folder_id(employee_number)
|
230
|
-
end
|
231
|
-
|
232
|
-
def add_file(employee_number, file_name, file, folder_id)
|
233
|
-
@bamboo.add_file(employee_number, file_name, file, folder_id)
|
234
|
-
end
|
235
|
-
|
236
|
-
def add_employment_status(employee_number, data)
|
237
|
-
# Only used in parental_pto_to_bamboo.rb
|
238
|
-
# That integration adds the following statuses: Parental Leave, End of Parental Leave, Active
|
239
|
-
# Asked what to do about this one on Slack: https://gitlab.slack.com/archives/C02FHJ9BYTZ/p1650666853127429
|
240
|
-
|
241
|
-
raise NotImplementedError
|
242
|
-
end
|
243
|
-
|
244
|
-
def add_currency_conversion(employee_number, data)
|
245
|
-
@bamboo.add_currency_conversion(employee_number, data)
|
246
|
-
end
|
247
|
-
|
248
|
-
def add_on_target_earnings(employee_number, data)
|
249
|
-
@bamboo.add_on_target_earnings(employee_number, data)
|
250
|
-
end
|
251
|
-
|
252
|
-
def add_signing_bonus(employee_number, data)
|
253
|
-
@bamboo.add_signing_bonus(employee_number, data)
|
254
|
-
end
|
255
|
-
|
256
|
-
def add_family_member(employee_number, data)
|
257
|
-
@bamboo.add_family_member(employee_number, data)
|
258
|
-
end
|
259
|
-
|
260
|
-
def add_additional_data(employee_number, data)
|
261
|
-
@bamboo.add_additional_data(employee_number, data)
|
262
|
-
end
|
263
|
-
|
264
|
-
def additional_data(employee_number)
|
265
|
-
@bamboo.additional_data(employee_number)
|
266
|
-
end
|
267
|
-
|
268
243
|
def add_bonus(employee_number, comment)
|
269
244
|
@workday.add_bonus(employee_number, comment)
|
270
245
|
end
|
271
246
|
|
247
|
+
# List active departments from Workday.
|
248
|
+
# @return [Array<Hash>] list of departments from Workday.
|
272
249
|
def departments
|
273
250
|
@workday.departments
|
274
251
|
end
|
275
252
|
|
253
|
+
# Check team member eligibility status based on salary ranges.
|
254
|
+
# @param eid [String|Integer] the team members employee number
|
255
|
+
# @param salary_min [Integer] the minimum salary.
|
256
|
+
# @param salary_max [Integer] the maximum salary.
|
257
|
+
# @return [Hash] details regarding their eligibility status.
|
276
258
|
def eligibility_status(eid, salary_min, salary_max)
|
277
259
|
@workday.eligibility_status(eid, salary_min, salary_max)
|
278
260
|
end
|
@@ -20,9 +20,9 @@ module PeopleGroup
|
|
20
20
|
# Adds a bonus to the team members
|
21
21
|
# @param employee_number [Integer] The Employee Number of the team member receiving the bonus.
|
22
22
|
# @param comment [String] The comment to leave on the business process.
|
23
|
-
def add_bonus(employee_number, comment)
|
23
|
+
def add_bonus(employee_number, comment: '', auto_complete: false)
|
24
24
|
options = {
|
25
|
-
**business_process_params(comment),
|
25
|
+
**business_process_params(comment: comment, auto_complete: auto_complete),
|
26
26
|
**one_time_payment_data(employee_number, comment)
|
27
27
|
}
|
28
28
|
|
@@ -38,7 +38,7 @@ module PeopleGroup
|
|
38
38
|
# @param auto_complete: [Boolean] Defaults to true to apply this bonus without needing approval.
|
39
39
|
#
|
40
40
|
# @return [Hash] The <wd:Business_Process_Parameters> for this request.
|
41
|
-
def business_process_params(comment
|
41
|
+
def business_process_params(comment:, run_now: true, auto_complete: true)
|
42
42
|
{
|
43
43
|
'Business_Process_Parameters' => {
|
44
44
|
'Auto_Complete' => auto_complete,
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: peoplegroup-connectors
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- lien van den steen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: bamboozled-gitlab
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 0.2.14
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 0.2.14
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: gitlab
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,7 +108,7 @@ dependencies:
|
|
122
108
|
- - "~>"
|
123
109
|
- !ruby/object:Gem::Version
|
124
110
|
version: '0.1'
|
125
|
-
description: Avoid repeating methods in different projects for our
|
111
|
+
description: Avoid repeating methods in different projects for our connectors.
|
126
112
|
email:
|
127
113
|
- lvandensteen@gitlab.com
|
128
114
|
executables: []
|
@@ -132,7 +118,6 @@ files:
|
|
132
118
|
- LICENSE.txt
|
133
119
|
- README.md
|
134
120
|
- lib/peoplegroup/connectors.rb
|
135
|
-
- lib/peoplegroup/connectors/bamboo.rb
|
136
121
|
- lib/peoplegroup/connectors/gitlab.rb
|
137
122
|
- lib/peoplegroup/connectors/google_sheets.rb
|
138
123
|
- lib/peoplegroup/connectors/greenhouse.rb
|
@@ -171,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
156
|
- !ruby/object:Gem::Version
|
172
157
|
version: '0'
|
173
158
|
requirements: []
|
174
|
-
rubygems_version: 3.5.
|
159
|
+
rubygems_version: 3.5.23
|
175
160
|
signing_key:
|
176
161
|
specification_version: 4
|
177
162
|
summary: Library for our shared connectors.
|
@@ -1,355 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'bamboozled'
|
4
|
-
|
5
|
-
module PeopleGroup
|
6
|
-
module Connectors
|
7
|
-
class Bamboo
|
8
|
-
attr_accessor :client
|
9
|
-
|
10
|
-
EmployeeNotFoundError = Class.new(StandardError)
|
11
|
-
|
12
|
-
def initialize(use_report: false)
|
13
|
-
@use_report = use_report
|
14
|
-
@client = Bamboozled.client(subdomain: 'gitlab', api_key: ENV['BAMBOO_API_KEY'])
|
15
|
-
end
|
16
|
-
|
17
|
-
def get_employee_details(id)
|
18
|
-
employees.find { |emp| emp['employeeNumber'] == id.to_s }
|
19
|
-
end
|
20
|
-
alias_method :get_team_member_details, :get_employee_details
|
21
|
-
|
22
|
-
def get_employee_details!(id)
|
23
|
-
employee_details = get_employee_details(id)
|
24
|
-
raise EmployeeNotFoundError, "No team member found with employee number #{id}" if employee_details.nil?
|
25
|
-
|
26
|
-
employee_details
|
27
|
-
end
|
28
|
-
alias_method :get_team_member_details!, :get_employee_details!
|
29
|
-
|
30
|
-
def bamboo_id!(employee_number)
|
31
|
-
get_team_member_details!(employee_number)['id']
|
32
|
-
end
|
33
|
-
|
34
|
-
def search_employee(name)
|
35
|
-
return if name.empty?
|
36
|
-
|
37
|
-
employees.find do |emp|
|
38
|
-
[
|
39
|
-
emp['displayName']&.downcase,
|
40
|
-
"#{emp['firstName']&.downcase} #{emp['lastName']&.downcase}",
|
41
|
-
"#{emp['preferredName']&.downcase} #{emp['lastName']&.downcase}",
|
42
|
-
"#{emp['firstName']&.downcase} #{emp['customPreferredLastName']&.downcase}",
|
43
|
-
"#{emp['preferredName']&.downcase} #{emp['customPreferredLastName']&.downcase}",
|
44
|
-
emp['fullName5']&.downcase # this is firstName middleName lastName
|
45
|
-
].include?(name.downcase)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
alias_method :search_team_member, :search_employee
|
49
|
-
|
50
|
-
def search_employee!(name)
|
51
|
-
employee = search_employee(name)
|
52
|
-
raise EmployeeNotFoundError, "No employee found with name #{name}" if employee.nil?
|
53
|
-
|
54
|
-
employee
|
55
|
-
end
|
56
|
-
alias_method :search_team_member!, :search_employee!
|
57
|
-
|
58
|
-
def search_employee_by_field(field:, value:)
|
59
|
-
employees.find { |employee| employee[field] == value.to_s }
|
60
|
-
end
|
61
|
-
alias_method :search_team_member_by_field, :search_employee_by_field
|
62
|
-
|
63
|
-
def search_employee_by_field!(field:, value:)
|
64
|
-
employee = search_employee_by_field(field: field, value: value)
|
65
|
-
raise EmployeeNotFoundError, "No employee found with #{field}: #{value}" if employee.nil?
|
66
|
-
|
67
|
-
employee
|
68
|
-
end
|
69
|
-
alias_method :search_team_member_by_field!, :search_employee_by_field!
|
70
|
-
|
71
|
-
# Find the associated team member without checking case on the 'workEmail' field.
|
72
|
-
def search_team_member_by_email(email)
|
73
|
-
team_members.find { |team_member| team_member['workEmail']&.downcase == email&.downcase }
|
74
|
-
end
|
75
|
-
alias_method :slack_email_lookup_with_fallback, :search_team_member_by_email
|
76
|
-
|
77
|
-
def search_team_member_by_email!(email)
|
78
|
-
team_member = search_team_member_by_email(email)
|
79
|
-
raise EmployeeNotFoundError, "No team member found with email #{email}." unless team_member
|
80
|
-
|
81
|
-
team_member
|
82
|
-
end
|
83
|
-
alias_method :slack_email_lookup_with_fallback!, :search_team_member_by_email!
|
84
|
-
|
85
|
-
def team_members_by_department(department)
|
86
|
-
active_and_current_team_members.select { |team_member| team_member['department'] == department }
|
87
|
-
end
|
88
|
-
|
89
|
-
def team_members_by_division(division)
|
90
|
-
active_and_current_team_members.select { |team_member| team_member['division'] == division }
|
91
|
-
end
|
92
|
-
|
93
|
-
def fetch_manager(team_member)
|
94
|
-
active_team_members.find { |tm| tm['employeeNumber'] == team_member['supervisorId'] }
|
95
|
-
end
|
96
|
-
|
97
|
-
def fetch_manager!(team_member)
|
98
|
-
manager = fetch_manager(team_member)
|
99
|
-
raise EmployeeNotFoundError, "Manager not found for employee #{team_member['employeeNumber']}" if manager.nil?
|
100
|
-
|
101
|
-
manager
|
102
|
-
end
|
103
|
-
|
104
|
-
def fetch_second_level_manager(team_member)
|
105
|
-
fetch_manager(fetch_manager(team_member))
|
106
|
-
end
|
107
|
-
|
108
|
-
def fetch_second_level_manager!(team_member)
|
109
|
-
manager = fetch_second_level_manager(team_member)
|
110
|
-
raise EmployeeNotFoundError, "No second level manager found for employee #{team_member['employeeNumber']}" if manager.nil?
|
111
|
-
|
112
|
-
manager
|
113
|
-
end
|
114
|
-
|
115
|
-
def create_employee(employee_details_hash)
|
116
|
-
invalidate_cache
|
117
|
-
retry_on_error { @client.employee.add(employee_details_hash) }
|
118
|
-
end
|
119
|
-
alias_method :create_team_member, :create_employee
|
120
|
-
|
121
|
-
def update_employee(employee_number, employee_details_hash)
|
122
|
-
id = bamboo_id!(employee_number)
|
123
|
-
invalidate_cache
|
124
|
-
retry_on_error { @client.employee.update(id, employee_details_hash) }
|
125
|
-
end
|
126
|
-
alias_method :update_team_member, :update_employee
|
127
|
-
|
128
|
-
def locations
|
129
|
-
meta_fields.detect { |res| res['name'] == 'Location' }['options'].each_with_object([]) { |option, array| array << option['name'] if option['archived'] == 'no' } || []
|
130
|
-
end
|
131
|
-
|
132
|
-
def employees
|
133
|
-
return @employees unless @employees.nil?
|
134
|
-
|
135
|
-
report = retry_on_error do
|
136
|
-
if @use_report
|
137
|
-
@client.report.find(@use_report, 'JSON', true)
|
138
|
-
else
|
139
|
-
@client.report.custom(fields, 'JSON')
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
filtered = report.reject { |team_member| team_member['lastName'] == 'Test-Gitlab' }
|
144
|
-
@employees = filtered.map { |team_member| format_team_member(team_member) }
|
145
|
-
end
|
146
|
-
alias_method :team_members, :employees
|
147
|
-
|
148
|
-
def active_employees
|
149
|
-
employees.select { |employee| employee['status'] == 'Active' }
|
150
|
-
end
|
151
|
-
alias_method :active_team_members, :active_employees
|
152
|
-
|
153
|
-
def active_and_current_employees
|
154
|
-
today = Date.current
|
155
|
-
employees.select do |employee|
|
156
|
-
employee['status'] == 'Active' && Date.parse(employee['hireDate']) <= today
|
157
|
-
end
|
158
|
-
end
|
159
|
-
alias_method :active_and_current_team_members, :active_and_current_employees
|
160
|
-
|
161
|
-
def add_stock_options(employee_number, data)
|
162
|
-
id = bamboo_id!(employee_number)
|
163
|
-
retry_on_error { @client.employee.add_table_row(id, 'customEquity', data) }
|
164
|
-
end
|
165
|
-
|
166
|
-
def add_job_details(employee_number, data)
|
167
|
-
id = bamboo_id!(employee_number)
|
168
|
-
retry_on_error { @client.employee.add_table_row(id, 'jobInfo', data) }
|
169
|
-
end
|
170
|
-
|
171
|
-
def update_job_details(employee_number, data)
|
172
|
-
id = bamboo_id!(employee_number)
|
173
|
-
current_data = job_details(employee_number) # it should only be one row as we just created this user
|
174
|
-
row_id = current_data.first['id']
|
175
|
-
retry_on_error { @client.employee.update_table_row(id, 'jobInfo', row_id, data) }
|
176
|
-
end
|
177
|
-
|
178
|
-
def add_compensation_details(employee_number, data)
|
179
|
-
id = bamboo_id!(employee_number)
|
180
|
-
retry_on_error { @client.employee.add_table_row(id, 'compensation', data) }
|
181
|
-
end
|
182
|
-
|
183
|
-
def employment_statuses(employee_number)
|
184
|
-
id = bamboo_id!(employee_number)
|
185
|
-
retry_on_error { @client.employee.table_data(id, 'employmentStatus') }
|
186
|
-
end
|
187
|
-
|
188
|
-
def time_off_type(name)
|
189
|
-
time_off_types['timeOffTypes'].find { |type| type['name'] == name }
|
190
|
-
end
|
191
|
-
|
192
|
-
def time_off_adjustment(employee_number, options)
|
193
|
-
id = bamboo_id!(employee_number)
|
194
|
-
retry_on_error { @client.employee.time_off_balance_adjustment(id, options) }
|
195
|
-
end
|
196
|
-
|
197
|
-
def accrued_days(employee_number)
|
198
|
-
employee_accruals_type = time_off_type('Employee Accruals')
|
199
|
-
total_accruals = time_off_estimate(employee_number).find { |type| type['timeOffType'] == employee_accruals_type['id'] }
|
200
|
-
|
201
|
-
total_accruals['balance'].to_f
|
202
|
-
end
|
203
|
-
|
204
|
-
def time_off_policies
|
205
|
-
@time_off_policies ||= retry_on_error { @client.meta.time_off_policies }
|
206
|
-
end
|
207
|
-
|
208
|
-
def employee_time_off_policies(employee_number)
|
209
|
-
id = bamboo_id!(employee_number)
|
210
|
-
retry_on_error { @client.employee.time_off_policies(id) }
|
211
|
-
end
|
212
|
-
alias_method :team_member_time_off_policies, :employee_time_off_policies
|
213
|
-
|
214
|
-
def add_time_off_policy(employee_number, time_off_policy_id, accrual_start_date)
|
215
|
-
policies = [{ timeOffPolicyId: time_off_policy_id, accrualStartDate: accrual_start_date }]
|
216
|
-
id = bamboo_id!(employee_number)
|
217
|
-
|
218
|
-
retry_on_error { @client.employee.add_time_off_policies(id, policies) }
|
219
|
-
end
|
220
|
-
|
221
|
-
def remove_time_off_policy(employee_number, time_off_policy_id)
|
222
|
-
# A nil accrual start date removes a policy assignment
|
223
|
-
# Reference: https://documentation.bamboohr.com/reference#time-off-assign-time-off-policies-for-an-employee
|
224
|
-
add_time_off_policy(employee_number, time_off_policy_id, nil)
|
225
|
-
end
|
226
|
-
|
227
|
-
def job_details(employee_number)
|
228
|
-
id = bamboo_id!(employee_number)
|
229
|
-
retry_on_error { @client.employee.table_data(id, 'jobInfo') }
|
230
|
-
end
|
231
|
-
|
232
|
-
def resumes_folder_id(employee_number)
|
233
|
-
@resumes_folder_id ||= files(employee_number)['categories'].find { |folder| folder['name'] == 'Resumes and Applications' }['id']
|
234
|
-
end
|
235
|
-
|
236
|
-
def contract_folder_id(employee_number)
|
237
|
-
@contract_folder_id ||= files(employee_number)['categories'].find { |folder| folder['name'] == 'Contracts & Changes' }['id']
|
238
|
-
end
|
239
|
-
|
240
|
-
def add_file(employee_number, file_name, file, folder_id)
|
241
|
-
id = bamboo_id!(employee_number)
|
242
|
-
|
243
|
-
options = {
|
244
|
-
category: folder_id,
|
245
|
-
fileName: file_name,
|
246
|
-
share: 'yes',
|
247
|
-
file: file
|
248
|
-
}
|
249
|
-
retry_on_error { @client.employee.add_file(id, options) }
|
250
|
-
end
|
251
|
-
|
252
|
-
def add_employment_status(employee_number, data)
|
253
|
-
id = bamboo_id!(employee_number)
|
254
|
-
retry_on_error { @client.employee.add_table_row(id, 'employmentStatus', data) }
|
255
|
-
end
|
256
|
-
|
257
|
-
def add_currency_conversion(employee_number, data)
|
258
|
-
id = bamboo_id!(employee_number)
|
259
|
-
retry_on_error { @client.employee.add_table_row(id, 'customCurrencyConversion', data) }
|
260
|
-
end
|
261
|
-
|
262
|
-
def add_on_target_earnings(employee_number, data)
|
263
|
-
id = bamboo_id!(employee_number)
|
264
|
-
retry_on_error { @client.employee.add_table_row(id, 'customOnTargetEarnings', data) }
|
265
|
-
end
|
266
|
-
|
267
|
-
def add_signing_bonus(employee_number, data)
|
268
|
-
id = bamboo_id!(employee_number)
|
269
|
-
retry_on_error { @client.employee.add_table_row(id, 'customBonus', data) }
|
270
|
-
end
|
271
|
-
|
272
|
-
def add_family_member(employee_number, data)
|
273
|
-
id = bamboo_id!(employee_number)
|
274
|
-
retry_on_error { @client.employee.add_table_row(id, 'customFamilyMember', data) }
|
275
|
-
end
|
276
|
-
|
277
|
-
def add_additional_data(employee_number, data)
|
278
|
-
id = bamboo_id!(employee_number)
|
279
|
-
retry_on_error { @client.employee.add_table_row(id, 'customAdditionalInformation1', data) }
|
280
|
-
end
|
281
|
-
|
282
|
-
def additional_data(employee_number)
|
283
|
-
id = bamboo_id!(employee_number)
|
284
|
-
retry_on_error { @client.employee.table_data(id, 'employmentStatus') }
|
285
|
-
end
|
286
|
-
|
287
|
-
def add_bonus(employee_number, comment)
|
288
|
-
team_member = search_employee_by_field(field: 'employeeNumber', value: employee_number)
|
289
|
-
data = {
|
290
|
-
customBonusdate: Date.today.to_s,
|
291
|
-
customBonusamount: { value: 1_000, currency: 'USD' },
|
292
|
-
customBonustype: 'Discretionary Bonus',
|
293
|
-
customBonuscomments: comment
|
294
|
-
# customNominatedBy: 'TODO'
|
295
|
-
}
|
296
|
-
retry_on_error { @client.employee.add_table_row(team_member['id'], 'customBonus', data) }
|
297
|
-
end
|
298
|
-
|
299
|
-
private
|
300
|
-
|
301
|
-
def fields
|
302
|
-
return @fields if @fields
|
303
|
-
|
304
|
-
meta_field_aliases = retry_on_error { @client.meta.fields }.map { |f| f['alias'] }
|
305
|
-
@fields = (Bamboozled::API::FieldCollection.all_names + meta_field_aliases).compact.uniq
|
306
|
-
@fields.delete('flsaCode') # temp fix for problems with BambooHR
|
307
|
-
@fields
|
308
|
-
end
|
309
|
-
|
310
|
-
def meta_fields
|
311
|
-
@meta_fields ||= retry_on_error { @client.meta.lists }
|
312
|
-
end
|
313
|
-
|
314
|
-
def time_off_types
|
315
|
-
@time_off_types ||= retry_on_error { @client.meta.time_off_types }
|
316
|
-
end
|
317
|
-
|
318
|
-
def time_off_estimate(employee_number, end_date = Date.today)
|
319
|
-
id = bamboo_id!(employee_number)
|
320
|
-
retry_on_error { @client.employee.time_off_estimate(id, end_date) }
|
321
|
-
end
|
322
|
-
|
323
|
-
def files(employee_number)
|
324
|
-
id = bamboo_id!(employee_number)
|
325
|
-
@files ||= retry_on_error { @client.employee.files(id) }
|
326
|
-
end
|
327
|
-
|
328
|
-
def retry_on_error(&block)
|
329
|
-
Utils.retry_on_error(errors: [Net::ReadTimeout, Bamboozled::GatewayError, Bamboozled::ServiceUnavailable], delay: 3, &block)
|
330
|
-
end
|
331
|
-
|
332
|
-
def invalidate_cache
|
333
|
-
@employees = nil
|
334
|
-
end
|
335
|
-
|
336
|
-
MALFORMED_FIELDS = [
|
337
|
-
'customExportName/LocationtoTeamPage?',
|
338
|
-
'customI-9Processed',
|
339
|
-
'customJobTitleSpecialty(Multi-Select)'
|
340
|
-
].freeze
|
341
|
-
|
342
|
-
# Convert BHR team member to Workday.
|
343
|
-
def format_team_member(team_member)
|
344
|
-
return nil if team_member.nil?
|
345
|
-
|
346
|
-
{
|
347
|
-
**team_member.except(*MALFORMED_FIELDS),
|
348
|
-
'customExportNameLocationtoTeamPage' => team_member['customExportName/LocationtoTeamPage?'] || 'No',
|
349
|
-
'customJobTitleSpecialtyMultiSelect' => team_member['customJobTitleSpecialty(Multi-Select)'],
|
350
|
-
'customI9Processed' => team_member['customI-9Processed']
|
351
|
-
}
|
352
|
-
end
|
353
|
-
end
|
354
|
-
end
|
355
|
-
end
|