amorail 0.4.0 → 0.7.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.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/rspec.yml +23 -0
  3. data/.gitignore +2 -1
  4. data/.rubocop.yml +3 -0
  5. data/CHANGELOG.md +19 -0
  6. data/README.md +49 -8
  7. data/RELEASING.md +43 -0
  8. data/amorail.gemspec +5 -3
  9. data/lib/amorail.rb +27 -6
  10. data/lib/amorail/access_token.rb +44 -0
  11. data/lib/amorail/client.rb +84 -23
  12. data/lib/amorail/config.rb +14 -8
  13. data/lib/amorail/entities/company.rb +2 -0
  14. data/lib/amorail/entities/contact.rb +3 -0
  15. data/lib/amorail/entities/contact_link.rb +2 -0
  16. data/lib/amorail/entities/elementable.rb +4 -2
  17. data/lib/amorail/entities/lead.rb +3 -0
  18. data/lib/amorail/entities/leadable.rb +3 -0
  19. data/lib/amorail/entities/note.rb +2 -0
  20. data/lib/amorail/entities/task.rb +2 -0
  21. data/lib/amorail/entities/webhook.rb +44 -0
  22. data/lib/amorail/entity.rb +17 -3
  23. data/lib/amorail/entity/finders.rb +5 -2
  24. data/lib/amorail/entity/params.rb +3 -2
  25. data/lib/amorail/entity/persistence.rb +5 -0
  26. data/lib/amorail/exceptions.rb +2 -0
  27. data/lib/amorail/property.rb +7 -3
  28. data/lib/amorail/railtie.rb +3 -1
  29. data/lib/amorail/store_adapters.rb +15 -0
  30. data/lib/amorail/store_adapters/abstract_store_adapter.rb +23 -0
  31. data/lib/amorail/store_adapters/memory_store_adapter.rb +50 -0
  32. data/lib/amorail/store_adapters/redis_store_adapter.rb +83 -0
  33. data/lib/amorail/version.rb +3 -1
  34. data/lib/tasks/amorail.rake +2 -0
  35. data/spec/access_token_spec.rb +59 -0
  36. data/spec/client_spec.rb +36 -24
  37. data/spec/company_spec.rb +2 -0
  38. data/spec/contact_link_spec.rb +2 -0
  39. data/spec/contact_spec.rb +2 -0
  40. data/spec/entity_spec.rb +2 -0
  41. data/spec/fixtures/amorail_test.yml +5 -3
  42. data/spec/fixtures/authorize.json +6 -0
  43. data/spec/fixtures/webhooks/list.json +24 -0
  44. data/spec/fixtures/webhooks/subscribe.json +17 -0
  45. data/spec/fixtures/webhooks/unsubscribe.json +17 -0
  46. data/spec/helpers/webmock_helpers.rb +80 -13
  47. data/spec/lead_spec.rb +2 -0
  48. data/spec/my_contact_spec.rb +2 -0
  49. data/spec/note_spec.rb +2 -0
  50. data/spec/property_spec.rb +2 -0
  51. data/spec/spec_helper.rb +4 -2
  52. data/spec/store_adapters/memory_store_adapter_spec.rb +56 -0
  53. data/spec/store_adapters/redis_store_adapter_spec.rb +67 -0
  54. data/spec/support/elementable_example.rb +2 -0
  55. data/spec/support/entity_class_example.rb +2 -0
  56. data/spec/support/leadable_example.rb +2 -0
  57. data/spec/support/my_contact.rb +2 -0
  58. data/spec/support/my_entity.rb +2 -0
  59. data/spec/task_spec.rb +2 -0
  60. data/spec/webhook_spec.rb +61 -0
  61. metadata +48 -17
  62. data/.travis.yml +0 -9
@@ -1,17 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'anyway'
2
4
 
3
5
  module Amorail
4
6
  # Amorail config contains:
5
- # - usermail ("user@gmail.com")
6
- # - api_key ("13601bbac84727df")
7
7
  # - api_endpoint ("http://you_company.amocrm.com")
