amorail 0.5.0

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