amorail 0.4.0 → 0.7.0

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