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,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