amorail 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 151952934c801a5cce9b699869f1e46bd88f4e6a
4
- data.tar.gz: 9ed39010c5f080bd68c07d35fc3434edba549b4d
3
+ metadata.gz: 7d135728d3ff91d041ff0114f7674f548758ede9
4
+ data.tar.gz: 66c9aef842b6989ceb4eca793358e9d6c61140ca
5
5
  SHA512:
6
- metadata.gz: a11232b9fbc2f0b93f87d90e14c8ac068dff7281e732727156229708e3cb8bb0f1406840922ad203f41cbd451bd0d926e75d46c1c37a3f2893ff20a65d100069
7
- data.tar.gz: 0eba066877b078d2676e0c9896b5c36a6a9451cf5894c4cabe0b83377c2d65b528970e8bc5bfc2cdd7e2185d789136fb8db8ce0faa2034181b624bd03997dce8
6
+ metadata.gz: 9bb9ea6019c46ce4ec6778abbc45d6d1c1c7dee9fe99177ab9a7753b810d7a6495c9f813eab700309b17e9f3e85bf7af359d4b1e798c09d5355244a3aadb2377
7
+ data.tar.gz: 2671b2d09412cda7bba66d90961a474fad00848a39e004cf22f6ecb76821e98412966151158f855caee92631ed8dd31ff56217ac4e003d9836cdb7649a91f9f7
data/README.md CHANGED
@@ -22,7 +22,143 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
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
+ Also you can update objects, e.g:
132
+
133
+ ```ruby
134
+ company = Amorail::Company.find(company_id)
135
+ contact = Amorail::Contact.find(contact_id)
136
+
137
+ # like this
138
+ contact.linked_company_id = company.id
139
+ contact.save!
140
+
141
+ # or
142
+
143
+ contact.update(linked_company_id: company.id)
144
+ ```
145
+
146
+
147
+ ### Properties Configuration
148
+
149
+ AmoCRM is using "custom_fields" architecture,
150
+ to get all information for your account, you can
151
+ find properties and set up configuration manually in config/secrets.yml.
152
+
153
+ Note: response example in official documentation:
154
+ https://developers.amocrm.ru/rest_api/accounts_current.php
155
+
156
+ 1) Get list of properties for your account
157
+
158
+ ```
159
+ rake amorail:check
160
+ ```
161
+ Rake task will returns information about properties.
26
162
 
27
163
  ## Contributing
28
164
 
data/Rakefile CHANGED
@@ -6,3 +6,6 @@ RSpec::Core::RakeTask.new(:spec)
6
6
 
7
7
  task :default => :spec
8
8
 
9
+ task :console do
10
+ sh 'pry -r ./lib/amorail.rb'
11
+ end
@@ -71,7 +71,7 @@ module Amorail
71
71
  when 403
72
72
  fail ::Amorail::AmoForbiddenError
73
73
  when 404
74
- fail ::Amorail::AmoNotFoudError
74
+ fail ::Amorail::AmoNotFoundError
75
75
  when 500
76
76
  fail ::Amorail::AmoInternalError
77
77
  when 502
@@ -12,6 +12,11 @@ module Amorail
12
12
  class << self
13
13
  attr_reader :amo_name, :amo_response_name
14
14
 
15
+ # copy Amo names
16
+ def inherited(subclass)
17
+ subclass.amo_names amo_name, amo_response_name
18
+ end
19
+
15
20
  def amo_names(name, response_name = nil)
16
21
  @amo_name = @amo_response_name = name
17
22
  @amo_response_name = response_name unless response_name.nil?
@@ -34,17 +39,20 @@ module Amorail
34
39
  end
35
40
 
36
41
  def properties
37
- @properties ||= {}
42
+ @properties ||=
43
+ superclass.respond_to?(:properties) ? superclass.properties.dup : {}
38
44
  end
39
- end
40
45
 
41
- amo_names 'entity'
46
+ def remote_url(action)
47
+ File.join(Amorail.config.api_path, amo_name, action)
48
+ end
49
+ end
42
50
 
