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 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
@@ -0,0 +1,6 @@
1
+ module Operto
2
+ class Error < StandardError; end
3
+
4
+ # Raised by operations for malformed arguments before any network call.
5
+ class ArgumentError < Error; end
6
+ end
@@ -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,11 @@
1
+ module Operto
2
+ module IssueTypes
3
+ ISSUE_TYPES = {
4
+ 0 => 'damage',
5
+ 1 => 'maintenance',
6
+ 2 => 'lost_and_found',
7
+ 3 => 'supply_flag',
8
+ -1 => 'unclassified'
9
+ }.freeze
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module Operto
2
+ VERSION = '0.1.0'.freeze
3
+ 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: []