operto 0.1.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 +7 -0
- data/lib/operto/access_tokens/upsert.rb +63 -0
- data/lib/operto/client.rb +81 -0
- data/lib/operto/config.rb +13 -0
- data/lib/operto/error.rb +6 -0
- data/lib/operto/filtered_pagination.rb +35 -0
- data/lib/operto/homes/index.rb +54 -0
- data/lib/operto/issue_types.rb +11 -0
- data/lib/operto/issues/close.rb +29 -0
- data/lib/operto/issues/index.rb +71 -0
- data/lib/operto/memory_token_store.rb +31 -0
- data/lib/operto/operation.rb +31 -0
- data/lib/operto/staff/index.rb +59 -0
- data/lib/operto/staff_task_times/index.rb +51 -0
- data/lib/operto/staff_tasks/create.rb +33 -0
- data/lib/operto/staff_tasks/destroy.rb +23 -0
- data/lib/operto/tasks/create.rb +34 -0
- data/lib/operto/tasks/index.rb +70 -0
- data/lib/operto/tasks/shared.rb +44 -0
- data/lib/operto/tasks/update.rb +27 -0
- data/lib/operto/version.rb +3 -0
- data/lib/operto.rb +41 -0
- metadata +129 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0cf8a26aabacf6a653ef155a836d95ef727b29794a0f1fbdf1365d2306ba0a4b
|
|
4
|
+
data.tar.gz: 300fc9d056683aa3d1cedc322b887b263b454c4449a84592875484624dafd320
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 12713010ba3c3b7b909e1c54e959bf00411647352f959862446f54c40ad443461af683c141057f0cf59e192191cb8866ee04c82bba929ddad6b7857fa3c2ae4f
|
|
7
|
+
data.tar.gz: e609b53f901d429bc8957decbf04029e26b815e7ae266f1929d3d3000810026a10025111c9b2ab3854987d12aded73cdc23d1175a2253d15a10e4a9c12a7beaa
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module AccessTokens
|
|
3
|
+
# Ensures a live access token: reuse the stored one, else refresh, else log
|
|
4
|
+
# in fresh. All persistence/caching/expiry policy lives in the injected
|
|
5
|
+
# token store (Operto.config.token_store).
|
|
6
|
+
class Upsert
|
|
7
|
+
include Operto::Operation
|
|
8
|
+
|
|
9
|
+
OAUTH_ENDPOINT = 'oauth/login'.freeze
|
|
10
|
+
REFRESH_ENDPOINT = 'oauth/refresh'.freeze
|
|
11
|
+
|
|
12
|
+
# @rbs () -> Dry::Monads::Result[untyped]
|
|
13
|
+
def call
|
|
14
|
+
token = store.active
|
|
15
|
+
return Success(token) if token
|
|
16
|
+
|
|
17
|
+
attempt_refresh || request_new_token
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
Failure(e)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def store
|
|
25
|
+
Operto.config.token_store
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def attempt_refresh
|
|
29
|
+
token = store.refreshable
|
|
30
|
+
return unless token
|
|
31
|
+
|
|
32
|
+
response = Operto::Client.connection(skip_auth: true).get(REFRESH_ENDPOINT) do |req|
|
|
33
|
+
req.headers['Authorization'] = "VRS #{token.refresh_token}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
return unless Operto::Client.successful_response?(response)
|
|
37
|
+
|
|
38
|
+
Success(update_token(response.body, refresh_only: true))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def request_new_token
|
|
42
|
+
response = Operto::Client.connection(skip_auth: true).post(OAUTH_ENDPOINT) do |req|
|
|
43
|
+
req.body = { API_Key: Operto::Client.client_id, API_Value: Operto::Client.secret }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Operto::Client.handle_response(response) { |body| update_token(body) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_token(body, refresh_only: false)
|
|
50
|
+
access = body[:Access_Token]
|
|
51
|
+
attributes = { access_token: access[:token], expires_at: access[:Expiry].to_i.seconds.from_now }
|
|
52
|
+
|
|
53
|
+
unless refresh_only
|
|
54
|
+
refresh = body[:Refresh_Token]
|
|
55
|
+
attributes[:refresh_token] = refresh[:token]
|
|
56
|
+
attributes[:refresh_expires_at] = refresh[:Expiry].to_i.seconds.from_now
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
store.save(**attributes)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
# The single HTTP gateway: Faraday connection, auth header, retry, and Operto's
|
|
3
|
+
# quirky response envelope (a 200 with ReasonCode >= 300 is a failure).
|
|
4
|
+
module Client
|
|
5
|
+
extend self
|
|
6
|
+
include Dry::Monads[:result]
|
|
7
|
+
|
|
8
|
+
RETRY_OPTIONS = {
|
|
9
|
+
max: 3,
|
|
10
|
+
interval: 1.0,
|
|
11
|
+
interval_randomness: 0.5,
|
|
12
|
+
backoff_factor: 2,
|
|
13
|
+
retry_statuses: [401, 429, 502, 503, 504],
|
|
14
|
+
exceptions: [*Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS, Faraday::ConnectionFailed],
|
|
15
|
+
methods: [*Faraday::Retry::Middleware::IDEMPOTENT_METHODS, :patch, :post],
|
|
16
|
+
retry_block: ->(env:, **) { Operto.config.token_store.clear if env.status == 401 }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def connection(url: Operto.config.base_url, skip_auth: false)
|
|
20
|
+
Faraday.new(url:) do |builder|
|
|
21
|
+
builder.request :authorization, 'VRS', -> { auth_token } unless skip_auth
|
|
22
|
+
|
|
23
|
+
builder.request :json
|
|
24
|
+
builder.request :retry, RETRY_OPTIONS
|
|
25
|
+
builder.request :url_encoded
|
|
26
|
+
builder.response :json, parser_options: { symbolize_names: true, mode: :rails, decoder: [Oj, :load] }
|
|
27
|
+
builder.options[:timeout] = 20
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def handle_response(response, &block)
|
|
32
|
+
return Failure(formatted_error(response)) unless successful_response?(response)
|
|
33
|
+
|
|
34
|
+
Success(block ? block.call(response_body(response)) : true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def successful_response?(response)
|
|
38
|
+
body = response_body(response)
|
|
39
|
+
reason_code = body[:ReasonCode]
|
|
40
|
+
|
|
41
|
+
response.success? && (reason_code.nil? || reason_code.to_i < 300)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def formatted_error(response)
|
|
45
|
+
body = response_body(response)
|
|
46
|
+
reason_code = body[:ReasonCode] || response.status
|
|
47
|
+
reason_text = body[:ReasonText] || body[:Message] || response.body.to_s
|
|
48
|
+
|
|
49
|
+
Operto::Error.new("#{reason_code}: #{reason_text}")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def response_body(response)
|
|
53
|
+
body = response.body
|
|
54
|
+
return body if body.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
parsed_body_from_string(body)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parsed_body_from_string(body)
|
|
60
|
+
string_body = body.to_s
|
|
61
|
+
return {} if string_body.empty?
|
|
62
|
+
|
|
63
|
+
parsed = Oj.load(string_body, symbol_keys: true)
|
|
64
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
65
|
+
rescue StandardError
|
|
66
|
+
{}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def auth_token
|
|
70
|
+
AccessTokens::Upsert.new.call!.value!.access_token
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def client_id
|
|
74
|
+
Operto.config.client_id
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def secret
|
|
78
|
+
Operto.config.secret
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
# Injected by the host app (see the Operto.configure initializer): the API
|
|
3
|
+
# credentials, a cache, and a token store. Keeping these out of the gem is
|
|
4
|
+
# what makes it host-agnostic.
|
|
5
|
+
class Config
|
|
6
|
+
attr_accessor :client_id, :secret, :token_store, :base_url
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@base_url = 'https://teams-api.operto.com/api/v1/'.freeze
|
|
10
|
+
@token_store = MemoryTokenStore.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/operto/error.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
# Including class must define FILTER_MAPPINGS (Hash), DATE_KEYS (Array<Symbol>), DEFAULT_SORT (String).
|
|
3
|
+
module FilteredPagination
|
|
4
|
+
FILTER_MAPPINGS = {}.freeze
|
|
5
|
+
DATE_KEYS = [].freeze
|
|
6
|
+
DEFAULT_SORT = ''.freeze
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def validate_arguments!(attributes)
|
|
11
|
+
argument! attributes: :invalid unless attributes.is_a?(Hash) && attributes.present? && attributes.values.none?(&:blank?)
|
|
12
|
+
|
|
13
|
+
invalid_keys = attributes.keys - self.class::FILTER_MAPPINGS.keys
|
|
14
|
+
argument! attributes: :invalid_keys if invalid_keys.any?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_query_params(attributes, skip, take)
|
|
18
|
+
params = {
|
|
19
|
+
per_page: take,
|
|
20
|
+
page: skip.div(take) + 1,
|
|
21
|
+
Sort: self.class::DEFAULT_SORT
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
attributes.each do |key, value|
|
|
25
|
+
params[self.class::FILTER_MAPPINGS[key]] = format_attribute_value(key, value)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
params
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def format_attribute_value(key, value)
|
|
32
|
+
self.class::DATE_KEYS.include?(key) ? value.strftime('%Y%m%d') : value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Homes
|
|
3
|
+
class Index
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
|
|
6
|
+
DEFAULT_PAGE_SIZE = 25
|
|
7
|
+
|
|
8
|
+
# @rbs (?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
9
|
+
def call(skip: 0, take: DEFAULT_PAGE_SIZE)
|
|
10
|
+
query_params = build_query_params(skip, take)
|
|
11
|
+
response = Operto::Client.connection.get('properties', query_params)
|
|
12
|
+
Operto::Client.handle_response(response) { |body| format_results(body) }
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
Failure(e)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def build_query_params(skip, take)
|
|
20
|
+
page = skip.div(take) + 1
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
per_page: take,
|
|
24
|
+
page:
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def format_results(body)
|
|
29
|
+
results = body.fetch(:data, []).map { |home| format_home(home) }
|
|
30
|
+
|
|
31
|
+
formatted = body.dup
|
|
32
|
+
formatted[:results] = results
|
|
33
|
+
formatted[:count] = body[:total_items] || results.size
|
|
34
|
+
formatted
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_home(home)
|
|
38
|
+
normalized = home.transform_keys { |key| key.to_s.underscore.to_sym }
|
|
39
|
+
|
|
40
|
+
normalized.merge(
|
|
41
|
+
created_at: parse_date(normalized[:create_date])
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_date(value)
|
|
46
|
+
return if value.blank?
|
|
47
|
+
|
|
48
|
+
Date.strptime(value.to_s, '%Y%m%d')
|
|
49
|
+
rescue ArgumentError
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Issues
|
|
3
|
+
class Close
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
|
|
6
|
+
# @rbs (issue_id: ::Integer | ::String) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
7
|
+
def call(issue_id:)
|
|
8
|
+
argument! issue_id: :required if issue_id.blank?
|
|
9
|
+
|
|
10
|
+
response = Operto::Client.connection.post('issues', close_body(issue_id))
|
|
11
|
+
Operto::Client.handle_response(response) { |body| { issue_id: body.dig(:Data, :IssueID) } }
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
Failure(e)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def close_body(issue_id)
|
|
19
|
+
{
|
|
20
|
+
request: {
|
|
21
|
+
IssueID: issue_id,
|
|
22
|
+
Update: true,
|
|
23
|
+
ClosedDate: Date.current.strftime('%Y%m%d')
|
|
24
|
+
}.to_json
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Issues
|
|
3
|
+
class Index
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
|
|
6
|
+
# @rbs (start_date: ::Date, ?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
7
|
+
def call(start_date:, skip: 0, take: 50)
|
|
8
|
+
query_params = build_query_params(start_date, skip, take)
|
|
9
|
+
response = Operto::Client.connection.get('issues', query_params)
|
|
10
|
+
Operto::Client.handle_response(response) { |body| format_results(body) }
|
|
11
|
+
rescue StandardError => e
|
|
12
|
+
Failure(e)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_query_params(start_date, skip, take)
|
|
18
|
+
page = skip.div(take) + 1
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
per_page: take,
|
|
22
|
+
page:,
|
|
23
|
+
closed: 0,
|
|
24
|
+
CreateStartDate: start_date.strftime('%Y%m%d'),
|
|
25
|
+
Sort: 'IssueID desc'
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format_results(body)
|
|
30
|
+
results = body[:data].map { |issue| format_issue(issue) }
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
results:,
|
|
34
|
+
count: body[:total_items] || results.size
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def format_issue(issue)
|
|
39
|
+
issue_type = deduce_issue_type(issue[:IssueType])
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
issue_id: issue[:IssueID],
|
|
43
|
+
status_id: issue[:StatusID],
|
|
44
|
+
issue_type:,
|
|
45
|
+
issue_type_name: issue_type.humanize,
|
|
46
|
+
urgent: issue[:Urgent],
|
|
47
|
+
issue: issue[:Issue],
|
|
48
|
+
notes: issue[:Notes],
|
|
49
|
+
staff_notes: issue[:StaffNotes],
|
|
50
|
+
property_id: issue[:PropertyID],
|
|
51
|
+
billable: issue[:Billable],
|
|
52
|
+
create_date: issue[:CreateDate]&.to_date,
|
|
53
|
+
submitted_by_servicer_id: issue[:SubmittedByServicerID],
|
|
54
|
+
images: extract_images(issue[:Images])
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def deduce_issue_type(raw_type)
|
|
59
|
+
raw_type = -1 unless Operto::IssueTypes::ISSUE_TYPES.key?(raw_type)
|
|
60
|
+
|
|
61
|
+
Operto::IssueTypes::ISSUE_TYPES[raw_type]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_images(images)
|
|
65
|
+
Array(images).map do |image|
|
|
66
|
+
{ issue_image_id: image[:IssueImageID], image_url: image[:Image] }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
# The default token store: keeps the token in memory for the life of the
|
|
3
|
+
# process. Fine for a single long-running process (the token simply re-issues
|
|
4
|
+
# after a restart); provide a shared/persistent store for multi-process
|
|
5
|
+
# deployments.
|
|
6
|
+
class MemoryTokenStore
|
|
7
|
+
Token = Struct.new(:access_token, :refresh_token, :refresh_expires_at, :expires_at, keyword_init: true) do
|
|
8
|
+
def expired?
|
|
9
|
+
expires_at.nil? || expires_at < Time.now
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
REFRESH_BUFFER = 120
|
|
14
|
+
|
|
15
|
+
def active
|
|
16
|
+
@token if @token && !@token.expired?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def refreshable
|
|
20
|
+
@token if @token&.refresh_expires_at && @token.refresh_expires_at > Time.now + REFRESH_BUFFER
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save(access_token:, expires_at:, refresh_token: nil, refresh_expires_at: nil)
|
|
24
|
+
@token = Token.new(access_token:, expires_at:, refresh_token:, refresh_expires_at:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear
|
|
28
|
+
@token = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
# Shared monadic contract for the client operations: #call returns a Result,
|
|
3
|
+
# #call! unwraps it and raises the underlying failure.
|
|
4
|
+
module Operation
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.include Dry::Monads[:result]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call!(...)
|
|
10
|
+
result = call(...)
|
|
11
|
+
raise result.failure if result.failure?
|
|
12
|
+
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def argument!(**attrs)
|
|
19
|
+
key, reason = attrs.first
|
|
20
|
+
raise Operto::ArgumentError, "#{key}: #{reason}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def required_attribute!(key)
|
|
24
|
+
raise Operto::ArgumentError, "#{key}: required"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def invalid_attribute!(key, type:)
|
|
28
|
+
raise Operto::ArgumentError, "#{key}: expected #{type}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Staff
|
|
3
|
+
class Index
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
|
|
6
|
+
DEFAULT_PAGE_SIZE = 100
|
|
7
|
+
|
|
8
|
+
# @rbs (?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
9
|
+
def call(skip: 0, take: DEFAULT_PAGE_SIZE)
|
|
10
|
+
query_params = build_query_params(skip, take)
|
|
11
|
+
response = Operto::Client.connection.get('staff', query_params)
|
|
12
|
+
Operto::Client.handle_response(response) { |body| format_results(body) }
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
Failure(e)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def build_query_params(skip, take)
|
|
20
|
+
page = skip.div(take) + 1
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
per_page: take,
|
|
24
|
+
page:
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def format_results(body)
|
|
29
|
+
results = body.fetch(:data, []).map { |staff| format_staff(staff) }
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
results:,
|
|
33
|
+
count: body[:total_items] || results.size
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_staff(staff)
|
|
38
|
+
{
|
|
39
|
+
staff_id: staff[:StaffID],
|
|
40
|
+
name: staff[:Name],
|
|
41
|
+
abbreviation: staff[:Abbreviation],
|
|
42
|
+
email: staff[:Email]&.downcase,
|
|
43
|
+
phone: staff[:Phone],
|
|
44
|
+
country_id: staff[:CountryID],
|
|
45
|
+
active: staff[:Active],
|
|
46
|
+
created_at: parse_date(staff[:CreateDate])
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_date(value)
|
|
51
|
+
return if value.blank?
|
|
52
|
+
|
|
53
|
+
Date.parse(value.to_s)
|
|
54
|
+
rescue ArgumentError
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module StaffTaskTimes
|
|
3
|
+
class Index
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
include FilteredPagination
|
|
6
|
+
|
|
7
|
+
FILTER_MAPPINGS = {
|
|
8
|
+
task_id: 'TaskID',
|
|
9
|
+
staff_id: 'StaffID',
|
|
10
|
+
property_id: 'PropertyID',
|
|
11
|
+
start_date: 'StartDate',
|
|
12
|
+
end_date: 'EndDate'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
DATE_KEYS = %i[start_date end_date].freeze
|
|
16
|
+
DEFAULT_SORT = 'StaffTaskTimeID desc'.freeze
|
|
17
|
+
|
|
18
|
+
# @rbs (?attributes: ::Hash[::Symbol, untyped], ?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
19
|
+
def call(attributes: {}, skip: 0, take: 50)
|
|
20
|
+
validate_arguments!(attributes)
|
|
21
|
+
|
|
22
|
+
query_params = build_query_params(attributes, skip, take)
|
|
23
|
+
response = Operto::Client.connection.get('stafftasktimes', query_params)
|
|
24
|
+
|
|
25
|
+
Operto::Client.handle_response(response) { |body| format_results(body) }
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
Failure(e)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def format_results(body)
|
|
33
|
+
results = body[:data].map do |entry|
|
|
34
|
+
{
|
|
35
|
+
staff_task_time_id: entry[:StaffTaskTimeID],
|
|
36
|
+
staff_id: entry[:StaffID],
|
|
37
|
+
task_id: entry[:TaskID],
|
|
38
|
+
clock_in: entry[:ClockIn]&.to_datetime,
|
|
39
|
+
clock_out: entry[:ClockOut]&.to_datetime,
|
|
40
|
+
note: entry[:Note]
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
results:,
|
|
46
|
+
count: body[:total_items] || results.size
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module StaffTasks
|
|
3
|
+
class Create
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
|
|
6
|
+
# @rbs (task_id: ::String, staff_id: ::String, property_id: ::String) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
7
|
+
def call(task_id:, staff_id:, property_id:)
|
|
8
|
+
validate_arguments!(task_id, staff_id, property_id)
|
|
9
|
+
|
|
10
|
+
response = Operto::Client.connection.post('stafftasks', request_attributes(task_id, staff_id, property_id))
|
|
11
|
+
Operto::Client.handle_response(response) { |body| { staff_task_id: body[:StaffTaskID] } }
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
Failure(e)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def validate_arguments!(task_id, staff_id, property_id)
|
|
19
|
+
argument! task_id: :required if task_id.blank?
|
|
20
|
+
argument! staff_id: :required if staff_id.blank?
|
|
21
|
+
argument! property_id: :required if property_id.blank?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def request_attributes(task_id, staff_id, property_id)
|
|
25
|
+
{
|
|
26
|
+
TaskID: task_id,
|
|
27
|
+
StaffID: staff_id,
|
|
28
|
+
PropertyID: property_id
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module StaffTasks
|
|
3
|
+
class Destroy
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
|
|
6
|
+
# @rbs (staff_task_id: ::String) -> Dry::Monads::Result[bool]
|
|
7
|
+
def call(staff_task_id:)
|
|
8
|
+
validate_arguments!(staff_task_id)
|
|
9
|
+
|
|
10
|
+
response = Operto::Client.connection.delete("stafftasks/#{staff_task_id}")
|
|
11
|
+
Operto::Client.handle_response(response)
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
Failure(e)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def validate_arguments!(staff_task_id)
|
|
19
|
+
argument! staff_task_id: :required if staff_task_id.blank?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Tasks
|
|
3
|
+
class Create
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
include Shared
|
|
6
|
+
|
|
7
|
+
# @rbs (attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
8
|
+
def call(attributes:)
|
|
9
|
+
validate_arguments!(attributes)
|
|
10
|
+
|
|
11
|
+
request_attributes = prepare_attributes(attributes)
|
|
12
|
+
response = Operto::Client.connection.post('tasks', request_attributes)
|
|
13
|
+
|
|
14
|
+
handle_task_response(response)
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
Failure(e)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate_arguments!(attributes)
|
|
22
|
+
argument! attributes: :invalid unless attributes.is_a?(Hash) && attributes.present?
|
|
23
|
+
|
|
24
|
+
required_keys.each do |key|
|
|
25
|
+
required_attribute!(key) if attributes[key].blank?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def required_keys
|
|
30
|
+
%i[house_id name start_date end_date rule_id]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Tasks
|
|
3
|
+
class Index
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
include FilteredPagination
|
|
6
|
+
|
|
7
|
+
FILTER_MAPPINGS = {
|
|
8
|
+
property_id: 'PropertyID',
|
|
9
|
+
task_rule_id: 'TaskRuleID',
|
|
10
|
+
approved_start_date: 'ApprovedStartDate',
|
|
11
|
+
approved_end_date: 'ApprovedEndDate',
|
|
12
|
+
completed_start_date: 'CompletedStartDate',
|
|
13
|
+
completed_end_date: 'CompletedEndDate',
|
|
14
|
+
task_start_date: 'TaskStartDate',
|
|
15
|
+
task_end_date: 'TaskEndDate'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
DATE_KEYS = %i[
|
|
19
|
+
approved_start_date approved_end_date
|
|
20
|
+
completed_start_date completed_end_date
|
|
21
|
+
task_start_date task_end_date
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
DEFAULT_SORT = 'TaskID desc'.freeze
|
|
25
|
+
|
|
26
|
+
# @rbs (?attributes: ::Hash[::Symbol, untyped], ?skip: ::Integer, ?take: ::Integer) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
27
|
+
def call(attributes: {}, skip: 0, take: 50)
|
|
28
|
+
validate_arguments!(attributes)
|
|
29
|
+
|
|
30
|
+
query_params = build_query_params(attributes, skip, take)
|
|
31
|
+
response = Operto::Client.connection.get('tasks', query_params)
|
|
32
|
+
Operto::Client.handle_response(response) { |body| format_results(body) }
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
Failure(e)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_results(body)
|
|
40
|
+
results = body[:data].map do |task|
|
|
41
|
+
{
|
|
42
|
+
task_id: task[:TaskID],
|
|
43
|
+
property_id: task[:PropertyID],
|
|
44
|
+
approved_at: task[:ApprovedDate]&.to_datetime,
|
|
45
|
+
completed_at: task[:CompleteConfirmedDate]&.to_datetime,
|
|
46
|
+
staff: extract_staff(task[:Staff])
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
results:,
|
|
52
|
+
count: body[:total_items] || results.size
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_staff(staff_array)
|
|
57
|
+
return [] if staff_array.blank?
|
|
58
|
+
|
|
59
|
+
staff_array.map do |staff|
|
|
60
|
+
{
|
|
61
|
+
staff_id: staff[:StaffID],
|
|
62
|
+
name: staff[:Name],
|
|
63
|
+
email: staff[:Email]&.downcase,
|
|
64
|
+
active: staff[:Active]
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Tasks
|
|
3
|
+
module Shared
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def prepare_attributes(attrs)
|
|
7
|
+
base_attrs = {
|
|
8
|
+
PropertyID: attrs[:house_id],
|
|
9
|
+
TaskRuleID: attrs[:rule_id],
|
|
10
|
+
TaskName: attrs[:name],
|
|
11
|
+
TaskDescription: attrs[:description].presence || '-'
|
|
12
|
+
}.compact_blank
|
|
13
|
+
|
|
14
|
+
base_attrs.merge(datetime_attributes(attrs))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def datetime_attributes(attrs)
|
|
18
|
+
h = {}
|
|
19
|
+
|
|
20
|
+
start_date = parse_datetime(attrs[:start_date])
|
|
21
|
+
h[:TaskDate] = h[:TaskStartDate] = as_date(start_date)
|
|
22
|
+
h[:TaskTime] = h[:TaskStartTime] = start_date.hour
|
|
23
|
+
|
|
24
|
+
complete_by = parse_datetime(attrs[:end_date])
|
|
25
|
+
h[:TaskCompleteByDate] = as_date(complete_by)
|
|
26
|
+
h[:TaskCompleteByTime] = complete_by.hour
|
|
27
|
+
|
|
28
|
+
h
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_datetime(value)
|
|
32
|
+
Time.zone.parse(value.to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def as_date(value)
|
|
36
|
+
value.strftime('%Y-%m-%d')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def handle_task_response(response)
|
|
40
|
+
Operto::Client.handle_response(response) { |body| { task_id: body[:TaskID] } }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Operto
|
|
2
|
+
module Tasks
|
|
3
|
+
class Update
|
|
4
|
+
include Operto::Operation
|
|
5
|
+
include Shared
|
|
6
|
+
|
|
7
|
+
# @rbs (task_id: ::String, attributes: ::Hash[::Symbol, untyped]) -> Dry::Monads::Result[::Hash[::Symbol, untyped]]
|
|
8
|
+
def call(task_id:, attributes:)
|
|
9
|
+
validate_arguments!(task_id, attributes)
|
|
10
|
+
|
|
11
|
+
request_attributes = prepare_attributes(attributes).compact_blank
|
|
12
|
+
response = Operto::Client.connection.put("tasks/#{task_id}", request_attributes)
|
|
13
|
+
|
|
14
|
+
handle_task_response(response)
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
Failure(e)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate_arguments!(task_id, attributes)
|
|
22
|
+
argument! task_id: :required if task_id.blank?
|
|
23
|
+
argument! attributes: :invalid if attributes.blank?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/operto.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'active_support'
|
|
2
|
+
require 'active_support/core_ext'
|
|
3
|
+
require 'dry/monads'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'faraday/retry'
|
|
6
|
+
require 'oj'
|
|
7
|
+
|
|
8
|
+
require_relative 'operto/version'
|
|
9
|
+
require_relative 'operto/error'
|
|
10
|
+
require_relative 'operto/memory_token_store'
|
|
11
|
+
require_relative 'operto/config'
|
|
12
|
+
require_relative 'operto/operation'
|
|
13
|
+
require_relative 'operto/client'
|
|
14
|
+
require_relative 'operto/issue_types'
|
|
15
|
+
require_relative 'operto/filtered_pagination'
|
|
16
|
+
require_relative 'operto/access_tokens/upsert'
|
|
17
|
+
require_relative 'operto/tasks/shared'
|
|
18
|
+
require_relative 'operto/tasks/create'
|
|
19
|
+
require_relative 'operto/tasks/update'
|
|
20
|
+
require_relative 'operto/tasks/index'
|
|
21
|
+
require_relative 'operto/staff_tasks/create'
|
|
22
|
+
require_relative 'operto/staff_tasks/destroy'
|
|
23
|
+
require_relative 'operto/staff_task_times/index'
|
|
24
|
+
require_relative 'operto/issues/index'
|
|
25
|
+
require_relative 'operto/issues/close'
|
|
26
|
+
require_relative 'operto/homes/index'
|
|
27
|
+
require_relative 'operto/staff/index'
|
|
28
|
+
|
|
29
|
+
module Operto
|
|
30
|
+
class << self
|
|
31
|
+
# @rbs () { (Config) -> void } -> void
|
|
32
|
+
def configure
|
|
33
|
+
yield(config)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs () -> Config
|
|
37
|
+
def config
|
|
38
|
+
@config ||= Config.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: operto
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Guestit
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dry-monads
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: faraday-retry
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: oj
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: HTTP client and operations for the Operto Teams API, decoupled from the
|
|
83
|
+
host application.
|
|
84
|
+
executables: []
|
|
85
|
+
extensions: []
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- lib/operto.rb
|
|
89
|
+
- lib/operto/access_tokens/upsert.rb
|
|
90
|
+
- lib/operto/client.rb
|
|
91
|
+
- lib/operto/config.rb
|
|
92
|
+
- lib/operto/error.rb
|
|
93
|
+
- lib/operto/filtered_pagination.rb
|
|
94
|
+
- lib/operto/homes/index.rb
|
|
95
|
+
- lib/operto/issue_types.rb
|
|
96
|
+
- lib/operto/issues/close.rb
|
|
97
|
+
- lib/operto/issues/index.rb
|
|
98
|
+
- lib/operto/memory_token_store.rb
|
|
99
|
+
- lib/operto/operation.rb
|
|
100
|
+
- lib/operto/staff/index.rb
|
|
101
|
+
- lib/operto/staff_task_times/index.rb
|
|
102
|
+
- lib/operto/staff_tasks/create.rb
|
|
103
|
+
- lib/operto/staff_tasks/destroy.rb
|
|
104
|
+
- lib/operto/tasks/create.rb
|
|
105
|
+
- lib/operto/tasks/index.rb
|
|
106
|
+
- lib/operto/tasks/shared.rb
|
|
107
|
+
- lib/operto/tasks/update.rb
|
|
108
|
+
- lib/operto/version.rb
|
|
109
|
+
licenses: []
|
|
110
|
+
metadata:
|
|
111
|
+
rubygems_mfa_required: 'true'
|
|
112
|
+
rdoc_options: []
|
|
113
|
+
require_paths:
|
|
114
|
+
- lib
|
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '4.0'
|
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
requirements: []
|
|
126
|
+
rubygems_version: 4.0.15
|
|
127
|
+
specification_version: 4
|
|
128
|
+
summary: Client for the Operto Teams API
|
|
129
|
+
test_files: []
|