43
51
  amo_field :id, :request_id, :responsible_user_id,
44
52
  date_create: :timestamp, last_modified: :timestamp
45
53
 
46
54
  delegate :client, :properties, to: Amorail
47
- delegate :amo_name, to: :class
55
+ delegate :amo_name, :remote_url, to: :class
48
56
 
49
57
  def initialize(attributes = {})
50
58
  super(attributes)
@@ -75,16 +83,14 @@ module Amorail
75
83
  def merge_custom_fields(fields)
76
84
  return if fields.nil?
77
85
  fields.each do |f|
78
- fname = "#{f.fetch('code').downcase}="
86
+ fname = f['code'] || f['name']
87
+ next if fname.nil?
88
+ fname = "#{fname.downcase}="
79
89
  fval = f.fetch('values').first.fetch('value')
80
90
  send(fname, fval) if respond_to?(fname)
81
91
  end
82
92
  end
83
93
 
84
- def remote_url(action)
85
- File.join(Amorail.config.api_path, self.class.amo_name, action)
86
- end
87
-
88
94
  # call safe method <safe_request>. safe_request call authorize
89
95
  # if current session undefined or expires.
90
96
  def push(method)
@@ -1,15 +1,32 @@
1
1
  module Amorail # :nodoc: all
2
2
  class Entity
3
3
  class << self
4
+ # Find AMO entity by id
4
5
  def find(id)
5
6
  new.load_record(id)
6
7
  end
7
8
 
9
+ # Find AMO entity by id
10
+ # and raise RecordNotFound if nothing was found
8
11
  def find!(id)
9
12
  rec = find(id)
10
13
  fail RecordNotFound unless rec
11
14
  rec
12
15
  end
16
+
17
+ # Find AMO entities by query
18
+ # Returns array of matching entities.
19
+ def find_by_query(q)
20
+ response = Amorail.client.safe_request(
21
+ :get,
22
+ remote_url('list'),
23
+ query: q
24
+ )
25
+ return [] unless response.status == 200
26
+
27
+ (response.body['response'][amo_response_name] || [])
28
+ .map { |info| new.reload_model(info) }
29
+ end
13
30
  end
14
31
 
15
32
  def load_record(id)
@@ -13,7 +13,7 @@ module Amorail
13
13
 
14
14
  class AmoForbiddenError < APIError; end
15
15
 
16
- class AmoNotFoudError < APIError; end
16
+ class AmoNotFoundError < APIError; end
17
17
 
18
18
  class AmoInternalError < APIError; end
19
19
 
@@ -19,8 +19,10 @@ module Amorail
19
19
 
20
20
  def parse(data)
21
21
  hash = {}
22
- data['custom_fields'][source_name].each do |contact|
23
- hash[contact['code'].downcase] = PropertyItem.new(contact)
22
+ data['custom_fields'].fetch(source_name, []).each do |contact|
23
+ identifier = contact['code'].presence || contact['name'].presence
24
+ next if identifier.nil?
25
+ hash[identifier.downcase] = PropertyItem.new(contact)
24
26
  end
25
27
  new hash
26
28
  end
@@ -87,23 +89,33 @@ module Amorail
87
89
  self.source_name = 'companies'
88
90
  end
89
91
 
90
- class Lead < StatusItem
91
- def self.parse(data)
92
- hash = {}
93
- data['leads_statuses'].each do |prop|
94
- hash[prop['name']] = PropertyItem.new(prop)
92
+ class Lead < PropertyItem
93
+ self.source_name = 'leads'
94
+
95
+ attr_accessor :statuses
96
+
97
+ class << self
98
+ def parse(data)
99
+ obj = super
100
+ hash = {}
101
+ data.fetch('leads_statuses', []).each do |prop|
102
+ hash[prop['name']] = PropertyItem.new(prop)
103
+ end
104
+ obj.statuses = hash
105
+ obj
95
106
  end
96
- new hash
97
107
  end
98
108
  end
99
109
 
100
110
  class Task < PropertyItem
