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,67 @@
|
|
1
|
+
module Amorail # :nodoc: all
|
2
|
+
class Entity
|
3
|
+
class << self
|
4
|
+
# Find AMO entity by id
|
5
|
+
def find(id)
|
6
|
+
new.load_record(id)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Find AMO entity by id
|
10
|
+
# and raise RecordNotFound if nothing was found
|
11
|
+
def find!(id)
|
12
|
+
rec = find(id)
|
13
|
+
fail RecordNotFound unless rec
|
14
|
+
|
15
|
+
rec
|
16
|
+
end
|
17
|
+
|
18
|
+
# General method to load many records by proving some filters
|
19
|
+
def where(options)
|
20
|
+
response = client.safe_request(
|
21
|
+
:get,
|
22
|
+
remote_url('list'),
|
23
|
+
options
|
24
|
+
)
|
25
|
+
load_many(response)
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_all(*ids)
|
29
|
+
ids = ids.first if ids.size == 1 && ids.first.is_a?(Array)
|
30
|
+
|
31
|
+
where(id: ids)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Find AMO entities by query
|
35
|
+
# Returns array of matching entities.
|
36
|
+
def find_by_query(query)
|
37
|
+
where(query: query)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# We can have response with 200 or 204 here.
|
43
|
+
# 204 response has no body, so we don't want to parse it.
|
44
|
+
def load_many(response)
|
45
|
+
return [] if response.status == 204
|
46
|
+
|
47
|
+
response.body['response'].fetch(amo_response_name, [])
|
48
|
+
.map { |info| new.reload_model(info) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_record(id)
|
53
|
+
response = client.safe_request(
|
54
|
+
:get,
|
55
|
+
remote_url('list'),
|
56
|
+
id: id
|
57
|
+
)
|
58
|
+
handle_response(response, 'load') || nil
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def extract_data_load(response)
|
64
|
+
response.first
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "active_support/core_ext/hash/indifferent_access"
|
2
|
+
|
3
|
+
module Amorail # :nodoc: all
|
4
|
+
class Entity
|
5
|
+
def params
|
6
|
+
data = {}
|
7
|
+
self.class.attributes.each do |k, v|
|
8
|
+
data[k] = send("to_#{v}", send(k))
|
9
|
+
end
|
10
|
+
|
11
|
+
data[:custom_fields] = custom_fields if properties.respond_to?(amo_name)
|
12
|
+
|
13
|
+
normalize_params(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def custom_fields
|
19
|
+
props = properties.send(self.class.amo_name)
|
20
|
+
|
21
|
+
custom_fields = []
|
22
|
+
|
23
|
+
self.class.properties.each do |k, v|
|
24
|
+
prop_id = props.send(k).id
|
25
|
+
prop_val = { value: send(k) }.merge(v)
|
26
|
+
custom_fields << { id: prop_id, values: [prop_val] }
|
27
|
+
end
|
28
|
+
|
29
|
+
custom_fields
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_params(method)
|
33
|
+
{
|
34
|
+
request: {
|
35
|
+
self.class.amo_response_name => {
|
36
|
+
method => [
|
37
|
+
params
|
38
|
+
]
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def normalize_custom_fields(val)
|
45
|
+
val.reject do |field|
|
46
|
+
field[:values].all? { |item| !item[:value] }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# this method removes nil values and empty arrays from params hash (deep)
|
51
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
52
|
+
# rubocop:disable Metrics/MethodLength
|
53
|
+
def normalize_params(data)
|
54
|
+
return data unless data.is_a?(Hash)
|
55
|
+
|
56
|
+
compacted = {}
|
57
|
+
data.each do |key, val|
|
58
|
+
case val
|
59
|
+
when Numeric, String
|
60
|
+
compacted[key] = val
|
61
|
+
when Array
|
62
|
+
val.compact!
|
63
|
+
# handle custom keys
|
64
|
+
val = normalize_custom_fields(val) if key == :custom_fields
|
65
|
+
unless val.empty?
|
66
|
+
compacted[key] = val.map { |el| normalize_params(el) }
|
67
|
+
end
|
68
|
+
else
|
69
|
+
params = normalize_params(val)
|
70
|
+
compacted[key] = params unless params.nil?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
compacted.with_indifferent_access
|
74
|
+
end
|
75
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
76
|
+
# rubocop:enable Metrics/MethodLength
|
77
|
+
|
78
|
+
def to_timestamp(val)
|
79
|
+
return if val.nil?
|
80
|
+
|
81
|
+
case val
|
82
|
+
when String
|
83
|
+
(date = Time.parse(val)) && date.to_i
|
84
|
+
when Date
|
85
|
+
val.to_time.to_i
|
86
|
+
else
|
87
|
+
val.to_i
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_default(val)
|
92
|
+
val
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Amorail # :nodoc: all
|
2
|
+
class Entity
|
3
|
+
class InvalidRecord < ::Amorail::Error; end
|
4
|
+
class NotPersisted < ::Amorail::Error; end
|
5
|
+
|
6
|
+
def new_record?
|
7
|
+
id.blank?
|
8
|
+
end
|
9
|
+
|
10
|
+
def persisted?
|
11
|
+
!new_record?
|
12
|
+
end
|
13
|
+
|
14
|
+
def save
|
15
|
+
return false unless valid?
|
16
|
+
|
17
|
+
new_record? ? push('add') : push('update')
|
18
|
+
end
|
19
|
+
|
20
|
+
def save!
|
21
|
+
save || fail(InvalidRecord)
|
22
|
+
end
|
23
|
+
|
24
|
+
def update(attrs = {})
|
25
|
+
return false if new_record?
|
26
|
+
|
27
|
+
merge_params(attrs)
|
28
|
+
push('update')
|
29
|
+
end
|
30
|
+
|
31
|
+
def update!(attrs = {})
|
32
|
+
update(attrs) || fail(NotPersisted)
|
33
|
+
end
|
34
|
+
|
35
|
+
def reload
|
36
|
+
fail NotPersisted if id.nil?
|
37
|
+
|
38
|
+
load_record(id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def extract_data_add(response)
|
44
|
+
response.fetch('add').first
|
45
|
+
end
|
46
|
+
|
47
|
+
# Update response can have status 200 and contain errors.
|
48
|
+
# In case of errors "update" key in a response is a Hash with "errors" key.
|
49
|
+
# If there are no errors "update" key is an Array with entities attributes.
|
50
|
+
def extract_data_update(response)
|
51
|
+
case data = response.fetch('update')
|
52
|
+
when Array
|
53
|
+
data.first
|
54
|
+
when Hash
|
55
|
+
merge_errors(data)
|
56
|
+
raise(InvalidRecord)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def merge_errors(data)
|
61
|
+
data.fetch("errors").each do |_, message|
|
62
|
+
errors.add(:base, message)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Amorail Exceptions.
|
2
|
+
# Every class is name of HTTP response error code(status)
|
3
|
+
module Amorail
|
4
|
+
class Error < ::StandardError; end
|
5
|
+
|
6
|
+
class APIError < Error; end
|
7
|
+
|
8
|
+
class AmoBadRequestError < APIError; end
|
9
|
+
|
10
|
+
class AmoMovedPermanentlyError < APIError; end
|
11
|
+
|
12
|
+
class AmoUnauthorizedError < APIError; end
|
13
|
+
|
14
|
+
class AmoForbiddenError < APIError; end
|
15
|
+
|
16
|
+
class AmoNotFoundError < APIError; end
|
17
|
+
|
18
|
+
class AmoInternalError < APIError; end
|
19
|
+
|
20
|
+
class AmoBadGatewayError < APIError; end
|
21
|
+
|
22
|
+
class AmoServiceUnaviableError < APIError; end
|
23
|
+
|
24
|
+
class AmoUnknownError < APIError; end
|
25
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Amorail
|
2
|
+
# Return hash key as method call
|
3
|
+
module MethodMissing
|
4
|
+
def method_missing(method_sym, *arguments, &block)
|
5
|
+
if data.key?(method_sym.to_s)
|
6
|
+
data.fetch(method_sym.to_s)
|
7
|
+
else
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def respond_to_missing?(method_sym, *args)
|
13
|
+
args.size.zero? && data.key?(method_sym.to_s)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Property # :nodoc: all
|
18
|
+
class PropertyItem
|
19
|
+
include MethodMissing
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :source_name
|
23
|
+
|
24
|
+
def parse(data)
|
25
|
+
hash = {}
|
26
|
+
data['custom_fields'].fetch(source_name, []).each do |contact|
|
27
|
+
identifier = contact['code'].presence || contact['name'].presence
|
28
|
+
next if identifier.nil?
|
29
|
+
|
30
|
+
hash[identifier.downcase] = PropertyItem.new(contact)
|
31
|
+
end
|
32
|
+
new hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :data
|
37
|
+
|
38
|
+
def initialize(data)
|
39
|
+
@data = data
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](key)
|
43
|
+
@data[key]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class StatusItem
|
48
|
+
attr_reader :statuses
|
49
|
+
|
50
|
+
def initialize(data)
|
51
|
+
@statuses = data
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :client, :data, :contacts,
|
56
|
+
:company, :leads, :tasks
|
57
|
+
|
58
|
+
def initialize(client)
|
59
|
+
@client = client
|
60
|
+
reload
|
61
|
+
end
|
62
|
+
|
63
|
+
def reload
|
64
|
+
@data = load_fields
|
65
|
+
parse_all_data
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_fields
|
69
|
+
response = client.safe_request(
|
70
|
+
:get,
|
71
|
+
'/private/api/v2/json/accounts/current'
|
72
|
+
)
|
73
|
+
response.body["response"]["account"]
|
74
|
+
end
|
75
|
+
|
76
|
+
def inspect
|
77
|
+
@data
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def parse_all_data
|
83
|
+
@contacts = Contact.parse(data)
|
84
|
+
@company = Company.parse(data)
|
85
|
+
@leads = Lead.parse(data)
|
86
|
+
@tasks = Task.parse(data)
|
87
|
+
end
|
88
|
+
|
89
|
+
class Contact < PropertyItem
|
90
|
+
self.source_name = 'contacts'
|
91
|
+
end
|
92
|
+
|
93
|
+
class Company < PropertyItem
|
94
|
+
self.source_name = 'companies'
|
95
|
+
end
|
96
|
+
|
97
|
+
class Lead < PropertyItem
|
98
|
+
self.source_name = 'leads'
|
99
|
+
|
100
|
+
attr_accessor :statuses
|
101
|
+
|
102
|
+
class << self
|
103
|
+
def parse(data)
|
104
|
+
obj = super
|
105
|
+
hash = {}
|
106
|
+
data.fetch('leads_statuses', []).each do |prop|
|
107
|
+
hash[prop['name']] = PropertyItem.new(prop)
|
108
|
+
end
|
109
|
+
obj.statuses = hash
|
110
|
+
obj
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Task < PropertyItem
|
116
|
+
def self.parse(data)
|
117
|
+
hash = {}
|
118
|
+
data.fetch('task_types', []).each do |tt|
|
119
|
+
prop_item = PropertyItem.new(tt)
|
120
|
+
identifier = tt['code'].presence || tt['name'].presence
|
121
|
+
next if identifier.nil?
|
122
|
+
|
123
|
+
hash[identifier.downcase] = prop_item
|
124
|
+
hash[identifier] = prop_item
|
125
|
+
end
|
126
|
+
new hash
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Amorail::Client do
|
4
|
+
let(:client) { Amorail.client }
|
5
|
+
|
6
|
+
before(:each) { mock_api }
|
7
|
+
|
8
|
+
context "default client" do
|
9
|
+
it "should create client", :aggregate_failures do
|
10
|
+
expect(subject.usermail).to eq "amorail@test.com"
|
11
|
+
expect(subject.api_key).to eq "75742b166417fe32ae132282ce178cf6"
|
12
|
+
expect(subject.api_endpoint).to eq "https://test.amocrm.ru"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should #authorize method call" do
|
16
|
+
res = client.authorize
|
17
|
+
expect(res.status).to eq 200
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should #authorize and set cookie" do
|
21
|
+
res = client.get("/private/api/v2/json/accounts/current")
|
22
|
+
expect(res.status).to eq 200
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#with_client" do
|
27
|
+
before { mock_custom_api("https://custom.amo.com", "custom@amo.com", "123") }
|
28
|
+
|
29
|
+
let(:new_client) do
|
30
|
+
described_class.new(
|
31
|
+
api_endpoint: "https://custom.amo.com",
|
32
|
+
usermail: "custom@amo.com",
|
33
|
+
api_key: "123"
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "use custom client as instance", :aggregate_failures do
|
38
|
+
expect(Amorail.client.usermail).to eq "amorail@test.com"
|
39
|
+
Amorail.with_client(new_client) do
|
40
|
+
expect(Amorail.client.usermail).to eq "custom@amo.com"
|
41
|
+
expect(Amorail.client.api_endpoint).to eq "https://custom.amo.com"
|
42
|
+
expect(Amorail.client.api_key).to eq "123"
|
43
|
+
end
|
44
|
+
|
45
|
+
expect(Amorail.client.usermail).to eq "amorail@test.com"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "use custom client as options", :aggregate_failures do
|
49
|
+
expect(Amorail.client.usermail).to eq "amorail@test.com"
|
50
|
+
Amorail.with_client(
|
51
|
+
api_endpoint: "https://custom.amo.com",
|
52
|
+
usermail: "custom@amo.com",
|
53
|
+
api_key: "123"
|
54
|
+
) do
|
55
|
+
expect(Amorail.client.usermail).to eq "custom@amo.com"
|
56
|
+
expect(Amorail.client.api_endpoint).to eq "https://custom.amo.com"
|
57
|
+
expect(Amorail.client.api_key).to eq "123"
|
58
|
+
end
|
59
|
+
|
60
|
+
expect(Amorail.client.usermail).to eq "amorail@test.com"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "loads custom properties", :aggregate_failures do
|
64
|
+
expect(Amorail.properties.company.phone.id).to eq "1460589"
|
65
|
+
|
66
|
+
Amorail.with_client(new_client) do
|
67
|
+
expect(Amorail.properties.company.phone.id).to eq "301"
|
68
|
+
end
|
69
|
+
|
70
|
+
expect(Amorail.properties.company.phone.id).to eq "1460589"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "threadsafe", :aggregate_failures do
|
74
|
+
results = []
|
75
|
+
q1 = Queue.new
|
76
|
+
q2 = Queue.new
|
77
|
+
q3 = Queue.new
|
78
|
+
threads = []
|
79
|
+
|
80
|
+
# This thread enters block first but commits result
|
81
|
+
# only after the second thread enters block
|
82
|
+
threads << Thread.new do
|
83
|
+
q1.pop
|
84
|
+
Amorail.with_client(usermail: 'test1@amo.com') do
|
85
|
+
q2 << 1
|
86
|
+
q1.pop
|
87
|
+
results << Amorail.client.usermail
|
88
|
+
q2 << 1
|
89
|
+
end
|
90
|
+
q3 << 1
|
91
|
+
end
|
92
|
+
|
93
|
+
# This thread enters block second and commits result
|
94
|
+
# after the first block
|
95
|
+
threads << Thread.new do
|
96
|
+
q2.pop
|
97
|
+
Amorail.with_client(usermail: 'test2@amo.com') do
|
98
|
+
q1 << 1
|
99
|
+
q2.pop
|
100
|
+
results << Amorail.client.usermail
|
101
|
+
end
|
102
|
+
q3 << 1
|
103
|
+
end
|
104
|
+
|
105
|
+
# This thread enters block third and commits
|
106
|
+
# after all other threads left blocks
|
107
|
+
threads << Thread.new do
|
108
|
+
Amorail.with_client(usermail: 'test3@amo.com') do
|
109
|
+
q3.pop
|
110
|
+
q3.pop
|
111
|
+
results << Amorail.client.usermail
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
q1 << 1
|
116
|
+
threads.each(&:join)
|
117
|
+
|
118
|
+
expect(results[0]).to eq 'test1@amo.com'
|
119
|
+
expect(results[1]).to eq 'test2@amo.com'
|
120
|
+
expect(results[2]).to eq 'test3@amo.com'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|