peoplegroup-connectors 1.0.5 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|