101
111
  def self.parse(data)
102
112
  hash = {}
103
- data['task_types'].each do |tt|
113
+ data.fetch('task_types', []).each do |tt|
104
114
  prop_item = PropertyItem.new(tt)
105
- hash[tt['code'].downcase] = prop_item
106
- hash[tt['code']] = prop_item
115
+ identifier = tt['code'].presence || tt['name'].presence
116
+ next if identifier.nil?
117
+ hash[identifier.downcase] = prop_item
118
+ hash[identifier] = prop_item
107
119
  end
108
120
  new hash
109
121
  end
@@ -1,4 +1,4 @@
1
1
  # Amorail version
2
2
  module Amorail
3
- VERSION = "0.1.6"
3
+ VERSION = "0.1.8"
4
4
  end
@@ -91,6 +91,28 @@ describe Amorail::Contact do
91
91
  end
92
92
  end
93
93
 
94
+ describe ".find_by_query" do
95
+ before { contacts_find_query_stub(Amorail.config.api_endpoint, 'foo') }
96
+ before { contacts_find_query_stub(Amorail.config.api_endpoint, 'faa', nil) }
97
+
98
+ it "loads entities" do
99
+ res = described_class.find_by_query('foo')
100
+ expect(res.size).to eq 2
101
+ expect(res.first.id).to eq 101
102
+ expect(res.last.id).to eq 102
103
+ expect(res.first.company_name).to eq "Foo Inc."
104
+ expect(res.last.email).to eq "foo2@tb.com"
105
+ expect(res.first.phone).to eq "1111 111 111"
106
+ expect(res.first.params[:id]).to eq 101
107
+ end
108
+
109
+ it "returns empty array" do
110
+ res = described_class.find_by_query('faa')
111
+ expect(res).to be_a(Array)
112
+ expect(res).to be_empty
113
+ end
114
+ end
115
+
94
116
  describe "#save" do
95
117
  before { contact_create_stub(Amorail.config.api_endpoint) }
96
118
 
@@ -114,13 +136,14 @@ describe Amorail::Contact do
114
136
 
115
137
  it "raise error if id is blank?" do
116
138
  obj = described_class.new
117
- expect { obj.update!(name: 'Igor') }.to raise_error
139
+ expect { obj.update!(name: 'Igor') }
140
+ .to raise_error(Amorail::Entity::NotPersisted)
118
141
  end
119
142
 
120
143
  it "raise error" do
121
144
  obj = described_class.new
122
145
  expect { obj.update!(id: 101, name: "Igor") }
123
- .to(raise_error)
146
+ .to(raise_error(Amorail::Entity::NotPersisted))
124
147
  end
125
148
  end
126
149
  end
@@ -1,6 +1,6 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Amorail::Entity do
3
+ describe MyEntity do
4
4
  before { mock_api }
5
5
 
6
6
  let(:entity) { described_class.new }
@@ -139,8 +139,30 @@
139
139
  "3392112": "MSN",
140
140
  "3392114": "OTHER"
141
141
  }
142
+ },
143
+ {
144
+ "id": "116302",
145
+ "name": "teachbase_id",
146
+ "multiple": "N",
147
+ "type_id": "8"
142
148
  }
