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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +61 -0
- data/.travis.yml +9 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +245 -0
- data/Rakefile +15 -0
- data/amorail.gemspec +33 -0
- data/lib/amorail.rb +49 -0
- data/lib/amorail/client.rb +101 -0
- data/lib/amorail/config.rb +17 -0
- data/lib/amorail/entities/company.rb +23 -0
- data/lib/amorail/entities/contact.rb +29 -0
- data/lib/amorail/entities/contact_link.rb +32 -0
- data/lib/amorail/entities/elementable.rb +37 -0
- data/lib/amorail/entities/lead.rb +26 -0
- data/lib/amorail/entities/leadable.rb +29 -0
- data/lib/amorail/entities/note.rb +17 -0
- data/lib/amorail/entities/task.rb +18 -0
- data/lib/amorail/entities/webhook.rb +42 -0
- data/lib/amorail/entity.rb +128 -0
- data/lib/amorail/entity/finders.rb +67 -0
- data/lib/amorail/entity/params.rb +95 -0
- data/lib/amorail/entity/persistence.rb +66 -0
- data/lib/amorail/exceptions.rb +25 -0
- data/lib/amorail/property.rb +130 -0
- data/lib/amorail/railtie.rb +8 -0
- data/lib/amorail/version.rb +4 -0
- data/lib/tasks/amorail.rake +6 -0
- data/spec/client_spec.rb +123 -0
- data/spec/company_spec.rb +82 -0
- data/spec/contact_link_spec.rb +40 -0
- data/spec/contact_spec.rb +187 -0
- data/spec/entity_spec.rb +55 -0
- data/spec/fixtures/accounts/response_1.json +344 -0
- data/spec/fixtures/accounts/response_2.json +195 -0
- data/spec/fixtures/amorail_test.yml +3 -0
- data/spec/fixtures/contacts/create.json +13 -0
- data/spec/fixtures/contacts/find_many.json +57 -0
- data/spec/fixtures/contacts/find_one.json +41 -0
- data/spec/fixtures/contacts/links.json +16 -0
- data/spec/fixtures/contacts/my_contact_find.json +47 -0
- data/spec/fixtures/contacts/update.json +13 -0
- data/spec/fixtures/leads/create.json +13 -0
- data/spec/fixtures/leads/find_many.json +73 -0
- data/spec/fixtures/leads/links.json +16 -0
- data/spec/fixtures/leads/update.json +13 -0
- data/spec/fixtures/leads/update_errors.json +12 -0
- data/spec/fixtures/webhooks/list.json +24 -0
- data/spec/fixtures/webhooks/subscribe.json +17 -0
- data/spec/fixtures/webhooks/unsubscribe.json +17 -0
- data/spec/helpers/webmock_helpers.rb +279 -0
- data/spec/lead_spec.rb +101 -0
- data/spec/my_contact_spec.rb +48 -0
- data/spec/note_spec.rb +26 -0
- data/spec/property_spec.rb +45 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/elementable_example.rb +52 -0
- data/spec/support/entity_class_example.rb +15 -0
- data/spec/support/leadable_example.rb +33 -0
- data/spec/support/my_contact.rb +3 -0
- data/spec/support/my_entity.rb +4 -0
- data/spec/task_spec.rb +49 -0
- data/spec/webhook_spec.rb +59 -0
- 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
|