amorail 0.5.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 (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rubocop.yml +61 -0
  4. data/.travis.yml +9 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +245 -0
  8. data/Rakefile +15 -0
  9. data/amorail.gemspec +33 -0
  10. data/lib/amorail.rb +49 -0
  11. data/lib/amorail/client.rb +101 -0
  12. data/lib/amorail/config.rb +17 -0
  13. data/lib/amorail/entities/company.rb +23 -0
  14. data/lib/amorail/entities/contact.rb +29 -0
  15. data/lib/amorail/entities/contact_link.rb +32 -0
  16. data/lib/amorail/entities/elementable.rb +37 -0
  17. data/lib/amorail/entities/lead.rb +26 -0
  18. data/lib/amorail/entities/leadable.rb +29 -0
  19. data/lib/amorail/entities/note.rb +17 -0
  20. data/lib/amorail/entities/task.rb +18 -0
  21. data/lib/amorail/entities/webhook.rb +42 -0
  22. data/lib/amorail/entity.rb +128 -0
  23. data/lib/amorail/entity/finders.rb +67 -0
  24. data/lib/amorail/entity/params.rb +95 -0
  25. data/lib/amorail/entity/persistence.rb +66 -0
  26. data/lib/amorail/exceptions.rb +25 -0
  27. data/lib/amorail/property.rb +130 -0
  28. data/lib/amorail/railtie.rb +8 -0
  29. data/lib/amorail/version.rb +4 -0
  30. data/lib/tasks/amorail.rake +6 -0
  31. data/spec/client_spec.rb +123 -0
  32. data/spec/company_spec.rb +82 -0
  33. data/spec/contact_link_spec.rb +40 -0
  34. data/spec/contact_spec.rb +187 -0
  35. data/spec/entity_spec.rb +55 -0
  36. data/spec/fixtures/accounts/response_1.json +344 -0
  37. data/spec/fixtures/accounts/response_2.json +195 -0
  38. data/spec/fixtures/amorail_test.yml +3 -0
  39. data/spec/fixtures/contacts/create.json +13 -0
  40. data/spec/fixtures/contacts/find_many.json +57 -0
  41. data/spec/fixtures/contacts/find_one.json +41 -0
  42. data/spec/fixtures/contacts/links.json +16 -0
  43. data/spec/fixtures/contacts/my_contact_find.json +47 -0
  44. data/spec/fixtures/contacts/update.json +13 -0
  45. data/spec/fixtures/leads/create.json +13 -0
  46. data/spec/fixtures/leads/find_many.json +73 -0
  47. data/spec/fixtures/leads/links.json +16 -0
  48. data/spec/fixtures/leads/update.json +13 -0
  49. data/spec/fixtures/leads/update_errors.json +12 -0
  50. data/spec/fixtures/webhooks/list.json +24 -0
  51. data/spec/fixtures/webhooks/subscribe.json +17 -0
  52. data/spec/fixtures/webhooks/unsubscribe.json +17 -0
  53. data/spec/helpers/webmock_helpers.rb +279 -0
  54. data/spec/lead_spec.rb +101 -0
  55. data/spec/my_contact_spec.rb +48 -0
  56. data/spec/note_spec.rb +26 -0
  57. data/spec/property_spec.rb +45 -0
  58. data/spec/spec_helper.rb +20 -0
  59. data/spec/support/elementable_example.rb +52 -0
  60. data/spec/support/entity_class_example.rb +15 -0
  61. data/spec/support/leadable_example.rb +33 -0
  62. data/spec/support/my_contact.rb +3 -0
  63. data/spec/support/my_entity.rb +4 -0
  64. data/spec/task_spec.rb +49 -0
  65. data/spec/webhook_spec.rb +59 -0
  66. metadata +319 -0
@@ -0,0 +1,101 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'json'
4
+ require 'active_support'
5
+
6
+ module Amorail
7
+ # Amorail http client
8
+ class Client
9
+ SUCCESS_STATUS_CODES = [200, 204].freeze
10
+
11
+ attr_reader :usermail, :api_key, :api_endpoint
12
+
13
+ def initialize(api_endpoint: Amorail.config.api_endpoint,
14
+ api_key: Amorail.config.api_key,
15
+ usermail: Amorail.config.usermail)
16
+ @api_endpoint = api_endpoint
17
+ @api_key = api_key
18
+ @usermail = usermail
19
+ @connect = Faraday.new(url: api_endpoint) do |faraday|
20
+ faraday.response :json, content_type: /\bjson$/
21
+ faraday.use :instrumentation
22
+ faraday.adapter Faraday.default_adapter
23
+ end
24
+ end
25
+
26
+ def properties
27
+ @properties ||= Property.new(self)
28
+ end
29
+
30
+ def connect
31
+ @connect || self.class.new
32
+ end
33
+
34
+ def authorize
35
+ self.cookies = nil
36
+ response = post(
37
+ Amorail.config.auth_url,
38
+ 'USER_LOGIN' => usermail,
39
+ 'USER_HASH' => api_key
40
+ )
41
+ cookie_handler(response)
42
+ response
43
+ end
44
+
45
+ def safe_request(method, url, params = {})
46
+ public_send(method, url, params)
47
+ rescue ::Amorail::AmoUnauthorizedError
48
+ authorize
49
+ public_send(method, url, params)
50
+ end
51
+
52
+ def get(url, params = {})
53
+ response = connect.get(url, params) do |request|
54
+ request.headers['Cookie'] = cookies if cookies.present?
55
+ end
56
+ handle_response(response)
57
+ end
58
+
59
+ def post(url, params = {})
60
+ response = connect.post(url) do |request|
61
+ request.headers['Cookie'] = cookies if cookies.present?
62
+ request.headers['Content-Type'] = 'application/json'
63
+ request.body = params.to_json
64
+ end
65
+ handle_response(response)
66
+ end
67
+
68
+ private
69
+
70
+ attr_accessor :cookies
71
+
72
+ def cookie_handler(response)
73
+ self.cookies = response.headers['set-cookie'].split('; ')[0]
74
+ end
75
+
76
+ def handle_response(response) # rubocop:disable all
77
+ return response if SUCCESS_STATUS_CODES.include?(response.status)
78
+
79
+ case response.status
80
+ when 301
81
+ fail ::Amorail::AmoMovedPermanentlyError
82
+ when 400
83
+ fail ::Amorail::AmoBadRequestError
84
+ when 401
85
+ fail ::Amorail::AmoUnauthorizedError
86
+ when 403
87
+ fail ::Amorail::AmoForbiddenError
88
+ when 404
89
+ fail ::Amorail::AmoNotFoundError
90
+ when 500
91
+ fail ::Amorail::AmoInternalError
92
+ when 502
93
+ fail ::Amorail::AmoBadGatewayError
94
+ when 503
95
+ fail ::Amorail::AmoServiceUnaviableError
96
+ else
97
+ fail ::Amorail::AmoUnknownError, response.body
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,17 @@
1
+ require 'anyway'
2
+
3
+ module Amorail
4
+ # Amorail config contains:
5
+ # - usermail ("user@gmail.com")
6
+ # - api_key ("13601bbac84727df")
7
+ # - api_endpoint ("http://you_company.amocrm.com")
8
+ # - api_path (default: "/private/api/v2/json/")
9
+ # - auth_url (default: "/private/api/auth.php?type=json")
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"
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ require 'amorail/entities/leadable'
2
+
3
+ module Amorail
4
+ # AmoCRM company entity
5
+ class Company < Amorail::Entity
6
+ include Leadable
7
+ amo_names 'company', 'contacts'
8
+
9
+ amo_field :name
10
+ amo_property :email, enum: 'WORK'
11
+ amo_property :phone, enum: 'WORK'
12
+ amo_property :address
13
+ amo_property :web
14
+
15
+ validates :name, presence: true
16
+
17
+ def params
18
+ data = super
19
+ data[:type] = 'contact'
20
+ data
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ require 'amorail/entities/leadable'
2
+
3
+ module Amorail
4
+ # AmoCRM contact entity
5
+ class Contact < Amorail::Entity
6
+ include Leadable
7
+ amo_names 'contacts'
8
+
9
+ amo_field :name, :company_name, :linked_company_id
10
+
11
+ amo_property :email, enum: 'WORK'
12
+ amo_property :phone, enum: 'MOB'
13
+ amo_property :position
14
+
15
+ validates :name, presence: true
16
+
17
+ # Clear company cache
18
+ def reload
19
+ @company = nil
20
+ super
21
+ end
22
+
23
+ def company
24
+ return if linked_company_id.nil?
25
+
26
+ @company ||= Amorail::Company.find(linked_company_id)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Amorail
2
+ # AmoCRM contact-link join model
3
+ class ContactLink < Amorail::Entity
4
+ amo_names "contacts", "links"
5
+
6
+ amo_field :contact_id, :lead_id
7
+
8
+ class << self
9
+ # Find links by contacts ids
10
+ def find_by_contacts(*ids)
11
+ ids = ids.first if ids.size == 1 && ids.first.is_a?(Array)
12
+ response = client.safe_request(
13
+ :get,
14
+ remote_url('links'),
15
+ contacts_link: ids
16
+ )
17
+ load_many(response)
18
+ end
19
+
20
+ # Find links by leads ids
21
+ def find_by_leads(*ids)
22
+ ids = ids.first if ids.size == 1 && ids.first.is_a?(Array)
23
+ response = client.safe_request(
24
+ :get,
25
+ remote_url('links'),
26
+ deals_link: ids
27
+ )
28
+ load_many(response)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ module Amorail
2
+ # Provides common functionallity for entities
3
+ # that can be attached to another objects.
4
+ module Elementable
5
+ extend ActiveSupport::Concern
6
+
7
+ ELEMENT_TYPES = {
8
+ contact: 1,
9
+ lead: 2,
10
+ company: 3,
11
+ task: 4
12
+ }.freeze
13
+
14
+ included do
15
+ amo_field :element_id, :element_type
16
+
17
+ validates :element_id, :element_type,
18
+ presence: true
19
+ end
20
+
21
+ ELEMENT_TYPES.each do |type, value|
22
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
23
+ def #{type}=(val) # def contact=(val)
24
+ #{type}! if val # contact! if val
25
+ end # end
26
+
27
+ def #{type}? # def contact?
28
+ self.element_type == #{value} # self.element_type == 1
29
+ end # end
30
+
31
+ def #{type}! # def contact!
32
+ self.element_type = #{value} # self.element_type = 1
33
+ end # end
34
+ CODE
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ module Amorail
2
+ # AmoCRM lead entity
3
+ class Lead < Amorail::Entity
4
+ amo_names "leads"
5
+
6
+ amo_field :name, :price, :status_id, :pipeline_id, :tags
7
+
8
+ validates :name, :status_id, presence: true
9
+
10
+ def reload
11
+ @contacts = nil
12
+ super
13
+ end
14
+
15
+ # Return list of associated contacts
16
+ def contacts
17
+ fail NotPersisted if id.nil?
18
+
19
+ @contacts ||=
20
+ begin
21
+ links = Amorail::ContactLink.find_by_leads(id)
22
+ links.empty? ? [] : Amorail::Contact.find_all(links.map(&:contact_id))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ module Amorail
2
+ # Lead associations
3
+ module Leadable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ amo_field :linked_leads_id
8
+ end
9
+
10
+ # Set initial value for linked_leads_id to []
11
+ def initialize(*args)
12
+ super
13
+ self.linked_leads_id ||= []
14
+ end
15
+
16
+ # Clear leads cache on reload
17
+ def reload
18
+ @leads = nil
19
+ super
20
+ end
21
+
22
+ # Return all linked leads
23
+ def leads
24
+ return [] if linked_leads_id.empty?
25
+
26
+ @leads ||= Amorail::Lead.find_all(linked_leads_id)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ require 'amorail/entities/elementable'
2
+
3
+ module Amorail
4
+ # AmoCRM note entity
5
+ class Note < Amorail::Entity
6
+ include Elementable
7
+
8
+ amo_names 'notes'
9
+
10
+ amo_field :note_type, :text
11
+
12
+ validates :note_type, :text,
13
+ presence: true
14
+
15
+ validates :element_type, inclusion: ELEMENT_TYPES.values
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ require 'amorail/entities/elementable'
2
+
3
+ module Amorail
4
+ # AmoCRM task entity
5
+ class Task < Amorail::Entity
6
+ include Elementable
7
+
8
+ amo_names 'tasks'
9
+
10
+ amo_field :task_type, :text, complete_till: :timestamp
11
+
12
+ validates :task_type, :text, :complete_till,
13
+ presence: true
14
+
15
+ validates :element_type, inclusion:
16
+ ELEMENT_TYPES.reject { |type, _| type == :task }.values
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ module Amorail
2
+ # AmoCRM webhook entity
3
+ class Webhook < Entity
4
+ amo_names 'webhooks'
5
+
6
+ amo_field :id, :url, :events, :disabled
7
+
8
+ def self.list
9
+ response = client.safe_request(:get, remote_url('list'))
10
+
11
+ return [] if response.body.blank?
12
+
13
+ response.body['response'].fetch(amo_response_name, []).map do |attributes|
14
+ new.reload_model(attributes)
15
+ end
16
+ end
17
+
18
+ def self.subscribe(webhooks)
19
+ perform_webhooks_request('subscribe', webhooks) do |data|
20
+ data.map { |attrs| new.reload_model(attrs) }
21
+ end
22
+ end
23
+
24
+ def self.unsubscribe(webhooks)
25
+ perform_webhooks_request('unsubscribe', webhooks)
26
+ end
27
+
28
+ def self.perform_webhooks_request(action, webhooks, &block)
29
+ response = client.safe_request(
30
+ :post,
31
+ remote_url(action),
32
+ request: { webhooks: { action => webhooks } }
33
+ )
34
+
35
+ return response unless block
36
+
37
+ block.call(response.body['response'].dig(amo_response_name, 'subscribe'))
38
+ end
39
+
40
+ private_class_method :perform_webhooks_request
41
+ end
42
+ end
@@ -0,0 +1,128 @@
1
+ require 'active_model'
2
+
3
+ module Amorail
4
+ # Core class for all Amo entities (company, contact, etc)
5
+ class Entity
6
+ include ActiveModel::Model
7
+ include ActiveModel::AttributeMethods
8
+ include ActiveModel::Validations
9
+
10
+ class RecordNotFound < ::Amorail::Error; end
11
+
12
+ class << self
13
+ attr_reader :amo_name, :amo_response_name
14
+
15
+ delegate :client, to: Amorail
16
+
17
+ # copy Amo names
18
+ def inherited(subclass)
19
+ subclass.amo_names amo_name, amo_response_name
20
+ end
21
+
22
+ def amo_names(name, response_name = nil)
23
+ @amo_name = @amo_response_name = name
24
+ @amo_response_name = response_name unless response_name.nil?
25
+ end
26
+
27
+ def amo_field(*vars, **hargs)
28
+ vars.each { |v| attributes[v] = :default }
29
+ hargs.each { |k, v| attributes[k] = v }
30
+ attr_accessor(*(vars + hargs.keys))
31
+ end
32
+
33
+ def amo_property(name, options = {})
34
+ properties[name] = options
35
+ attr_accessor(name)
36
+ end
37
+
38
+ def attributes
39
+ @attributes ||=
40
+ superclass.respond_to?(:attributes) ? superclass.attributes.dup : {}
41
+ end
42
+
43
+ def properties
44
+ @properties ||=
45
+ superclass.respond_to?(:properties) ? superclass.properties.dup : {}
46
+ end
47
+
48
+ def remote_url(action)
49
+ File.join(Amorail.config.api_path, amo_name, action)
50
+ end
51
+ end
52
+
53
+ amo_field :id, :request_id, :responsible_user_id,
54
+ date_create: :timestamp, last_modified: :timestamp
55
+
56
+ delegate :amo_name, :remote_url, :client, to: :class
57
+ delegate :properties, to: Amorail
58
+
59
+ def initialize(attributes = {})
60
+ super(attributes)
61
+ self.last_modified = Time.now.to_i if last_modified.nil?
62
+ end
63
+
64
+ require 'amorail/entity/params'
65
+ require 'amorail/entity/persistence'
66
+ require 'amorail/entity/finders'
67
+
68
+ def reload_model(info)
69
+ merge_params(info)
70
+ merge_custom_fields(info['custom_fields'])
71
+ self
72
+ end
73
+
74
+ private
75
+
76
+ def merge_params(attrs)
77
+ attrs.each do |k, v|
78
+ action = "#{k}="
79
+ next unless respond_to?(action)
80
+
81
+ send(action, v)
82
+ end
83
+ self
84
+ end
85
+
86
+ def merge_custom_fields(fields)
87
+ return if fields.nil?
88
+
89
+ fields.each do |f|
90
+ fname = f['code'] || f['name']
91
+ next if fname.nil?
92
+
93
+ fname = "#{fname.downcase}="
94
+ fval = f.fetch('values').first.fetch('value')
95
+ send(fname, fval) if respond_to?(fname)
96
+ end
97
+ end
98
+
99
+ # call safe method <safe_request>. safe_request call authorize
100
+ # if current session undefined or expires.
101
+ def push(method)
102
+ response = commit_request(create_params(method))
103
+ handle_response(response, method)
104
+ end
105
+
106
+ def commit_request(attrs)
107
+ client.safe_request(
108
+ :post,
109
+ remote_url('set'),
110
+ normalize_params(attrs)
111
+ )
112
+ end
113
+
114
+ # We can have response with 200 or 204 here.
115
+ # 204 response has no body, so we don't want to parse it.
116
+ def handle_response(response, method)
117
+ return false if response.status == 204
118
+
119
+ data = send(
120
+ "extract_data_#{method}",
121
+ response.body['response'][self.class.amo_response_name]
122
+ )
123
+ reload_model(data)
124
+ rescue InvalidRecord
125
+ false
126
+ end
127
+ end
128
+ end