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,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
@@ -0,0 +1,8 @@
1
+ module Amorail
2
+ # Add amorail rake tasks
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ load File.expand_path('../tasks/amorail.rake', __dir__)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ # Amorail version
2
+ module Amorail
3
+ VERSION = "0.5.0".freeze
4
+ end
@@ -0,0 +1,6 @@
1
+ namespace :amorail do
2
+ desc 'Check Amorail configuration'
3
+ task :check do
4
+ puts Amorail.properties.inspect
5
+ end
6
+ end
@@ -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