virtuous 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +26 -0
  3. data/.gitignore +5 -0
  4. data/.reek.yml +36 -0
  5. data/.rubocop.yml +87 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +18 -0
  9. data/Gemfile +17 -0
  10. data/LICENSE +21 -0
  11. data/README.md +54 -0
  12. data/Rakefile +24 -0
  13. data/lib/virtuous/client/contact.rb +220 -0
  14. data/lib/virtuous/client/contact_address.rb +78 -0
  15. data/lib/virtuous/client/gift.rb +394 -0
  16. data/lib/virtuous/client/gift_designation.rb +59 -0
  17. data/lib/virtuous/client/individual.rb +125 -0
  18. data/lib/virtuous/client/recurring_gift.rb +86 -0
  19. data/lib/virtuous/client.rb +272 -0
  20. data/lib/virtuous/error.rb +54 -0
  21. data/lib/virtuous/helpers/hash_helper.rb +28 -0
  22. data/lib/virtuous/helpers/string_helper.rb +31 -0
  23. data/lib/virtuous/parse_oj.rb +24 -0
  24. data/lib/virtuous/version.rb +5 -0
  25. data/lib/virtuous.rb +12 -0
  26. data/logo/virtuous.svg +1 -0
  27. data/spec/spec_helper.rb +25 -0
  28. data/spec/support/client_factory.rb +10 -0
  29. data/spec/support/fixtures/contact.json +112 -0
  30. data/spec/support/fixtures/contact_address.json +20 -0
  31. data/spec/support/fixtures/contact_addresses.json +42 -0
  32. data/spec/support/fixtures/contact_gifts.json +80 -0
  33. data/spec/support/fixtures/gift.json +55 -0
  34. data/spec/support/fixtures/gift_designation_query_options.json +2701 -0
  35. data/spec/support/fixtures/gift_designations.json +175 -0
  36. data/spec/support/fixtures/gifts.json +112 -0
  37. data/spec/support/fixtures/import.json +0 -0
  38. data/spec/support/fixtures/individual.json +46 -0
  39. data/spec/support/fixtures/recurring_gift.json +26 -0
  40. data/spec/support/fixtures_helper.rb +5 -0
  41. data/spec/support/virtuous_mock.rb +101 -0
  42. data/spec/virtuous/client_spec.rb +270 -0
  43. data/spec/virtuous/error_spec.rb +74 -0
  44. data/spec/virtuous/resources/contact_address_spec.rb +75 -0
  45. data/spec/virtuous/resources/contact_spec.rb +137 -0
  46. data/spec/virtuous/resources/gift_designation_spec.rb +70 -0
  47. data/spec/virtuous/resources/gift_spec.rb +249 -0
  48. data/spec/virtuous/resources/individual_spec.rb +95 -0
  49. data/spec/virtuous/resources/recurring_gift_spec.rb +67 -0
  50. data/spec/virtuous_spec.rb +7 -0
  51. data/virtuous.gemspec +25 -0
  52. metadata +121 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c435c6a389b27d1644f8fa32d0df952aa9bcb83baf152973c8e2f55b0b418a1