8
8
  # - api_path (default: "/private/api/v2/json/")
9
- # - auth_url (default: "/private/api/auth.php?type=json")
9
+ # - auth_url (default: "/oauth2/access_token")
10
10
  class Config < Anyway::Config
11
- attr_config :usermail,
12
- :api_key,
13
- :api_endpoint,
14
- api_path: "/private/api/v2/json/",
15
- auth_url: "/private/api/auth.php?type=json"
11
+ attr_config :api_endpoint,
12
+ :client_id,
13
+ :client_secret,
14
+ :code,
15
+ :redirect_uri,
16
+ :redis_url,
17
+ api_path: '/private/api/v2/json/',
18
+ auth_url: '/oauth2/access_token',
19
+ redis_host: '127.0.0.1',
20
+ redis_port: '6379',
21
+ redis_db_name: '0'
16
22
  end
17
23
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'amorail/entities/leadable'
2
4
 
3
5
  module Amorail
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'amorail/entities/leadable'
2
4
 
3
5
  module Amorail
@@ -22,6 +24,7 @@ module Amorail
22
24
 
23
25
  def company
24
26
  return if linked_company_id.nil?
27
+
25
28
  @company ||= Amorail::Company.find(linked_company_id)
26
29
  end
27
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail
2
4
  # AmoCRM contact-link join model
3
5
  class ContactLink < Amorail::Entity
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail
2
4
  # Provides common functionallity for entities
3
5
  # that can be attached to another objects.
@@ -6,9 +8,9 @@ module Amorail
6
8
 
7
9
  ELEMENT_TYPES = {
8
10
  contact: 1,
9
- lead: 2,
11
+ lead: 2,
10
12
  company: 3,
11
- task: 4
13
+ task: 4
12
14
  }.freeze
13
15
 
14
16
  included do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail
2
4
  # AmoCRM lead entity
3
5
  class Lead < Amorail::Entity
@@ -15,6 +17,7 @@ module Amorail
15
17
  # Return list of associated contacts
16
18
  def contacts
17
19
  fail NotPersisted if id.nil?
20
+
18
21
  @contacts ||=
19
22
  begin
20
23
  links = Amorail::ContactLink.find_by_leads(id)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail
2
4
  # Lead associations
3
5
  module Leadable
@@ -22,6 +24,7 @@ module Amorail
22
24
  # Return all linked leads
23
25
  def leads
24
26
  return [] if linked_leads_id.empty?
27
+
25
28
  @leads ||= Amorail::Lead.find_all(linked_leads_id)
26
29
  end
27
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'amorail/entities/elementable'
2
4
 
3
5
  module Amorail
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'amorail/entities/elementable'
2
4
 
3
5
  module Amorail
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amorail
4
+ # AmoCRM webhook entity
5
+ class Webhook < Entity
6
+ amo_names 'webhooks'
7
+
8
+ amo_field :id, :url, :events, :disabled
9
+
10
+ def self.list
11
+ response = client.safe_request(:get, remote_url('list'))
12
+
13
+ return [] if response.body.blank?
14
+
15
+ response.body['response'].fetch(amo_response_name, []).map do |attributes|
16
+ new.reload_model(attributes)
17
+ end
18
+ end
19
+
20
+ def self.subscribe(webhooks)
21
+ perform_webhooks_request('subscribe', webhooks) do |data|
22
+ data.map { |attrs| new.reload_model(attrs) }
23
+ end
24
+ end
25
+
26
+ def self.unsubscribe(webhooks)
27
+ perform_webhooks_request('unsubscribe', webhooks)
28
+ end
29
+
30
+ def self.perform_webhooks_request(action, webhooks, &block)
31
+ response = client.safe_request(
32
+ :post,
33
+ remote_url(action),
34
+ request: { webhooks: { action => webhooks } }
35
+ )
36
+
37
+ return response unless block
38
+
39
+ block.call(response.body['response'].dig(amo_response_name, 'subscribe'))
40
+ end
41
+
42
+ private_class_method :perform_webhooks_request
43
+ end
44
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model'
2
4
 