143
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
+ ],
144
166
  "companies": [
145
167
  {
146
168
  "id": "1460589",
@@ -0,0 +1,59 @@
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
+ {
13
+ "id": "1460591",
14
+ "name": "Email",
15
+ "code": "EMAIL",
16
+ "values": [
17
+ {
18
+ "value": "foo@tb.com",
19
+ "enum": "3392098"
20
+ }
21
+ ]
22
+ },
23
+ {
24
+ "id": "1460589",
25
+ "name": "Телефон",
26
+ "code": "PHONE",
27
+ "values": [
28
+ {
29
+ "value": "1111 111 111",
30
+ "enum": "3392086"
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ "id": 102,
38
+ "name": "Foo bar 2",
39
+ "account_id": "8195968",
40
+ "last_modified": 1423139150,
41
+ "company_name": "Foo Inc.",
42
+ "custom_fields":
43
+ [
44
+ {
45
+ "id": "1460591",
46
+ "name": "Email",
47
+ "code": "EMAIL",
48
+ "values": [
49
+ {
50
+ "value": "foo2@tb.com",
51
+ "enum": "3392098"
52
+ }
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+ ]
58
+ }
59
+ }
@@ -0,0 +1,48 @@
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
+ {
13
+ "id": "1460591",
14
+ "name": "Email",
15
+ "code": "EMAIL",
16
+ "values": [
17
+ {
18
+ "value": "foo@tb.com",
19
+ "enum": "3392098"
20
+ }
21
+ ]
22
+ },
23
+ {
24
+ "id": "1460589",
25
+ "name": "Телефон",
26
+ "code": "PHONE",
27
+ "values": [
28
+ {
29
+ "value": "1111 111 111",
30
+ "enum": "3392086"
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "id": "116302",
36
+ "name": "teachbase_id",
37
+ "values": [
38
+ {
39
+ "value": 1123,
40
+ "enum": "3392090"
41
+ }
42
+ ]
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ }
@@ -1,3 +1,4 @@
1
+ # rubocop: disable Metrics/ModuleLength
1
2
  module AmoWebMock
2
3
  def mock_api
3
4
  authorize_stub(
@@ -85,6 +86,42 @@ module AmoWebMock
85
86
  end
86
87
  end
87
88
 
89
+ def my_contact_find_stub(endpoint, id, success = true)
90
+ if success
91
+ stub_request(
92
+ :get,
93
+ "#{endpoint}/private/api/v2/json/contacts/list?id=#{id}")
94
+ .to_return(
95
+ body: File.read('./spec/fixtures/my_contact_find.json'),
96
+ headers: { 'Content-Type' => 'application/json' },
97
+ status: 200
98
+ )
99
+ else
100
+ stub_request(
101
+ :get,
102
+ "#{endpoint}/private/api/v2/json/contacts/list?id=#{id}")
103
+ .to_return(body: nil, status: 204)
104
+ end
105
+ end
106
+
107
+ def contacts_find_query_stub(endpoint, query, success = true)
108
+ if success
109
+ stub_request(
110
+ :get,
111
+ "#{endpoint}/private/api/v2/json/contacts/list?query=#{query}")
112
+ .to_return(
113
+ body: File.read('./spec/fixtures/contact_find_query.json'),
114
+ headers: { 'Content-Type' => 'application/json' },
115
+ status: 200
116
+ )
117
+ else
118
+ stub_request(
119
+ :get,
120
+ "#{endpoint}/private/api/v2/json/contacts/list?query=#{query}")
121
+ .to_return(status: 204)
122
+ end
123
+ end
124
+
88
125
  def company_create_stub(endpoint)
89
126
  stub_request(:post, endpoint + '/private/api/v2/json/company/set')
90
127
  .to_return(
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ describe MyContact do
4
+ before { mock_api }
5
+
6
+ describe ".properties" do
7
+ subject { described_class.properties }
8
+
9
+ specify do
10
+ is_expected.to include(:email, :phone, :teachbase_id)
11
+ end
12
+ end
13
+
14
+ describe "#params" do
15
+ let(:company) do
16
+ described_class.new(
17
+ name: 'Test inc',
18
+ phone: '12345678',
19
+ email: 'test@mala.ru',
20
+ teachbase_id: 123
21
+ )
22
+ end
23
+
24
+ subject { company.params }
25
+
26
+ specify { is_expected.to include(:last_modified) }
27
+ specify { is_expected.to include(name: 'Test inc') }
28
+
29
+ it "contains custom property" do
30
+ prop = subject[:custom_fields].detect { |p| p[:id] == "116302" }
31
+ expect(prop).not_to be_nil
32
+ expect(prop[:values].first[:value]).to eq 123
33
+ end
34
+ end
35
+
36
+ describe ".find" do
37
+ before { my_contact_find_stub(Amorail.config.api_endpoint, 11) }
38
+
39
+ it "loads entity" do
40
+ obj = described_class.find(11)
41
+ expect(obj.id).to eq 11
42
+ expect(obj.company_name).to eq "Foo Inc."
43
+ expect(obj.email).to eq "foo@tb.com"
44
+ expect(obj.teachbase_id).to eq 1123
45
+ expect(obj.params[:id]).to eq 11
46
+ end
47
+ end
48
+ end
@@ -28,9 +28,12 @@ describe Amorail::Property do
28
28
  expect(prop.contacts.position.id).to eq "1460587"
29
29
  expect(prop.contacts.phone.id).to eq "1460589"
30
30
  expect(prop.contacts.email.id).to eq "1460591"
31
+ expect(prop.contacts.teachbase_id.id).to eq "116302"
31
32
  end
32
33
 
33
34
  it "should parse leads hash" do
35
+ expect(prop.leads.textfield.id).to eq "484604"
36
+ expect(prop.leads.flag.id).to eq "484606"
34
37
  expect(prop.leads.statuses["Первичный контакт"].id).to eq "8195972"
35
38
  expect(prop.leads.statuses["Успешно реализовано"].id).to eq "142"
36
39
  end
@@ -8,7 +8,8 @@ require 'webmock/rspec'
8
8
  require 'shoulda/matchers'
9
9
  require 'helpers/webmock_helpers'
10
10
 
11
- ENV.clear
11
+ # Cleanup Amorail env
12
+ ENV.delete_if { |k, _| k =~ /amorail/i }
12
13
  ENV["AMORAIL_CONF"] = File.expand_path("../fixtures/amorail_test.yml", __FILE__)
13
14
 
14
15
  Dir[File.expand_path("../support/**/*.rb", __FILE__)].each { |f| require f }
@@ -0,0 +1,3 @@
1
+ class MyContact < Amorail::Contact # :nodoc:
2
+ amo_property :teachbase_id
3
+ end
@@ -0,0 +1,4 @@
1
+ # We only need this class to set Amo names for core Entity
2
+ class MyEntity < Amorail::Entity # :nodoc:
3
+ amo_names 'entity'
4
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amorail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - alekseenkoss
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-04-27 00:00:00.000000000 Z
12
+ date: 2015-06-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -213,13 +213,18 @@ files:
213
213
  - spec/fixtures/amorail_test.yml
214
214
  - spec/fixtures/contact_create.json
215
215
  - spec/fixtures/contact_find.json
216
+ - spec/fixtures/contact_find_query.json
216
217
  - spec/fixtures/contact_update.json
218
+ - spec/fixtures/my_contact_find.json
217
219
  - spec/helpers/webmock_helpers.rb
218
220
  - spec/lead_spec.rb
221
+ - spec/my_contact_spec.rb
219
222
  - spec/property_spec.rb
220
223
  - spec/spec_helper.rb
221
224
  - spec/support/entity_class_example.rb
222
225
  - spec/support/leadable_example.rb
226
+ - spec/support/my_contact.rb
227
+ - spec/support/my_entity.rb
223
228
  - spec/task_spec.rb
224
229
  homepage: ''
225
230
  licenses:
@@ -254,12 +259,17 @@ test_files:
254
259
  - spec/fixtures/amorail_test.yml
255
260
  - spec/fixtures/contact_create.json
256
261
  - spec/fixtures/contact_find.json
262
+ - spec/fixtures/contact_find_query.json
257
263
  - spec/fixtures/contact_update.json
264
+ - spec/fixtures/my_contact_find.json
258
265
  - spec/helpers/webmock_helpers.rb
259
266
  - spec/lead_spec.rb
267
+ - spec/my_contact_spec.rb
260
268
  - spec/property_spec.rb
261
269
  - spec/spec_helper.rb
262
270
  - spec/support/entity_class_example.rb
263
271
  - spec/support/leadable_example.rb
272
+ - spec/support/my_contact.rb
273
+ - spec/support/my_entity.rb
264
274
  - spec/task_spec.rb
265
275
  has_rdoc: