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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f65fb80eb5f426c50e32fa474252e764e42e3887cddbf7f735ad217ad98560cf
4
+ data.tar.gz: 5f44b5e558be01b346ad68d4d84c903deec88e05514430531b6aa0b6e02c3e73
5
+ SHA512:
6
+ metadata.gz: 14958c70d64dda63dd82c334d08b2c63cbe8183fbffc8b5c5a63893e42280b0439e8eef4411b9b222c52e54d7dfa0949e82061de6feb4b3814ad169d5ab37cc6
7
+ data.tar.gz: a370262be82cdde926d639976fea7e23833a6f999cff3100c2a5df57e9962c8ea05b5e6b682e4d2738b4f322a3cba73d81195e721eefe017e8717c103a665f2e
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,61 @@
1
+ AllCops:
2
+ # Include gemspec and Rakefile
3
+ Include:
4
+ - 'lib/**/*.rb'
5
+ - 'lib/**/*.rake'
6
+ - 'spec/**/*.rb'
7
+ Exclude:
8
+ - 'bin/**/*'
9
+ - Rakefile
10
+ - Gemfile
11
+ - '*.gemspec'
12
+ DisplayCopNames: true
13
+ StyleGuideCopsOnly: false
14
+
15
+ Style/Documentation:
16
+ Exclude:
17
+ - 'spec/**/*.rb'
18
+
19
+ Style/StringLiterals:
20
+ Enabled: false
21
+
22
+ Style/BlockDelimiters:
23
+ Exclude:
24
+ - 'spec/**/*.rb'
25
+
26
+ Metrics/MethodLength:
27
+ Exclude:
28
+ - 'spec/**/*.rb'
29
+
30
+ Rails/Date:
31
+ Enabled: false
32
+
33
+ Rails/TimeZone:
34
+ Enabled: false
35
+
36
+ Metrics/MethodLength:
37
+ Exclude:
38
+ - 'spec/**/*.rb'
39
+
40
+ Metrics/LineLength:
41
+ Exclude:
42
+ - 'spec/**/*.rb'
43
+
44
+ Metrics/BlockLength:
45
+ Exclude:
46
+ - 'spec/**/*.rb'
47
+
48
+ Style/WordArray:
49
+ Enabled: false
50
+
51
+ Style/SymbolArray:
52
+ Enabled: false
53
+
54
+ Style/SignalException:
55
+ Enabled: false
56
+
57
+ Layout/MultilineMethodCallBraceLayout:
58
+ Enabled: false
59
+
60
+ Lint/MissingCopEnableDirective:
61
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+
6
+ before_install: gem install bundler -v 1.13.6
7
+
8
+ notifications:
9
+ email: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in amorail.gemspec
4
+ gem 'pry-byebug'
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 alekseenkoss
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ [![Gem Version](https://badge.fury.io/rb/amorail.svg)](https://rubygems.org/gems/amorail) [![Build Status](https://travis-ci.org/teachbase/amorail.svg?branch=master)](https://travis-ci.org/teachbase/amorail)
2
+
3
+ # Amorail
4
+
5
+ AmoCRM client
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'amorail'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install amorail
22
+
23
+ ## Usage
24
+
25
+ With Amorail you can manipulate the following AmoCRM entities: Companies, Contacts, Leads and Tasks.
26
+ We're triying to build simple AR-like interface.
27
+
28
+ ### Auth configuration
29
+
30
+ Amorail uses [anyway_config](https://github.com/palkan/anyway_config) for configuration, so you
31
+ can provide configuration parameters through env vars, seperate config file (`config/amorail.yml`) or `secrets.yml`.
32
+
33
+ Required params: **usermail**, **api_key** and **api_endpoint**.
34
+
35
+ Example:
36
+
37
+ ```
38
+ # config/secrets.yml
39
+ development:
40
+ ...
41
+ amorail:
42
+ usermail: 'amorail@test.com'
43
+ api_key: '75742b166417fe32ae132282ce178cf6'
44
+ api_endpoint: 'https://test.amocrm.ru'
45
+ ```
46
+
47
+ ### Running from console
48
+
49
+ You can try amorail in action from console ([PRY](https://github.com/pry/pry) is required to be installed):
50
+
51
+ ```shell
52
+ # amorail gem directory
53
+ AMORAIL_USERMAIL=my_mail@test.com AMORAIL_API_KEY=my_key AMORAIL_API_ENDPOINT=my@amo.com bundle exec rake console
54
+ pry> Amorail.properties
55
+ # ... prints properties (custom_fields) data
56
+ pry> Amorail::Contact.find_by_query("test_contact")
57
+ # ... returns array of contacts which satisfy the query
58
+ ```
59
+
60
+ ### Create new objects
61
+
62
+ Create Leads
63
+
64
+ ```ruby
65
+ lead = Amorail::Lead.new(
66
+ name: "Example Lead",
67
+ tags: "IT, Sales",
68
+ price: 100,
69
+ status_id: Amorail.properties.leads.statuses[
70
+ Rails.application.secrets.amoparams['lead_status']
71
+ ].id
72
+ )
73
+
74
+ lead.save!
75
+ ```
76
+
77
+ Create Company
78
+
79
+ ```ruby
80
+ company = Amorail::Company.new(
81
+ name: "My company",
82
+ phone: "222-111",
83
+ email: "human@example.com"
84
+ )
85
+ company.linked_leads_id << lead.id
86
+ company.save!
87
+ ```
88
+
89
+ Create Contact
90
+
91
+ ```ruby
92
+ contact = Amorail::Contact.new(
93
+ name: "Ivan Ivanov",
94
+ linked_company_id: company.id,
95
+ phone: "111-222",
96
+ email: "ivan@example.com"
97
+ )
98
+
99
+ contact.linked_leads_id << lead.id
100
+ contact.save!
101
+ ```
102
+
103
+ Create Task
104
+
105
+ ```ruby
106
+ task = Amorail::Task.new(
107
+ text: "Example task",
108
+ lead: true,
109
+ complete_till: Time.zone.today.end_of_day,
110
+ task_type: Amorail.properties.tasks[Rails.application.secrets.amoparams['task_code']].id
111
+ )
112
+
113
+ # set up lead id
114
+ task.element_id = lead.id
115
+ # and save it
116
+ task.save!
117
+ ```
118
+
119
+ You can find any object by id:
120
+
121
+ ```ruby
122
+ Amorail::Company.find(company_id)
123
+ ```
124
+
125
+ Or using query:
126
+
127
+ ```ruby
128
+ Amorail::Company.find_by_query("vip")
129
+ ```
130
+
131
+ Or using arbitrary params:
132
+
133
+ ```ruby
134
+ Amorail::Company.where(query: "test", limit_rows: 10)
135
+ ```
136
+
137
+ Also you can update objects, e.g:
138
+
139
+ ```ruby
140
+ company = Amorail::Company.find(company_id)
141
+ contact = Amorail::Contact.find(contact_id)
142
+
143
+ # like this
144
+ contact.linked_company_id = company.id
145
+ contact.save!
146
+
147
+ # or
148
+
149
+ contact.update(linked_company_id: company.id)
150
+ ```
151
+
152
+ ### Querying
153
+
154
+ Load by id
155
+
156
+ ```ruby
157
+ Amorail::Contact.find(223) #=> #<Amorail::Contact ...>
158
+ ```
159
+
160
+ Load many entites by array of ids
161
+
162
+ ```ruby
163
+ Amorail::Contact.find_all(123, 124) #=> [#<Amorail::Contact ...>, ...]
164
+ ```
165
+
166
+ Load by query
167
+
168
+ ```ruby
169
+ Amorail::Contact.find_by_query("my_company") #=> [#<Amorail::Contact ...>, ...]
170
+ ```
171
+
172
+
173
+ Load contacts associated with lead
174
+
175
+ ```ruby
176
+ lead = Amorail::Lead.find(1)
177
+ lead.contacts #=> [#<Amorail::Contact ...>, ...]
178
+ ```
179
+
180
+ Load company associated with contact
181
+
182
+ ```ruby
183
+ contact = Amorail::Contact.find(1)
184
+ contact.company #=> #<Amorail::Company ...>
185
+ ```
186
+
187
+ Load leads associated with contact
188
+
189
+ ```ruby
190
+ contact = Amorail::Contact.find(1)
191
+ contact.leads #=> [#<Amorail::Lead ...>, ...]
192
+ ```
193
+
194
+ Load contacts-leads pairs
195
+
196
+ ```ruby
197
+ # Load all contact-leads pairs for contacts
198
+ Amorail::ContactLink.find_by_contacts(1, 2)
199
+
200
+ # Load all contact-leads pairs for leads
201
+ Amorail::ContactLink.find_by_leads(1, 2)
202
+ ```
203
+
204
+ ### Properties Configuration
205
+
206
+ AmoCRM is using "custom_fields" architecture,
207
+ to get all information for your account, you can
208
+ find properties and set up configuration manually in config/secrets.yml.
209
+
210
+ Note: response example in official documentation:
211
+ https://developers.amocrm.ru/rest_api/accounts_current.php
212
+
213
+ 1) Get list of properties for your account
214
+
215
+ ```
216
+ rake amorail:check
217
+ ```
218
+ Rake task will returns information about properties.
219
+
220
+ ### Multiple configurations
221
+
222
+ It is possible to use Amorail with multiple AmoCRM accounts. To do so use `Amorail.with_client` method,
223
+ which receive client params or client instance and a block to execute within custom context:
224
+
225
+ ```ruby
226
+ Amorail.with_client(usermail: "custom@mail.com", api_endpoint: "https://my.acmocrm.ru", api_key: "my_secret_key") do
227
+ # Client specific code here
228
+ end
229
+
230
+ # or using client instance
231
+ my_client = Amorail::Client.new(usermail: "custom@mail.com", api_endpoint: "https://my.acmocrm.ru", api_key: "my_secret_key")
232
+
233
+ Amorail.with_client(client) do
234
+ ...
235
+ end
236
+ ```
237
+
238
+ ## Contributing
239
+
240
+ 1. Fork it ( https://github.com/[my-github-username]/amorail/fork )
241
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
242
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
243
+ 4. Follow style guides (use Rubocop)
244
+ 5. Push to the branch (`git push origin my-new-feature`)
245
+ 6. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ require "rubocop/rake_task"
4
+
5
+ Dir.glob('lib/tasks/*.rake').each { |r| import r }
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :console do
10
+ sh 'pry -r ./lib/amorail.rb'
11
+ end
12
+
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: [:rubocop, :spec]
data/amorail.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'amorail/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "amorail"
8
+ spec.version = Amorail::VERSION
9
+ spec.authors = ["alekseenkoss", "palkan"]
10
+ spec.email = ["alekseenkoss@gmail.com", "dementiev.vm@gmail.com"]
11
+ spec.summary = %q{Ruby API client for AmoCRM}
12
+ spec.description = %q{Ruby API client for AmoCRM. You can integrate your system with it.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.4"
24
+ spec.add_development_dependency "webmock"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "shoulda-matchers", "~> 2.0"
27
+ spec.add_development_dependency "rubocop", "~> 0.49"
28
+ spec.add_dependency "anyway_config", "~> 0", ">= 0.3"
29
+ spec.add_dependency "faraday"
30
+ spec.add_dependency "faraday_middleware"
31
+ spec.add_dependency 'activemodel'
32
+ spec.add_dependency 'json'
33
+ end
data/lib/amorail.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'amorail/version'
2
+ require 'amorail/config'
3
+ require 'amorail/client'
4
+ require 'amorail/exceptions'
5
+ require 'amorail/entity'
6
+ require 'amorail/property'
7
+
8
+ Gem.find_files('amorail/entities/*.rb').each { |path| require path }
9
+
10
+ # AmoCRM API integration.
11
+ # https://www.amocrm.com/
12
+ module Amorail
13
+ def self.config
14
+ @config ||= Config.new
15
+ end
16
+
17
+ def self.properties
18
+ client.properties
19
+ end
20
+
21
+ def self.configure
22
+ yield(config) if block_given?
23
+ end
24
+
25
+ def self.client
26
+ ClientRegistry.client || (@client ||= Client.new)
27
+ end
28
+
29
+ def self.reset
30
+ @config = nil
31
+ @client = nil
32
+ end
33
+
34
+ def self.with_client(client)
35
+ client = Client.new(client) unless client.is_a?(Client)
36
+ ClientRegistry.client = client
37
+ yield
38
+ ensure
39
+ ClientRegistry.client = nil
40
+ end
41
+
42
+ class ClientRegistry # :nodoc:
43
+ extend ActiveSupport::PerThreadRegistry
44
+
45
+ attr_accessor :client
46
+ end
47
+
48
+ require 'amorail/railtie' if defined?(Rails)
49
+ end
data/lib/amorail/client.rb ADDED
@@ -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#x2F;
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
data/lib/amorail/config.rb ADDED
@@ -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
data/lib/amorail/entities/company.rb ADDED
@@ -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
data/lib/amorail/entities/contact.rb ADDED
@@ -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
data/lib/amorail/entities/contact_link.rb ADDED
@@ -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
data/lib/amorail/entities/elementable.rb ADDED
@@ -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
data/lib/amorail/entities/lead.rb ADDED
@@ -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
data/lib/amorail/entities/leadable.rb ADDED
@@ -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
data/lib/amorail/entities/note.rb ADDED
@@ -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
data/lib/amorail/entities/task.rb ADDED
@@ -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
data/lib/amorail/entities/webhook.rb ADDED
@@ -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
data/lib/amorail/entity.rb ADDED
@@ -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
data/lib/amorail/entity/finders.rb ADDED
@@ -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
data/lib/amorail/entity/params.rb ADDED
@@ -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
data/lib/amorail/entity/persistence.rb ADDED
@@ -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
data/lib/amorail/exceptions.rb ADDED
@@ -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
data/lib/amorail/property.rb ADDED
@@ -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/lib/amorail/railtie.rb ADDED
@@ -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
data/lib/amorail/version.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Amorail version
2
+ module Amorail
3
+ VERSION = "0.5.0".freeze
4
+ end
data/lib/tasks/amorail.rake ADDED
@@ -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
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
data/spec/company_spec.rb ADDED
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+
3
+ describe Amorail::Company do
4
+ before { mock_api }
5
+
6
+ describe "validations" do
7
+ it { should validate_presence_of(:name) }
8
+ end
9
+
10
+ describe ".attributes" do
11
+ subject { described_class.attributes }
12
+
13
+ it_behaves_like 'entity_class'
14
+
15
+ specify { is_expected.to include(:name) }
16
+ end
17
+
18
+ describe ".properties" do
19
+ subject { described_class.properties }
20
+
21
+ specify { is_expected.to include(:email, :phone, :web, :address) }
22
+ end
23
+
24
+ describe "#params" do
25
+ let(:company) do
26
+ described_class.new(
27
+ name: 'Test inc',
28
+ phone: '12345678',
29
+ email: 'test@mala.ru',
30
+ address: '10, State st',
31
+ web: 'hoohle.com'
32
+ )
33
+ end
34
+
35
+ subject { company.params }
36
+
37
+ specify { is_expected.to include(:last_modified) }
38
+ specify { is_expected.to include(name: 'Test inc') }
39
+ specify { is_expected.to include(type: 'contact') }
40
+
41
+ it "contains email property" do
42
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460591" }
43
+ expect(prop).not_to be_nil
44
+ expect(prop[:values].first[:value]).to eq 'test@mala.ru'
45
+ expect(prop[:values].first[:enum])
46
+ .to eq described_class.properties[:email][:enum]
47
+ end
48
+
49
+ it "contains phone property" do
50
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460589" }
51
+ expect(prop).not_to be_nil
52
+ expect(prop[:values].first[:value]).to eq '12345678'
53
+ expect(prop[:values].first[:enum])
54
+ .to eq described_class.properties[:phone][:enum]
55
+ end
56
+
57
+ it "contains address property" do
58
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460597" }
59
+ expect(prop).not_to be_nil
60
+ expect(prop[:values].first[:value]).to eq '10, State st'
61
+ end
62
+
63
+ it "contains web property" do
64
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460593" }
65
+ expect(prop).not_to be_nil
66
+ expect(prop[:values].first[:value]).to eq 'hoohle.com'
67
+ end
68
+
69
+ it_behaves_like 'leadable'
70
+ end
71
+
72
+ describe "#save" do
73
+ let(:company) { described_class.new(name: "test") }
74
+
75
+ before { company_create_stub(Amorail.config.api_endpoint) }
76
+
77
+ it "should set id after create" do
78
+ company.save!
79
+ expect(company.id).to eq 101
80
+ end
81
+ end
82
+ end
data/spec/contact_link_spec.rb ADDED
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+
3
+ describe Amorail::ContactLink do
4
+ before { mock_api }
5
+
6
+ describe ".attributes" do
7
+ subject { described_class.attributes }
8
+
9
+ it_behaves_like 'entity_class'
10
+
11
+ specify do
12
+ is_expected.to include(
13
+ :contact_id,
14
+ :lead_id
15
+ )
16
+ end
17
+ end
18
+
19
+ describe ".find_by_leads" do
20
+ before { leads_links_stub(Amorail.config.api_endpoint, [2]) }
21
+
22
+ it "returns list of contact links" do
23
+ res = described_class.find_by_leads(2)
24
+ expect(res.size).to eq 2
25
+ expect(res.first.contact_id).to eq "101"
26
+ expect(res.last.contact_id).to eq "102"
27
+ end
28
+ end
29
+
30
+ describe ".find_by_contacts" do
31
+ before { contacts_links_stub(Amorail.config.api_endpoint, [101]) }
32
+
33
+ it "returns list of contact links" do
34
+ res = described_class.find_by_contacts(101)
35
+ expect(res.size).to eq 2
36
+ expect(res.first.lead_id).to eq "1"
37
+ expect(res.last.lead_id).to eq "2"
38
+ end
39
+ end
40
+ end
data/spec/contact_spec.rb ADDED
@@ -0,0 +1,187 @@
1
+ require "spec_helper"
2
+
3
+ describe Amorail::Contact do
4
+ before { mock_api }
5
+
6
+ let(:contact) { described_class.new(name: "test") }
7
+
8
+ describe "validations" do
9
+ it { should validate_presence_of(:name) }
10
+ end
11
+
12
+ describe ".attributes" do
13
+ subject { described_class.attributes }
14
+
15
+ it_behaves_like 'entity_class'
16
+
17
+ specify { is_expected.to include(:name, :company_name) }
18
+ end
19
+
20
+ describe ".properties" do
21
+ subject { described_class.properties }
22
+
23
+ specify { is_expected.to include(:email, :phone, :position) }
24
+ end
25
+
26
+ describe "#params" do
27
+ let(:contact) do
28
+ described_class.new(
29
+ name: 'Tester',
30
+ company_name: 'Test inc',
31
+ linked_company_id: 123,
32
+ phone: '12345678',
33
+ email: 'test@mala.ru',
34
+ position: 'CEO'
35
+ )
36
+ end
37
+
38
+ subject { contact.params }
39
+
40
+ specify { is_expected.to include(:last_modified) }
41
+ specify { is_expected.to include(name: 'Tester') }
42
+ specify { is_expected.to include(company_name: 'Test inc') }
43
+ specify { is_expected.to include(linked_company_id: 123) }
44
+
45
+ it "contains email property" do
46
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460591" }
47
+ expect(prop).not_to be_nil
48
+ expect(prop[:values].first[:value]).to eq 'test@mala.ru'
49
+ expect(prop[:values].first[:enum])
50
+ .to eq described_class.properties[:email][:enum]
51
+ end
52
+
53
+ it "contains phone property" do
54
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460589" }
55
+ expect(prop).not_to be_nil
56
+ expect(prop[:values].first[:value]).to eq '12345678'
57
+ expect(prop[:values].first[:enum])
58
+ .to eq described_class.properties[:phone][:enum]
59
+ end
60
+
61
+ it "contains position property" do
62
+ prop = subject[:custom_fields].detect { |p| p[:id] == "1460587" }
63
+ expect(prop).not_to be_nil
64
+ expect(prop[:values].first[:value]).to eq 'CEO'
65
+ end
66
+
67
+ it_behaves_like 'leadable'
68
+ end
69
+
70
+ describe ".find" do
71
+ before { contact_find_stub(Amorail.config.api_endpoint, 101) }
72
+ before { contact_find_stub(Amorail.config.api_endpoint, 102, false) }
73
+
74
+ it "loads entity" do
75
+ obj = described_class.find(101)
76
+ expect(obj.id).to eq 101
77
+ expect(obj.company_name).to eq "Foo Inc."
78
+ expect(obj.email).to eq "foo@tb.com"
79
+ expect(obj.phone).to eq "1111 111 111"
80
+ expect(obj.params[:id]).to eq 101
81
+ expect(obj.linked_leads_id).to contain_exactly("1872746", "1885024")
82
+ end
83
+
84
+ it "returns nil" do
85
+ obj = described_class.find(102)
86
+ expect(obj).to be_nil
87
+ end
88
+
89
+ it "raise error" do
90
+ expect { described_class.find!(102) }
91
+ .to raise_error(Amorail::Entity::RecordNotFound)
92
+ end
93
+ end
94
+
95
+ describe ".find_by_query" do
96
+ before { contacts_find_query_stub(Amorail.config.api_endpoint, 'foo') }
97
+ before { contacts_find_query_stub(Amorail.config.api_endpoint, 'faa', nil) }
98
+
99
+ it "loads entities" do
100
+ res = described_class.find_by_query('foo')
101
+ expect(res.size).to eq 2
102
+ expect(res.first.id).to eq 101
103
+ expect(res.last.id).to eq 102
104
+ expect(res.first.company_name).to eq "Foo Inc."
105
+ expect(res.last.email).to eq "foo2@tb.com"
106
+ expect(res.first.phone).to eq "1111 111 111"
107
+ expect(res.first.params[:id]).to eq 101
108
+ end
109
+
110
+ it "returns empty array" do
111
+ res = described_class.find_by_query('faa')
112
+ expect(res).to be_a(Array)
113
+ expect(res).to be_empty
114
+ end
115
+ end
116
+
117
+ describe ".find_all" do
118
+ before { contacts_find_all_stub(Amorail.config.api_endpoint, [101, 102]) }
119
+ before { contacts_find_all_stub(Amorail.config.api_endpoint, [105, 104], false) }
120
+
121
+ it "loads entities" do
122
+ res = described_class.find_all(101, 102)
123
+ expect(res.size).to eq 2
124
+ expect(res.first.id).to eq 101
125
+ expect(res.last.id).to eq 102
126
+ expect(res.first.company_name).to eq "Foo Inc."
127
+ expect(res.last.email).to eq "foo2@tb.com"
128
+ expect(res.first.phone).to eq "1111 111 111"
129
+ expect(res.first.params[:id]).to eq 101
130
+ end
131
+
132
+ it "returns empty array" do
133
+ res = described_class.find_all([105, 104])
134
+ expect(res).to be_a(Array)
135
+ expect(res).to be_empty
136
+ end
137
+ end
138
+
139
+ describe ".where" do
140
+ before { contacts_where_stub(Amorail.config.api_endpoint, query: 'xxx', limit_rows: 10, limit_offset: 100) }
141
+
142
+ it "loads entities" do
143
+ res = described_class.where(query: 'xxx', limit_rows: 10, limit_offset: 100)
144
+ expect(res.size).to eq 2
145
+ expect(res.first.id).to eq 101
146
+ expect(res.last.id).to eq 102
147
+ expect(res.first.company_name).to eq "Foo Inc."
148
+ expect(res.last.email).to eq "foo2@tb.com"
149
+ expect(res.first.phone).to eq "1111 111 111"
150
+ expect(res.first.params[:id]).to eq 101
151
+ end
152
+ end
153
+
154
+ describe "#save" do
155
+ before { contact_create_stub(Amorail.config.api_endpoint) }
156
+
157
+ it "set id after create" do
158
+ contact.save!
159
+ expect(contact.id).to eq 101
160
+ end
161
+ end
162
+
163
+ describe "#update" do
164
+ before { contact_create_stub(Amorail.config.api_endpoint) }
165
+
166
+ it "update params" do
167
+ contact.save!
168
+ contact.name = "foo"
169
+
170
+ contact_update_stub(Amorail.config.api_endpoint)
171
+ expect(contact.save!).to be_truthy
172
+ expect(contact.name).to eq "foo"
173
+ end
174
+
175
+ it "raise error if id is blank?" do
176
+ obj = described_class.new
177
+ expect { obj.update!(name: 'Igor') }
178
+ .to raise_error(Amorail::Entity::NotPersisted)
179
+ end
180
+
181
+ it "raise error" do
182
+ obj = described_class.new
183
+ expect { obj.update!(id: 101, name: "Igor") }
184
+ .to(raise_error(Amorail::Entity::NotPersisted))
185
+ end
186
+ end
187
+ end
data/spec/entity_spec.rb ADDED
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+
3
+ describe MyEntity do
4
+ before { mock_api }
5
+
6
+ let(:entity) { described_class.new }
7
+
8
+ it_behaves_like 'entity_class'
9
+
10
+ describe "#params" do
11
+ let(:now) { Time.now }
12
+
13
+ subject { entity.params }
14
+
15
+ specify { is_expected.to include(:last_modified) }
16
+ specify {
17
+ is_expected.not_to include(
18
+ :id, :request_id, :responsible_user_id, :date_create)
19
+ }
20
+
21
+ context "with some values" do
22
+ let(:entity) do
23
+ described_class.new(
24
+ responsible_user_id: 2,
25
+ last_modified: now
26
+ )
27
+ end
28
+
29
+ specify { is_expected.to include(responsible_user_id: 2) }
30
+ specify { is_expected.to include(last_modified: now.to_i) }
31
+ specify {
32
+ is_expected.not_to include(
33
+ :id, :request_id, :date_create)
34
+ }
35
+ end
36
+
37
+ context "with all values" do
38
+ let(:entity) do
39
+ described_class.new(
40
+ id: 100,
41
+ request_id: 1,
42
+ responsible_user_id: 2,
43
+ date_create: now,
44
+ last_modified: now
45
+ )
46
+ end
47
+
48
+ specify { is_expected.to include(id: 100) }
49
+ specify { is_expected.to include(request_id: 1) }
50
+ specify { is_expected.to include(responsible_user_id: 2) }
51
+ specify { is_expected.to include(date_create: now.to_i) }
52
+ specify { is_expected.to include(last_modified: now.to_i) }
53
+ end
54
+ end
55
+ end
data/spec/fixtures/accounts/response_1.json ADDED
@@ -0,0 +1,344 @@
1
+ {
2
+ "response": {
3
+ "account": {
4
+ "id": "8195968",
5
+ "name": "База клиентов",
6
+ "subdomain": "test",
7
+ "currency": "RUB",
8
+ "paid_from": false,
9
+ "paid_till": false,
10
+ "timezone": "Europe/Moscow",
11
+ "language": "ru",
12
+ "date_pattern": "d.m.Y H:i",
13
+ "limits": {
14
+ "users_count": false,
15
+ "contacts_count": false,
16
+ "active_deals_count": false
17
+ },
18
+ "users": [
19
+ {
20
+ "id": "337914",
21
+ "name": "sergey",
22
+ "last_name": null,
23
+ "login": "alekseenkoss@gmail.com",
24
+ "group_id": 0,
25
+ "rights_lead_add": "A",
26
+ "rights_lead_view": "A",
27
+ "rights_lead_edit": "A",
28
+ "rights_lead_delete": "A",
29
+ "rights_lead_export": "A",
30
+ "rights_contact_add": "A",
31
+ "rights_contact_view": "A",
32
+ "rights_contact_edit": "A",
33
+ "rights_contact_delete": "A",
34
+ "rights_contact_export": "A",
35
+ "rights_company_add": "A",
36
+ "rights_company_view": "A",
37
+ "rights_company_edit": "A",
38
+ "rights_company_delete": "A",
39
+ "rights_company_export": "A",
40
+ "is_admin": "Y"
41
+ }
42
+ ],
43
+ "groups": [],
44
+ "leads_statuses": [
45
+ {
46
+ "name": "Первичный контакт",
47
+ "id": "8195972",
48
+ "color": "#99CCFF",
49
+ "editable": "Y",
50
+ "sort": "10"
51
+ },
52
+ {
53
+ "name": "Переговоры",
54
+ "id": "8195974",
55
+ "color": "#FFFF99",
56
+ "editable": "Y",
57
+ "sort": "20"
58
+ },
59
+ {
60
+ "name": "Принимают решение",
61
+ "id": "8195976",
62
+ "color": "#FFCC66",
63
+ "editable": "Y",
64
+ "sort": "30"
65
+ },
66
+ {
67
+ "name": "Согласование договора",
68
+ "id": "8195978",
69
+ "color": "#FFCCCC",
70
+ "editable": "Y",
71
+ "sort": "40"
72
+ },
73
+ {
74
+ "name": "Успешно реализовано",
75
+ "id": "142",
76
+ "color": "#CCFF66",
77
+ "editable": "N",
78
+ "sort": "600"
79
+ },
80
+ {
81
+ "name": "Закрыто и не реализовано",
82
+ "id": "143",
83
+ "color": "#D5D8DB",
84
+ "editable": "N",
85
+ "sort": "700"
86
+ }
87
+ ],
88
+ "custom_fields": {
89
+ "contacts": [
90
+ {
91
+ "id": "1460587",
92
+ "name": "Должность",
93
+ "code": "POSITION",
94
+ "multiple": "N",
95
+ "type_id": "1",
96
+ "disabled": "0"
97
+ },
98
+ {
99
+ "id": "1460589",
100
+ "name": "Телефон",
101
+ "code": "PHONE",
102
+ "multiple": "Y",
103
+ "type_id": "8",
104
+ "disabled": "0",
105
+ "enums": {
106
+ "3392086": "WORK",
107
+ "3392088": "WORKDD",
108
+ "3392090": "MOB",
109
+ "3392092": "FAX",
110
+ "3392094": "HOME",
111
+ "3392096": "OTHER"
112
+ }
113
+ },
114
+ {
115
+ "id": "1460591",
116
+ "name": "Email",
117
+ "code": "EMAIL",
118
+ "multiple": "Y",
119
+ "type_id": "8",
120
+ "disabled": "0",
121
+ "enums": {
122
+ "3392098": "WORK",
123
+ "3392100": "PRIV",
124
+ "3392102": "OTHER"
125
+ }
126
+ },
127
+ {
128
+ "id": "1460595",
129
+ "name": "Мгн. сообщения",
130
+ "code": "IM",
131
+ "multiple": "Y",
132
+ "type_id": "8",
133
+ "disabled": "0",
134
+ "enums": {
135
+ "3392104": "SKYPE",
136
+ "3392106": "ICQ",
137
+ "3392108": "JABBER",
138
+ "3392110": "GTALK",
139
+ "3392112": "MSN",
140
+ "3392114": "OTHER"
141
+ }
142
+ },
143
+ {
144
+ "id": "116302",
145
+ "name": "teachbase_id",
146
+ "multiple": "N",
147
+ "type_id": "8"
148
+ }
149
+ ],
150
+ "leads": [
151
+ {
152
+ "id": "484604",
153
+ "name": "textfield",
154
+ "code": null,
155
+ "multiple": "N",
156
+ "type_id": "1"
157
+ },
158
+ {
159
+ "id": "484606",
160
+ "name": "Flag",
161
+ "code": null,
162
+ "multiple": "N",
163
+ "type_id": "3"
164
+ }
165
+ ],
166
+ "companies": [
167
+ {
168
+ "id": "1460589",
169
+ "name": "Телефон",
170
+ "code": "PHONE",
171
+ "multiple": "Y",
172
+ "type_id": "8",
173
+ "disabled": "0",
174
+ "enums": {
175
+ "3392086": "WORK",
176
+ "3392088": "WORKDD",
177
+ "3392090": "MOB",
178
+ "3392092": "FAX",
179
+ "3392094": "HOME",
180
+ "3392096": "OTHER"
181
+ }
182
+ },
183
+ {
184
+ "id": "1460591",
185
+ "name": "Email",
186
+ "code": "EMAIL",
187
+ "multiple": "Y",
188
+ "type_id": "8",
189
+ "disabled": "0",
190
+ "enums": {
191
+ "3392098": "WORK",
192
+ "3392100": "PRIV",
193
+ "3392102": "OTHER"
194
+ }
195
+ },
196
+ {
197
+ "id": "1460593",
198
+ "name": "Web",
199
+ "code": "WEB",
200
+ "multiple": "N",
201
+ "type_id": "7",
202
+ "disabled": "0"
203
+ },
204
+ {
205
+ "id": "1460597",
206
+ "name": "Адрес",
207
+ "code": "ADDRESS",
208
+ "multiple": "N",
209
+ "type_id": "9",
210
+ "disabled": "0"
211
+ }
212
+ ]
213
+ },
214
+ "note_types": [
215
+ {
216
+ "id": 1,
217
+ "name": "",
218
+ "code": "DEAL_CREATED",
219
+ "editable": "N"
220
+ },
221
+ {
222
+ "id": 2,
223
+ "name": "",
224
+ "code": "CONTACT_CREATED",
225
+ "editable": "N"
226
+ },
227
+ {
228
+ "id": 3,
229
+ "name": "",
230
+ "code": "DEAL_STATUS_CHANGED",
231
+ "editable": "N"
232
+ },
233
+ {
234
+ "id": 4,
235
+ "name": "",
236
+ "code": "COMMON",
237
+ "editable": "Y"
238
+ },
239
+ {
240
+ "id": 5,
241
+ "name": "",
242
+ "code": "ATTACHEMENT",
243
+ "editable": "N"
244
+ },
245
+ {
246
+ "id": 6,
247
+ "name": "",
248
+ "code": "CALL",
249
+ "editable": "N"
250
+ },
251
+ {
252
+ "id": 7,
253
+ "name": "",
254
+ "code": "MAIL_MESSAGE",
255
+ "editable": "N"
256
+ },
257
+ {
258
+ "id": 8,
259
+ "name": "",
260
+ "code": "MAIL_MESSAGE_ATTACHMENT",
261
+ "editable": "N"
262
+ },
263
+ {
264
+ "id": 9,
265
+ "name": "",
266
+ "code": "EXTERNAL_ATTACH",
267
+ "editable": "N"
268
+ },
269
+ {
270
+ "id": 10,
271
+ "name": "",
272
+ "code": "CALL_IN",
273
+ "editable": "N"
274
+ },
275
+ {
276
+ "id": 11,
277
+ "name": "",
278
+ "code": "CALL_OUT",
279
+ "editable": "N"
280
+ },
281
+ {
282
+ "id": 12,
283
+ "name": "",
284
+ "code": "COMPANY_CREATED",
285
+ "editable": "N"
286
+ },
287
+ {
288
+ "id": 13,
289
+ "name": "",
290
+ "code": "TASK_RESULT",
291
+ "editable": "N"
292
+ },
293
+ {
294
+ "id": 99,
295
+ "name": "",
296
+ "code": "MAX_SYSTEM",
297
+ "editable": "N"
298
+ },
299
+ {
300
+ "id": 101,
301
+ "name": "Ссылка",
302
+ "code": "DROPBOX",
303
+ "editable": "N"
304
+ },
305
+ {
306
+ "id": 102,
307
+ "name": "Входящее",
308
+ "code": "SMS_IN",
309
+ "editable": "N"
310
+ },
311
+ {
312
+ "id": 103,
313
+ "name": "Исходящее",
314
+ "code": "SMS_OUT",
315
+ "editable": "N"
316
+ }
317
+ ],
318
+ "task_types": [
319
+ {
320
+ "id": 1,
321
+ "name": "Q_TASKS_CLASS_FOLLOW_UP",
322
+ "code": "FOLLOW_UP"
323
+ },
324
+ {
325
+ "id": 1,
326
+ "name": "Звонок",
327
+ "code": "CALL"
328
+ },
329
+ {
330
+ "id": 2,
331
+ "name": "Встреча",
332
+ "code": "MEETING"
333
+ },
334
+ {
335
+ "id": 3,
336
+ "name": "Письмо",
337
+ "code": "LETTER"
338
+ }
339
+ ],
340
+ "timezoneoffset": "+03:00"
341
+ },
342
+ "server_time": 1422442143
343
+ }
344
+ }
data/spec/fixtures/accounts/response_2.json ADDED
@@ -0,0 +1,195 @@
1
+ {
2
+ "response": {
3
+ "account": {
4
+ "id": "101",
5
+ "name": "База клиентов",
6
+ "subdomain": "custom_test",
7
+ "currency": "RUB",
8
+ "paid_from": false,
9
+ "paid_till": false,
10
+ "timezone": "Europe/Moscow",
11
+ "language": "ru",
12
+ "date_pattern": "d.m.Y H:i",
13
+ "limits": {
14
+ "users_count": false,
15
+ "contacts_count": false,
16
+ "active_deals_count": false
17
+ },
18
+ "users": [
19
+ {
20
+ "id": "337914",
21
+ "name": "sergey",
22
+ "last_name": null,
23
+ "login": "alekseenkoss@gmail.com",
24
+ "group_id": 0,
25
+ "rights_lead_add": "A",
26
+ "rights_lead_view": "A",
27
+ "rights_lead_edit": "A",
28
+ "rights_lead_delete": "A",
29
+ "rights_lead_export": "A",
30
+ "rights_contact_add": "A",
31
+ "rights_contact_view": "A",
32
+ "rights_contact_edit": "A",
33
+ "rights_contact_delete": "A",
34
+ "rights_contact_export": "A",
35
+ "rights_company_add": "A",
36
+ "rights_company_view": "A",
37
+ "rights_company_edit": "A",
38
+ "rights_company_delete": "A",
39
+ "rights_company_export": "A",
40
+ "is_admin": "Y"
41
+ }
42
+ ],
43
+ "groups": [],
44
+ "leads_statuses": [
45
+ {
46
+ "name": "Demo",
47
+ "id": "101",
48
+ "color": "#99CCFF",
49
+ "editable": "Y",
50
+ "sort": "10"
51
+ },
52
+ {
53
+ "name": "Closed",
54
+ "id": "102",
55
+ "color": "#FFFF99",
56
+ "editable": "Y",
57
+ "sort": "20"
58
+ }
59
+ ],
60
+ "custom_fields": {
61
+ "contacts": [
62
+ {
63
+ "id": "201",
64
+ "name": "Должность",
65
+ "code": "POSITION",
66
+ "multiple": "N",
67
+ "type_id": "1",
68
+ "disabled": "0"
69
+ },
70
+ {
71
+ "id": "202",
72
+ "name": "Телефон",
73
+ "code": "PHONE",
74
+ "multiple": "Y",
75
+ "type_id": "8",
76
+ "disabled": "0",
77
+ "enums": {
78
+ "3392086": "WORK",
79
+ "3392088": "WORKDD",
80
+ "3392090": "MOB",
81
+ "3392092": "FAX",
82
+ "3392094": "HOME",
83
+ "3392096": "OTHER"
84
+ }
85
+ },
86
+ {
87
+ "id": "203",
88
+ "name": "Email",
89
+ "code": "EMAIL",
90
+ "multiple": "Y",
91
+ "type_id": "8",
92
+ "disabled": "0",
93
+ "enums": {
94
+ "3392098": "WORK",
95
+ "3392100": "PRIV",
96
+ "3392102": "OTHER"
97
+ }
98
+ },
99
+ {
100
+ "id": "204",
101
+ "name": "Мгн. сообщения",
102
+ "code": "IM",
103
+ "multiple": "Y",
104
+ "type_id": "8",
105
+ "disabled": "0",
106
+ "enums": {
107
+ "3392104": "SKYPE",
108
+ "3392106": "ICQ",
109
+ "3392108": "JABBER",
110
+ "3392110": "GTALK",
111
+ "3392112": "MSN",
112
+ "3392114": "OTHER"
113
+ }
114
+ }
115
+ ],
116
+ "leads": [],
117
+ "companies": [
118
+ {
119
+ "id": "301",
120
+ "name": "Телефон",
121
+ "code": "PHONE",
122
+ "multiple": "Y",
123
+ "type_id": "8",
124
+ "disabled": "0",
125
+ "enums": {
126
+ "3392086": "WORK",
127
+ "3392088": "WORKDD",
128
+ "3392090": "MOB",
129
+ "3392092": "FAX",
130
+ "3392094": "HOME",
131
+ "3392096": "OTHER"
132
+ }
133
+ },
134
+ {
135
+ "id": "302",
136
+ "name": "Email",
137
+ "code": "EMAIL",
138
+ "multiple": "Y",
139
+ "type_id": "8",
140
+ "disabled": "0",
141
+ "enums": {
142
+ "3392098": "WORK",
143
+ "3392100": "PRIV",
144
+ "3392102": "OTHER"
145
+ }
146
+ },
147
+ {
148
+ "id": "303",
149
+ "name": "Web",
150
+ "code": "WEB",
151
+ "multiple": "N",
152
+ "type_id": "7",
153
+ "disabled": "0"
154
+ },
155
+ {
156
+ "id": "304",
157
+ "name": "Адрес",
158
+ "code": "ADDRESS",
159
+ "multiple": "N",
160
+ "type_id": "9",
161
+ "disabled": "0"
162
+ }
163
+ ]
164
+ },
165
+ "note_types": [
166
+ {
167
+ "id": 1000,
168
+ "name": "",
169
+ "code": "DEMO_REQUESTED",
170
+ "editable": "N"
171
+ },
172
+ {
173
+ "id": 1001,
174
+ "name": "",
175
+ "code": "BILL_CREATED",
176
+ "editable": "N"
177
+ }
178
+ ],
179
+ "task_types": [
180
+ {
181
+ "id": 1,
182
+ "name": "Звонок",
183
+ "code": "CALL"
184
+ },
185
+ {
186
+ "id": 2,
187
+ "name": "Встреча",
188
+ "code": "MEETING"
189
+ }
190
+ ],
191
+ "timezoneoffset": "+03:00"
192
+ },
193
+ "server_time": 1422442143
194
+ }
195
+ }
data/spec/fixtures/amorail_test.yml ADDED
@@ -0,0 +1,3 @@
1
+ usermail: 'amorail@test.com'
2
+ api_key: '75742b166417fe32ae132282ce178cf6'
3
+ api_endpoint: 'https://test.amocrm.ru'
data/spec/fixtures/contacts/create.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "response": {
3
+ "contacts": {
4
+ "add": [
5
+ {
6
+ "id": 101,
7
+ "request_id": 0
8
+ }
9
+ ]
10
+ },
11
+ "server_time": 1423139130
12
+ }
13
+ }
data/spec/fixtures/contacts/find_many.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "response": {
3
+ "contacts": [
4
+ {
5
+ "id": 101,
6
+ "name": "Foo bar",
7
+ "account_id": "8195968",
8
+ "last_modified": 1423139130,
9
+ "company_name": "Foo Inc.",
10
+ "custom_fields": [
11
+ {
12
+ "id": "1460591",
13
+ "name": "Email",
14
+ "code": "EMAIL",
15
+ "values": [
16
+ {
17
+ "value": "foo@tb.com",
18
+ "enum": "3392098"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "id": "1460589",
24
+ "name": "Телефон",
25
+ "code": "PHONE",
26
+ "values": [
27
+ {
28
+ "value": "1111 111 111",
29
+ "enum": "3392086"
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "id": 102,
37
+ "name": "Foo bar 2",
38
+ "account_id": "8195968",
39
+ "last_modified": 1423139150,
40
+ "company_name": "Foo Inc.",
41
+ "custom_fields": [
42
+ {
43
+ "id": "1460591",
44
+ "name": "Email",
45
+ "code": "EMAIL",
46
+ "values": [
47
+ {
48
+ "value": "foo2@tb.com",
49
+ "enum": "3392098"
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+ }
data/spec/fixtures/contacts/find_one.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "response": {
3
+ "contacts": [
4
+ {
5
+ "id": 101,
6
+ "name": "Foo bar",
7
+ "account_id": "8195968",
8
+ "last_modified": 1423139130,
9
+ "company_name": "Foo Inc.",
10
+ "linked_leads_id": [
11
+ "1872746",
12
+ "1885024"
13
+ ],
14
+ "custom_fields": [
15
+ {
16
+ "id": "1460591",
17
+ "name": "Email",
18
+ "code": "EMAIL",
19
+ "values": [
20
+ {
21
+ "value": "foo@tb.com",
22
+ "enum": "3392098"
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "id": "1460589",
28
+ "name": "Телефон",
29
+ "code": "PHONE",
30
+ "values": [
31
+ {
32
+ "value": "1111 111 111",
33
+ "enum": "3392086"
34
+ }
35
+ ]
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
41
+ }
data/spec/fixtures/contacts/links.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "response": {
3
+ "links": [
4
+ {
5
+ "contact_id": "101",
6
+ "lead_id": "1",
7
+ "last_modified": 1374741830
8
+ },
9
+ {
10
+ "contact_id": "101",
11
+ "lead_id": "2",
12
+ "last_modified": 1374839942
13
+ }
14
+ ]
15
+ }
16
+ }
data/spec/fixtures/contacts/my_contact_find.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "response": {
3
+ "contacts": [
4
+ {
5
+ "id": 11,
6
+ "name": "Foo bar",
7
+ "account_id": "8195968",
8
+ "last_modified": 1423139130,
9
+ "company_name": "Foo Inc.",
10
+ "custom_fields": [
11
+ {
12
+ "id": "1460591",
13
+ "name": "Email",
14
+ "code": "EMAIL",
15
+ "values": [
16
+ {
17
+ "value": "foo@tb.com",
18
+ "enum": "3392098"
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "id": "1460589",
24
+ "name": "Телефон",
25
+ "code": "PHONE",
26
+ "values": [
27
+ {
28
+ "value": "1111 111 111",
29
+ "enum": "3392086"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "id": "116302",
35
+ "name": "teachbase_id",
36
+ "values": [
37
+ {
38
+ "value": 1123,
39
+ "enum": "3392090"
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ }
data/spec/fixtures/contacts/update.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "response": {
3
+ "contacts": {
4
+ "update": [
5
+ {
6
+ "id": 12509853,
7
+ "last_modified": 1501256276
8
+ }
9
+ ]
10
+ },
11
+ "server_time": 1501452197
12
+ }
13
+ }
data/spec/fixtures/leads/create.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "response": {
3
+ "leads": {
4
+ "add": [
5
+ {
6
+ "id": 3980037,
7
+ "request_id": 0
8
+ }
9
+ ]
10
+ },
11
+ "server_time": 1502394640
12
+ }
13
+ }
data/spec/fixtures/leads/find_many.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "response": {
3
+ "leads": [
4
+ {
5
+ "id": "1",
6
+ "name": "Research new technologies",
7
+ "last_modified": 1374656336,
8
+ "status_id": "7046196",
9
+ "pipeline_id": 683506,
10
+ "price": "500000",
11
+ "responsible_user_id": "103586",
12
+ "tags": [
13
+ {
14
+ "id": "960472",
15
+ "name": "USA"
16
+ },
17
+ {
18
+ "id": "960854",
19
+ "name": "Lead"
20
+ }
21
+ ],
22
+ "date_create": 1386014400,
23
+ "account_id": "7046192",
24
+ "created_user_id": "4502311",
25
+ "custom_fields": [
26
+ {
27
+ "id": "484604",
28
+ "name": "field",
29
+ "values": [
30
+ {
31
+ "value": "text"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "id": "2",
39
+ "name": "Sell it!",
40
+ "last_modified": 1374656336,
41
+ "status_id": "7046196",
42
+ "pipeline_id": 683506,
43
+ "price": "100000",
44
+ "responsible_user_id": "103586",
45
+ "tags": [
46
+ {
47
+ "id": "960472",
48
+ "name": "USA"
49
+ },
50
+ {
51
+ "id": "960854",
52
+ "name": "Lead"
53
+ }
54
+ ],
55
+ "date_create": 1386014400,
56
+ "account_id": "7046192",
57
+ "created_user_id": "4502311",
58
+ "custom_fields": [
59
+ {
60
+ "id": "484604",
61
+ "name": "field",
62
+ "values": [
63
+ {
64
+ "value": "text"
65
+ }
66
+ ]
67
+ }
68
+ ]
69
+ }
70
+ ],
71
+ "server_time": 1374839787
72
+ }
73
+ }
data/spec/fixtures/leads/links.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "response": {
3
+ "links": [
4
+ {
5
+ "contact_id": "101",
6
+ "lead_id": "2",
7
+ "last_modified": 1374839942
8
+ },
9
+ {
10
+ "contact_id": "102",
11
+ "lead_id": "2",
12
+ "last_modified": 1374839942
13
+ }
14
+ ]
15
+ }
16
+ }
data