3
5
  module Amorail
@@ -32,7 +34,7 @@ module Amorail
32
34
 
33
35
  def amo_property(name, options = {})
34
36
  properties[name] = options
35
- attr_accessor(name)
37
+ attr_accessor(options.fetch(:method_name, name))
36
38
  end
37
39
 
38
40
  def attributes
@@ -77,6 +79,7 @@ module Amorail
77
79
  attrs.each do |k, v|
78
80
  action = "#{k}="
79
81
  next unless respond_to?(action)
82
+
80
83
  send(action, v)
81
84
  end
82
85
  self
@@ -84,15 +87,26 @@ module Amorail
84
87
 
85
88
  def merge_custom_fields(fields)
86
89
  return if fields.nil?
90
+
87
91
  fields.each do |f|
88
- fname = f['code'] || f['name']
92
+ fname = custom_field_name(f)
89
93
  next if fname.nil?
94
+
90
95
  fname = "#{fname.downcase}="
91
96
  fval = f.fetch('values').first.fetch('value')
92
97
  send(fname, fval) if respond_to?(fname)
93
98
  end
94
99
  end
95
100
 
101
+ def custom_field_name(field)
102
+ fname = field['code'] || field['name']
103
+ return if fname.nil?
104
+
105
+ fname = self.class.properties
106
+ .fetch(fname.downcase, {})[:method_name] || fname
107
+ fname
108
+ end
109
+
96
110
  # call safe method <safe_request>. safe_request call authorize
97
111
  # if current session undefined or expires.
98
112
  def push(method)
@@ -119,7 +133,7 @@ module Amorail
119
133
  )
120
134
  reload_model(data)
121
135
  rescue InvalidRecord
122
- return false
136
+ false
123
137
  end
124
138
  end
125
139
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail # :nodoc: all
2
4
  class Entity
3
5
  class << self
@@ -11,6 +13,7 @@ module Amorail # :nodoc: all
11
13
  def find!(id)
12
14
  rec = find(id)
13
15
  fail RecordNotFound unless rec
16
+
14
17
  rec
15
18
  end
16
19
 
@@ -32,8 +35,8 @@ module Amorail # :nodoc: all
32
35
 
33
36
  # Find AMO entities by query
34
37
  # Returns array of matching entities.
35
- def find_by_query(q)
36
- where(query: q)
38
+ def find_by_query(query)
39
+ where(query: query)
37
40
  end
38
41
 
39
42
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/hash/indifferent_access"
2
4
 
3
5
  module Amorail # :nodoc: all
@@ -19,10 +21,9 @@ module Amorail # :nodoc: all
19
21
  props = properties.send(self.class.amo_name)
20
22
 
21
23
  custom_fields = []
22
-
23
24
  self.class.properties.each do |k, v|
24
25
  prop_id = props.send(k).id
25
- prop_val = { value: send(k) }.merge(v)
26
+ prop_val = { value: send(v.fetch(:method_name, k)) }.merge(v)
26
27
  custom_fields << { id: prop_id, values: [prop_val] }
27
28
  end
28
29
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail # :nodoc: all
2
4
  class Entity
3
5
  class InvalidRecord < ::Amorail::Error; end
@@ -13,6 +15,7 @@ module Amorail # :nodoc: all
13
15
 
14
16
  def save
15
17
  return false unless valid?
18
+
16
19
  new_record? ? push('add') : push('update')
17
20
  end
18
21
 
@@ -22,6 +25,7 @@ module Amorail # :nodoc: all
22
25
 
23
26
  def update(attrs = {})
24
27
  return false if new_record?
28
+
25
29
  merge_params(attrs)
26
30
  push('update')
27
31
  end
@@ -32,6 +36,7 @@ module Amorail # :nodoc: all
32
36
 
33
37
  def reload
34
38
  fail NotPersisted if id.nil?
39
+
35
40
  load_record(id)
36
41
  end
37
42
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Amorail Exceptions.
2
4
  # Every class is name of HTTP response error code(status)
3
5
  module Amorail
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail
2
4
  # Return hash key as method call