4
+ data.tar.gz: 13075eb9634a01b1597c6a8be58444b1ec89738efe8842b124e4e640237163a4
5
+ SHA512:
6
+ metadata.gz: 720aaf08ccabd9f7d1b7da3b07b4d028646c910c32605d85c4fe915050799eec9880b70f377674af848af13cab25fe18e92c3c6f460393499739188d5010ed29
7
+ data.tar.gz: 8b65f36c8bef728c092210f0448769b2d581d445755ca35d23eea6d1ae9e624bc9bad1602fa5edc4f0f45ac9920cf5f6c02b5877f07444366133422f9457d4bf
@@ -0,0 +1,26 @@
1
+ name: Run specs
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ bundler-cache: true
19
+ - name: Install dependencies
20
+ run: bundle install
21
+ - name: Lint
22
+ run: |
23
+ bundle exec rubocop
24
+ bundle exec reek
25
+ - name: Run tests
26
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ virtuous-*.gem
2
+ .env
3
+ Gemfile.lock
4
+ doc
5
+ .yardoc
data/.reek.yml ADDED
@@ -0,0 +1,36 @@
1
+ detectors:
2
+ IrresponsibleModule:
3
+ enabled: false
4
+ DataClump:
5
+ enabled: false
6
+ FeatureEnvy:
7
+ exclude:
8
+ - "Virtuous::Client#connection"
9
+ - "Virtuous::Client#unauthorized_connection"
10
+ - "Virtuous::HashHelper#self.deep_transform_keys"
11
+ - "FaradayMiddleware::ParseOj#on_complete"
12
+ NilCheck:
13
+ exclude:
14
+ - "Virtuous::Client#initialize"
15
+ UtilityFunction:
16
+ public_methods_only: true
17
+ NestedIterators:
18
+ exclude:
19
+ - "Virtuous::HashHelper#self.deep_transform_keys"
20
+ TooManyStatements:
21
+ exclude:
22
+ - "Virtuous::HashHelper#self.deep_transform_keys"
23
+ - "Virtuous::Client#connection"
24
+ - "Virtuous::Client#unauthorized_connection"
25
+ - "FaradayMiddleware::VirtuousErrorHandler#on_complete"
26
+ ControlParameter:
27
+ exclude:
28
+ - "Virtuous::Client#initialize"
29
+ NilCheck:
30
+ enabled: false
31
+ TooManyInstanceVariables:
32
+ exclude:
33
+ - "Virtuous::Client"
34
+ InstanceVariableAssumption:
35
+ exclude:
36
+ - "Virtuous::Client"
data/.rubocop.yml ADDED
@@ -0,0 +1,87 @@
1
+ # All cops
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.7
5
+ NewCops: "enable"
6
+ Exclude:
7
+ - "bin/*"
8
+ - "vendor/**/*"
9
+
10
+ # Layout cops
11
+
12
+ Layout/LineLength:
13
+ Max: 100
14
+ # To make it possible to copy or click on URIs in the code, we allow lines
15
+ # containing a URI to be longer than Max.
16
+ AllowURI: true
17
+ URISchemes:
18
+ - http
19
+ - https
20
+
21
+ # Metrics cops
22
+
23
+ Metrics/AbcSize:
24
+ # The ABC size is a calculated magnitude, so this number can be an Integer or
25
+ # a Float.
26
+ Max: 15
27
+ Exclude:
28
+ - lib/virtuous/error.rb
29
+
30
+ Metrics/BlockLength:
31
+ CountComments: false # count full line comments?
32
+ Max: 25
33
+ Exclude:
34
+ - config/**/*
35
+ - spec/**/*
36
+
37
+ Metrics/BlockNesting:
38
+ Max: 4
39
+
40
+ Metrics/ClassLength:
41
+ CountComments: false # count full line comments?
42
+ Max: 200
43
+
44
+ # Avoid complex methods.
45
+ Metrics/CyclomaticComplexity:
46
+ Max: 6
47
+ Exclude:
48
+ - lib/virtuous/error.rb
49
+
50
+ Metrics/MethodLength:
51
+ CountComments: false # count full line comments?
52
+ Max: 24
53
+
54
+ Metrics/ModuleLength:
55
+ CountComments: false # count full line comments?
56
+ Max: 200
57
+
58
+ Metrics/ParameterLists:
59
+ Max: 5
60
+ CountKeywordArgs: true
61
+
62
+ Metrics/PerceivedComplexity:
63
+ Max: 12
64
+
65
+ # Style cops
66
+
67
+ Style/Documentation:
68
+ Enabled: false
69
+
70
+ Style/FrozenStringLiteralComment:
71
+ Enabled: false
72
+
73
+ Style/ModuleFunction:
74
+ Enabled: false
75
+
76
+ Style/SymbolArray:
77
+ Enabled: false
78
+
79
+ # Lint cops
80
+
81
+ Lint/AmbiguousBlockAssociation:
82
+ Exclude:
83
+ - spec/**/*
84
+
85
+ Naming/MemoizedInstanceVariableName:
86
+ Exclude:
87
+ - lib/virtuous/client.rb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - A Client class with support for api key and OAuth authentication
13
+ - Methods to find, create and update contacts
14
+ - Methods to find, create, update and delete individuals
15
+ - Methods to find, create, update and delete gifts
16
+ - Method query gift designations
17
+
18
+ [unreleased]: https://github.com/taylorbrooks/virtuous/compare/v0.0.0...HEAD
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'bundler', '~> 2.3'
8
+ gem 'dotenv', '~> 2.8.1'
9
+ gem 'pry', '~> 0.14.2'
10
+ gem 'puma', '~> 6.4'
11
+ gem 'rake', '~> 12.3.3'
12
+ gem 'reek', '~> 6.1'
13
+ gem 'rspec', '~> 3.7'
14
+ gem 'rubocop', '~> 1.57'
15
+ gem 'sinatra', '~> 2.0'
16
+ gem 'webmock', '~> 3.1'
17
+ gem 'yard', '~> 0.9'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Taylor Brooks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ <img src="./logo/virtuous.svg" width="200" />
2
+
3
+ # Virtuous Ruby Client ![example workflow](https://github.com/taylorbrooks/virtuous/actions/workflows/test.yml/badge.svg)
4
+
5
+ A Ruby wrapper for the Virtuous API
6
+
7
+ To get a general overview of Virtuous: https://virtuous.org
8
+
9
+ [RDocs](https://rubydoc.info/github/taylorbrooks/virtuous/master)
10
+
11
+ ### Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ # in your Gemfile
17
+ gem 'virtuous', '~> 0.0.1'
18
+
19
+ # then...
20
+ bundle install
21
+ ```
22
+
23
+ ### Usage
24
+
25
+ ```ruby
26
+ # Authenticating with username and password
27
+ client = Virtuous::Client.new
28
+ client.authenticate(username: ..., password: ...)
29
+
30
+ # Authenticating with api keys
31
+ client = Virtuous::Client.new(api_key: ...)
32
+
33
+ # Find a specific contact
34
+ client.find_contact_by_email('gob@bluthco.com')
35
+ ```
36
+
37
+ ### History
38
+
39
+ View the [changelog](https://github.com/taylorbrooks/virtuous/blob/master/CHANGELOG.md)
40
+
41
+ This gem follows [Semantic Versioning](http://semver.org/)
42
+
43
+ ### Contributing
44
+
45
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
46
+
47
+ - [Report bugs](https://github.com/taylorbrooks/virtuous/issues)
48
+ - Fix bugs and [submit pull requests](https://github.com/taylorbrooks/virtuous/pulls)
49
+ - Write, clarify, or fix documentation
50
+ - Suggest or add new features
51
+
52
+ ### Copyright
53
+
54
+ Copyright (c) 2018 Taylor Brooks. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
7
+
8
+ task :environment do
9
+ require 'dotenv'
10
+ Dotenv.load
11
+
12
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
13
+ require 'virtuous'
14
+ end
15
+
16
+ desc 'Launch a pry shell with libraries loaded'
17
+ task pry: :environment do
18
+ options = {}
19
+ options[:logger] = Logger.new($stdout) unless ENV['CLIENT_LOGGER'].nil?
20
+ @client = Virtuous::Client.new(**options) if ENV['VIRTUOUS_KEY']
21
+
22
+ require 'pry'
23
+ Pry.start
24
+ end
@@ -0,0 +1,220 @@
1
+ module Virtuous
2
+ class Client
3
+ ##
4
+ # ### Contact data
5
+ #
6
+ # {
7
+ # contact_type: [String],
8
+ # reference_source: [String],
9
+ # reference_id: [String],
10
+ # name: [String],
11
+ # informal_name: [String],
12
+ # description: [String],
13
+ # website: [String],
14
+ # marital_status: [String],
15
+ # anniversary_month: [Integer],
16
+ # anniversary_day: [Integer],
17
+ # anniversary_year: [Integer],
18
+ # origin_segment_id: [Integer],
19
+ # is_private: [Boolean],
20
+ # is_archived: [Boolean],
21
+ # contact_addresses: [
22
+ # {
23
+ # label: [String],
24
+ # address1: [String],
25
+ # address2: [String],
26
+ # city: [String],
27
+ # state_code: [String],
28
+ # postal: [String],
29
+ # country_code: [String],
30
+ # is_primary: [Boolean],
31
+ # latitude: [Float],
32
+ # longitude: [Float]
33
+ # }
34
+ # ],
35
+ # contact_individuals: [
36
+ # {
37
+ # first_name: [String],
38
+ # last_name: [String],
39
+ # prefix: [String],
40
+ # middle_name: [String],
41
+ # suffix: [String],
42
+ # birth_month: [Integer],
43
+ # birth_day: [Integer],
44
+ # birth_year: [Integer],
45
+ # approximate_age: [Integer],
46
+ # gender: [String],
47
+ # passion: [String],
48
+ # is_primary: [Boolean],
49
+ # is_secondary: [Boolean],
50
+ # is_deceased: [Boolean],
51
+ # contact_methods: [
52
+ # {
53
+ # type: [String],
54
+ # value: [String],
55
+ # is_opted_in: [Boolean],
56
+ # is_primary: [Boolean]
57
+ # }
58
+ # ],
59
+ # custom_fields: [Hash]
60
+ # }
61
+ # ],
62
+ # custom_fields: [Hash],
63
+ # custom_collections: [
64
+ # {
65
+ # name: [String],
66
+ # fields: [
67
+ # {
68
+ # name: [String],
69
+ # value: [String]
70
+ # }
71
+ # ]
72
+ # }
73
+ # ]
74
+ # }
75
+ #
76
+ module Contact
77
+ ##
78
+ # Fetches a contact record by email.
79
+ #
80
+ # @example
81
+ # client.find_contact_by_email('contact@email.com')
82
+ #
83
+ # @param email [String] The email of the contact.
84
+ #
85
+ # @return [Hash] The contact information in a hash.
86
+ def find_contact_by_email(email)
87
+ parse(get('api/Contact/Find', { email: email }))
88
+ end
89
+
90
+ ##
91
+ # Fetches a contact record by id.
92
+ #
93
+ # @example
94
+ # client.get_contact(1)
95
+ #
96
+ # @param id [Integer] The id of the contact.
97
+ #
98
+ # @return [Hash] The contact information in a hash.
99
+ def get_contact(id)
100
+ parse(get("api/Contact/#{id}"))
101
+ end
102
+
103
+ ##
104
+ # Creates a contact. This will use the virtuous import tool to match the new contact
105
+ # with existing ones. If the contact record exists already but there is new
106
+ # information the record will be flagged for review.
107
+ #
108
+ # Transactions are posted to the API and are set to a holding state.
109
+ # At midnight, transactions are bundled into imports based on the source they were posted
110
+ # with.
111
+ # The organization reviews the imported transactions, and then clicks run.
112
+ #
113
+ # @example
114
+ # client.import_contact(
115
+ # contact_type: 'Organization', name: 'Org name',
116
+ # first_name: 'John', last_name: 'Doe'
117
+ # )
118
+ #
119
+ # @param data [Hash] A hash containing the contact details.
120
+ #
121
+ # #### Required fields
122
+ # - `contact_type`: "Household", "Organization", "Foundation" or a custom type.
123
+ # - `contact_name`: required if Organization or Foundation.
124
+ # - `first_name`
125
+ # - `last_name`
126
+ #
127
+ # #### Suggested fields
128
+ # - `reference_source`: the system it came from.
129
+ # - `reference_id`: the ID in the original system.
130
+ # - `email`
131
+ # - `phone`
132
+ # - `address`
133
+ #
134
+ # #### Full list of accepted fields
135
+ #
136
+ # {
137
+ # reference_source: [String],
138
+ # reference_id: [String],
139
+ # contact_type: [String],
140
+ # name: [String],
141
+ # title: [String],
142
+ # first_name: [String],
143
+ # middle_name: [String],
144
+ # last_name: [String],
145
+ # suffix: [String],
146
+ # email_type: [String],
147
+ # email: [String],
148
+ # phone_type: [String],
149
+ # phone: [String],
150
+ # address1: [String],
151
+ # address2: [String],
152
+ # city: [String],
153
+ # state: [String],
154
+ # postal: [String],
155
+ # country: [String],
156
+ # event_id: [Integer],
157
+ # event_name: [String],
158
+ # invited: [Boolean],
159
+ # rsvp: [Boolean],
160
+ # rsvp_response: [Boolean],
161
+ # attended: [Boolean],
162
+ # tags: [String],
163
+ # origin_segment_code: [String],
164
+ # email_lists: [String[]],
165
+ # custom_fields: [Hash],
166
+ # volunteer_attendances: [
167
+ # {
168
+ # volunteer_opportunity_id: [Integer],
169
+ # volunteer_opportunity_name: [String],
170
+ # date: [String],
171
+ # hours: [String]
172
+ # }
173
+ # ]
174
+ # }
175
+ #
176
+ def import_contact(data)
177
+ post('api/Contact/Transaction', format(data))
178
+ end
179
+
180
+ ##
181
+ # Creates a contact.
182
+ #
183
+ # @example
184
+ # client.create_contact(
185
+ # contact_type: 'Organization', name: 'Org name',
186
+ # contact_individuals: [
187
+ # { first_name: 'John', last_name: 'Doe' }
188
+ # ]
189
+ # )
190
+ #
191
+ # @param data [Hash] A hash containing the contact details.
192
+ # Refer to the [Contact data](#label-Contact+data) section
193
+ # above to see the available attributes.
194
+ #
195
+ # @return [Hash] The contact that has been created.
196
+ def create_contact(data)
197
+ parse(post('api/Contact', format(data)))
198
+ end
199
+
200
+ ##
201
+ # Updates a contact.
202
+ #
203
+ # @example
204
+ # client.update_contact(1, contact_type: 'Organization', name: 'New name')
205
+ #
206
+ # @note Excluding a property will remove it's value from the object.
207
+ # If you're only updating a single property, the entire model is still required.
208
+ #
209
+ # @param id [Integer] The id of the contact to update.
210
+ # @param data [Hash] A hash containing the contact details.
211
+ # Refer to the [Contact data](#label-Contact+data) section
212
+ # above to see the available attributes.
213
+ #
214
+ # @return [Hash] The contact that has been updated.
215
+ def update_contact(id, data)
216
+ parse(put("api/Contact/#{id}", format(data)))
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,78 @@
1
+ module Virtuous
2
+ class Client
3
+ ##
4
+ # ### Contact Address data
5
+ #
6
+ # {
7
+ # contact_id: [Integer],
8
+ # label: [String],
9
+ # address1: [String],
10
+ # address2: [String],
11
+ # city: [String],
12
+ # state: [String],
13
+ # postal: [String],
14
+ # country: [String],
15
+ # set_as_primary: [Boolean],
16
+ # start_month: [Integer],
17
+ # start_day: [Integer],
18
+ # end_month: [Integer],
19
+ # end_day: [Integer]
20
+ # }
21
+ #
22
+ module ContactAddress
23
+ ##
24
+ # Gets the addresses of a contact.
25
+ #
26
+ # @example
27
+ # client.get_contact_addresses(1)
28
+ #
29
+ # @param contact_id [Integer] The id of the Contact.
30
+ #
31
+ # @return [Array] An array with all the addresses of a contact.
32
+ #
33
+ def get_contact_addresses(contact_id)
34
+ response = get("api/ContactAddress/ByContact/#{contact_id}")
35
+ response.map { |address| parse(address) }
36
+ end
37
+
38
+ ##
39
+ # Updates an address.
40
+ #
41
+ # @example
42
+ # client.update_contact_address(
43
+ # 1, label: 'Home address', address1: '324 Frank Island', address2: 'Apt. 366',
44
+ # city: 'Antonioborough', state: 'Massachusetts', postal: '27516', country: 'USA'
45
+ # )
46
+ #
47
+ # @note Excluding a property will remove it's value from the object.
48
+ # If you're only updating a single property, the entire model is still required.
49
+ #
50
+ # @param id [Integer] The id of the address to update.
51
+ # @param data [Hash] A hash containing the address details.
52
+ # Refer to the data section above to see the available attributes.
53
+ #
54
+ # @return [Hash] The address that has been updated.
55
+ def update_contact_address(id, data)
56
+ parse(put("api/ContactAddress/#{id}", format(data)))
57
+ end
58
+
59
+ ##
60
+ # Creates an address.
61
+ #
62
+ # @example
63
+ # client.create_contact_address(
64
+ # contact_id: 1, label: 'Home address', address1: '324 Frank Island',
65
+ # address2: 'Apt. 366', city: 'Antonioborough', state: 'Massachusetts', postal: '27516',
66
+ # country: 'USA'
67
+ # )
68
+ #
69
+ # @param data [Hash] A hash containing the address details.
70
+ # Refer to the data section above to see the available attributes.
71
+ #
72
+ # @return [Hash] The address that has been created.
73
+ def create_contact_address(data)
74
+ parse(post('api/ContactAddress', format(data)))
75
+ end
76
+ end
77
+ end
78
+ end