3
5
  module MethodMissing
4
6
  def method_missing(method_sym, *arguments, &block)
5
- if data.key?(method_sym.to_s)
6
- data.fetch(method_sym.to_s)
7
+ if data.key?(method_sym.to_s.downcase)
8
+ data.fetch(method_sym.to_s.downcase)
7
9
  else
8
10
  super
9
11
  end
10
12
  end
11
13
 
12
14
  def respond_to_missing?(method_sym, *args)
13
- args.size.zero? && data.key?(method_sym.to_s)
15
+ args.size.zero? && data.key?(method_sym.to_s.downcase)
14
16
  end
15
17
  end
16
18
 
@@ -26,6 +28,7 @@ module Amorail
26
28
  data['custom_fields'].fetch(source_name, []).each do |contact|
27
29
  identifier = contact['code'].presence || contact['name'].presence
28
30
  next if identifier.nil?
31
+
29
32
  hash[identifier.downcase] = PropertyItem.new(contact)
30
33
  end
31
34
  new hash
@@ -118,6 +121,7 @@ module Amorail
118
121
  prop_item = PropertyItem.new(tt)
119
122
  identifier = tt['code'].presence || tt['name'].presence
120
123
  next if identifier.nil?
124
+
121
125
  hash[identifier.downcase] = prop_item
122
126
  hash[identifier] = prop_item
123
127
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Amorail
2
4
  # Add amorail rake tasks
3
5
  class Railtie < Rails::Railtie
4
6
  rake_tasks do
5
- load File.expand_path('../../tasks/amorail.rake', __FILE__)
7
+ load File.expand_path('../tasks/amorail.rake', __dir__)
6
8
  end
7
9
  end
8
10
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'amorail/store_adapters/abstract_store_adapter'
4
+ require 'amorail/store_adapters/redis_store_adapter'
5
+ require 'amorail/store_adapters/memory_store_adapter'
6
+
7
+ module Amorail
8
+ module StoreAdapters
9
+ def self.build_by_name(adapter, options = nil)
10
+ camelized_adapter = adapter.to_s.split('_').map(&:capitalize).join
11
+ adapter_class_name = "#{camelized_adapter}StoreAdapter"
12
+ StoreAdapters.const_get(adapter_class_name).new(**(options || {}))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amorail
4
+ module StoreAdapters
5
+ class AbstractStoreAdapter
6
+ def fetch_access(_secret)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def persist_access(_secret, _token, _refresh_token, _expiration)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def update_refresh(_secret, _token, _refresh_token, _expiration)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def access_expired?(_key)
19
+ raise NotImplementedError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amorail
4
+ module StoreAdapters
5
+ class MemoryStoreAdapter < AbstractStoreAdapter
6
+ attr_reader :storage
7
+
8
+ def initialize(**options)
9
+ raise ArgumentError, 'Memory store doesn\'t support any options' if options.any?
10
+ @storage = Hash.new { |hh, kk| hh[kk] = {} }
11
+ end
12
+
13
+ def fetch_access(secret)
14
+ value_if_not_expired(secret)
15
+ end
16
+
17
+ def persist_access(secret, token, refresh_token, expiration)
18
+ access_token = { token: token, refresh_token: refresh_token, expiration: expiration }
19
+ storage.store(secret, access_token)
20
+ end
21
+
22
+ def update_access(secret, token, refresh_token, expiration)
23
+ update_access_fields(
24
+ secret,
25
+ token: token,
26
+ refresh_token: refresh_token,
27
+ expiration: expiration
28
+ )
29
+ end
30
+
31
+ def access_expired?(key)
32
+ storage[key][:expiration] && Time.now.to_i >= storage[key][:expiration]
33
+ end
34
+
35
+ private
36
+
37
+ def value_if_not_expired(key)
38
+ if !access_expired?(key)
39
+ storage[key]
40
+ else
41
+ {}
42
+ end
43
+ end
44
+
45
+ def update_access_fields(key, fields)
46
+ storage[key].merge!(fields)
47
+ end
48
+ end
49
+ end
50
+ end