virtuous 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +26 -0
- data/.gitignore +5 -0
- data/.reek.yml +36 -0
- data/.rubocop.yml +87 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +17 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/Rakefile +24 -0
- data/lib/virtuous/client/contact.rb +220 -0
- data/lib/virtuous/client/contact_address.rb +78 -0
- data/lib/virtuous/client/gift.rb +394 -0
- data/lib/virtuous/client/gift_designation.rb +59 -0
- data/lib/virtuous/client/individual.rb +125 -0
- data/lib/virtuous/client/recurring_gift.rb +86 -0
- data/lib/virtuous/client.rb +272 -0
- data/lib/virtuous/error.rb +54 -0
- data/lib/virtuous/helpers/hash_helper.rb +28 -0
- data/lib/virtuous/helpers/string_helper.rb +31 -0
- data/lib/virtuous/parse_oj.rb +24 -0
- data/lib/virtuous/version.rb +5 -0
- data/lib/virtuous.rb +12 -0
- data/logo/virtuous.svg +1 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/client_factory.rb +10 -0
- data/spec/support/fixtures/contact.json +112 -0
- data/spec/support/fixtures/contact_address.json +20 -0
- data/spec/support/fixtures/contact_addresses.json +42 -0
- data/spec/support/fixtures/contact_gifts.json +80 -0
- data/spec/support/fixtures/gift.json +55 -0
- data/spec/support/fixtures/gift_designation_query_options.json +2701 -0
- data/spec/support/fixtures/gift_designations.json +175 -0
- data/spec/support/fixtures/gifts.json +112 -0
- data/spec/support/fixtures/import.json +0 -0
- data/spec/support/fixtures/individual.json +46 -0
- data/spec/support/fixtures/recurring_gift.json +26 -0
- data/spec/support/fixtures_helper.rb +5 -0
- data/spec/support/virtuous_mock.rb +101 -0
- data/spec/virtuous/client_spec.rb +270 -0
- data/spec/virtuous/error_spec.rb +74 -0
- data/spec/virtuous/resources/contact_address_spec.rb +75 -0
- data/spec/virtuous/resources/contact_spec.rb +137 -0
- data/spec/virtuous/resources/gift_designation_spec.rb +70 -0
- data/spec/virtuous/resources/gift_spec.rb +249 -0
- data/spec/virtuous/resources/individual_spec.rb +95 -0
- data/spec/virtuous/resources/recurring_gift_spec.rb +67 -0
- data/spec/virtuous_spec.rb +7 -0
- data/virtuous.gemspec +25 -0
- metadata +121 -0
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
Dir[File.expand_path('client/*.rb', __dir__)].sort.each { |f| require f }
|
4
|
+
|
5
|
+
module Virtuous
|
6
|
+
##
|
7
|
+
# An API client for Virtuous.
|
8
|
+
# See {initialize} for a full list of supported configuration options.
|
9
|
+
#
|
10
|
+
# ### Authentication
|
11
|
+
#
|
12
|
+
# #### Api key auth
|
13
|
+
#
|
14
|
+
# To generate an api key you need to visit the
|
15
|
+
# [virtuous connect dashboard](https://connect.virtuoussoftware.com/).
|
16
|
+
# Then you can use the key by setting the `api_key` param while creating the client or
|
17
|
+
# by setting the `VIRTUOUS_KEY` environment variable beforehand.
|
18
|
+
#
|
19
|
+
# client = Virtuous::Client.new(
|
20
|
+
# api_key: api_key,
|
21
|
+
# # ...
|
22
|
+
# )
|
23
|
+
#
|
24
|
+
# #### Oauth
|
25
|
+
#
|
26
|
+
# First, an access token needs to be fetched by providing a user's email and password.
|
27
|
+
# This will return an access token that lasts for 15 days, and a refresh token that should be
|
28
|
+
# stored and used to create clients in the future.
|
29
|
+
# The client will use the expiry date of the access token to automatically determine when a
|
30
|
+
# new one needs to be fetched.
|
31
|
+
#
|
32
|
+
# client = Virtuous::Client.new
|
33
|
+
# client.authenticate(email, password)
|
34
|
+
# user.update(
|
35
|
+
# access_token: client.access_token, refresh_token: client.refresh_token,
|
36
|
+
# token_expiration: client.expires_at
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# # Afterwards
|
40
|
+
#
|
41
|
+
# client = Virtuous::Client.new(
|
42
|
+
# access_token: user.access_token, refresh_token: user.refresh_token,
|
43
|
+
# expires_at: user.token_expiration
|
44
|
+
# )
|
45
|
+
#
|
46
|
+
# # Use client
|
47
|
+
#
|
48
|
+
# if client.refreshed
|
49
|
+
# # Update values if they changed
|
50
|
+
# user.update(
|
51
|
+
# access_token: client.access_token, refresh_token: client.refresh_token,
|
52
|
+
# token_expiration: client.expires_at
|
53
|
+
# )
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# #### Two-Factor Authentication
|
57
|
+
#
|
58
|
+
# client = Virtuous::Client.new
|
59
|
+
# response = client.authenticate(email, password)
|
60
|
+
# if response[:requires_otp]
|
61
|
+
# # Prompt user for OTP
|
62
|
+
# client.authenticate(email, password, otp)
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# Check resource modules to see available client methods:
|
66
|
+
#
|
67
|
+
# - {Contact}
|
68
|
+
# - {Individual}
|
69
|
+
# - {Gift}
|
70
|
+
# - {GiftDesignation}
|
71
|
+
class Client
|
72
|
+
include Contact
|
73
|
+
include ContactAddress
|
74
|
+
include Individual
|
75
|
+
include Gift
|
76
|
+
include RecurringGift
|
77
|
+
include GiftDesignation
|
78
|
+
|
79
|
+
##
|
80
|
+
# Access token used for OAuth authentication.
|
81
|
+
attr_reader :access_token
|
82
|
+
##
|
83
|
+
# Token used to refresh the access token when it has expired.
|
84
|
+
attr_reader :refresh_token
|
85
|
+
##
|
86
|
+
# Expiration date of the access token.
|
87
|
+
attr_reader :expires_at
|
88
|
+
##
|
89
|
+
# True if the access token has been refreshed.
|
90
|
+
attr_reader :refreshed
|
91
|
+
|
92
|
+
##
|
93
|
+
# @option config [String] :base_url The base url to use for API calls.
|
94
|
+
# Default: `https://api.virtuoussoftware.com`.
|
95
|
+
# @option config [String] :api_key The key for the API.
|
96
|
+
# @option config [String] :access_token The OAuth access token.
|
97
|
+
# @option config [String] :refresh_token The OAuth refresh token.
|
98
|
+
# @option config [Time] :expires_at The expiration date of the access token.
|
99
|
+
# @option config [Hash] :ssl_options SSL options to use with Faraday.
|
100
|
+
# @option config [Logger] :logger Logger object for Faraday.
|
101
|
+
# @option config [Symbol] :adapter Faraday adapter to use. Default: `Faraday.default_adapter`.
|
102
|
+
def initialize(**config)
|
103
|
+
read_config(config)
|
104
|
+
|
105
|
+
@refreshed = false
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# @!method get(path, body = {})
|
110
|
+
# Makes a `GET` request to the path.
|
111
|
+
#
|
112
|
+
# @param path [String] The path to send the request.
|
113
|
+
# @param body [Hash] Hash of URI query unencoded key/value pairs.
|
114
|
+
#
|
115
|
+
# @return [Object] The body of the response.
|
116
|
+
|
117
|
+
##
|
118
|
+
# @!method post(path, body = {})
|
119
|
+
# Makes a `POST` request to the path.
|
120
|
+
#
|
121
|
+
# @param path [String] The path to send the request.
|
122
|
+
# @param body [Hash] Hash of URI query unencoded key/value pairs.
|
123
|
+
#
|
124
|
+
# @return [Object] The body of the response.
|
125
|
+
|
126
|
+
##
|
127
|
+
# @!method delete(path, body = {})
|
128
|
+
# Makes a `DELETE` request to the path.
|
129
|
+
#
|
130
|
+
# @param path [String] The path to send the request.
|
131
|
+
# @param body [Hash] Hash of URI query unencoded key/value pairs.
|
132
|
+
#
|
133
|
+
# @return [Object] The body of the response.
|
134
|
+
|
135
|
+
##
|
136
|
+
# @!method patch(path, body = {})
|
137
|
+
# Makes a `PATCH` request to the path.
|
138
|
+
#
|
139
|
+
# @param path [String] The path to send the request.
|
140
|
+
# @param body [Hash] Hash of URI query unencoded key/value pairs.
|
141
|
+
#
|
142
|
+
# @return [Object] The body of the response.
|
143
|
+
|
144
|
+
##
|
145
|
+
# @!method put(path, body = {})
|
146
|
+
# Makes a `PUT` request to the path.
|
147
|
+
#
|
148
|
+
# @param path [String] The path to send the request.
|
149
|
+
# @param body [Hash] Hash of URI query unencoded key/value pairs.
|
150
|
+
#
|
151
|
+
# @return [Object] The body of the response.
|
152
|
+
[:get, :post, :delete, :patch, :put].each do |http_method|
|
153
|
+
define_method(http_method) do |path, body = {}|
|
154
|
+
connection.public_send(http_method, path, body).body
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Send a request to get an access token using the email and password of a user.
|
160
|
+
#
|
161
|
+
# @param email [String] The email of the user.
|
162
|
+
# @param password [String] The password of the user.
|
163
|
+
# @param otp [Integer] The One Time Password of the two-factor authentication flow.
|
164
|
+
#
|
165
|
+
# @return [Hash] If the authentication was a success, it contains the OAuth tokens.
|
166
|
+
# If two factor is required, it will include the `:requires_otp` key.
|
167
|
+
# @example Output if authentication is a success
|
168
|
+
# {
|
169
|
+
# access_token: '<access_token>',
|
170
|
+
# refresh_token: '<refresh_token>',
|
171
|
+
# expires_at: Time
|
172
|
+
# }
|
173
|
+
#
|
174
|
+
# @example Output if Two-Factor Authentication is required
|
175
|
+
# {
|
176
|
+
# requires_otp: true
|
177
|
+
# }
|
178
|
+
def authenticate(email, password, otp = nil)
|
179
|
+
data = { grant_type: 'password', username: email, password: password }
|
180
|
+
data[:otp] = otp unless otp.nil?
|
181
|
+
get_access_token(data)
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def encode(string)
|
187
|
+
# CGI.escape changes spaces to '+', and we need '%20' instead.
|
188
|
+
CGI.escape(string).gsub('+', '%20')
|
189
|
+
end
|
190
|
+
|
191
|
+
def read_config(config)
|
192
|
+
[
|
193
|
+
:base_url, :api_key, :access_token, :refresh_token, :expires_at, :ssl_options, :logger,
|
194
|
+
:adapter
|
195
|
+
].each do |attribute|
|
196
|
+
instance_variable_set("@#{attribute}", config[attribute])
|
197
|
+
end
|
198
|
+
|
199
|
+
@api_key ||= ENV.fetch('VIRTUOUS_KEY', nil)
|
200
|
+
@base_url ||= 'https://api.virtuoussoftware.com'
|
201
|
+
@adapter ||= Faraday.default_adapter
|
202
|
+
end
|
203
|
+
|
204
|
+
def use_access_token?
|
205
|
+
!@access_token.nil? || !@refresh_token.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_access_token(body)
|
209
|
+
response = unauthorized_connection.post('/Token', URI.encode_www_form(body))
|
210
|
+
|
211
|
+
return { requires_otp: true } if response.env.status == 202
|
212
|
+
|
213
|
+
self.oauth_tokens = response.body
|
214
|
+
|
215
|
+
{ access_token: @access_token, refresh_token: @refresh_token, expires_at: @expires_at }
|
216
|
+
end
|
217
|
+
|
218
|
+
def oauth_tokens=(values)
|
219
|
+
@access_token = values['access_token']
|
220
|
+
@expires_at = Time.parse(values['.expires'])
|
221
|
+
@refresh_token = values['refresh_token']
|
222
|
+
@refreshed = true
|
223
|
+
end
|
224
|
+
|
225
|
+
def check_token_expiration
|
226
|
+
if !@refresh_token.nil? &&
|
227
|
+
((!@expires_at.nil? && @expires_at < Time.now) || @access_token.nil?)
|
228
|
+
get_access_token({ grant_type: 'refresh_token', refresh_token: @refresh_token })
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def parse(data)
|
233
|
+
data = JSON.parse(data) if data.is_a?(String)
|
234
|
+
|
235
|
+
HashHelper.deep_transform_keys(data) { |key| StringHelper.underscore(key).to_sym }
|
236
|
+
end
|
237
|
+
|
238
|
+
def format(data)
|
239
|
+
HashHelper.deep_transform_keys(data) { |key| StringHelper.camelize(key.to_s) }
|
240
|
+
end
|
241
|
+
|
242
|
+
def bearer_token
|
243
|
+
if @api_key.nil?
|
244
|
+
check_token_expiration
|
245
|
+
return @access_token
|
246
|
+
end
|
247
|
+
|
248
|
+
@api_key
|
249
|
+
end
|
250
|
+
|
251
|
+
def connection
|
252
|
+
unauthorized_connection do |conn|
|
253
|
+
conn.request :authorization, 'Bearer', -> { bearer_token }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def unauthorized_connection
|
258
|
+
options = { url: @base_url }
|
259
|
+
|
260
|
+
options[:ssl] = @ssl_options unless @ssl_options.nil?
|
261
|
+
|
262
|
+
Faraday.new(options) do |conn|
|
263
|
+
conn.request :json
|
264
|
+
conn.response :oj
|
265
|
+
conn.response :logger, @logger if @logger
|
266
|
+
conn.use FaradayMiddleware::VirtuousErrorHandler
|
267
|
+
conn.adapter @adapter
|
268
|
+
yield(conn) if block_given?
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Virtuous # :nodoc: all
|
2
|
+
class Error < StandardError; end
|
3
|
+
class BadGateway < Error; end
|
4
|
+
class BadRequest < Error; end
|
5
|
+
class CloudflareError < Error; end
|
6
|
+
class Forbidden < Error; end
|
7
|
+
class GatewayTimeout < Error; end
|
8
|
+
class InternalServerError < Error; end
|
9
|
+
class NotFound < Error; end
|
10
|
+
class ServiceUnavailable < Error; end
|
11
|
+
class Unauthorized < Error; end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'faraday'
|
15
|
+
module FaradayMiddleware
|
16
|
+
class VirtuousErrorHandler < Faraday::Middleware
|
17
|
+
ERROR_STATUSES = (400..600).freeze
|
18
|
+
|
19
|
+
##
|
20
|
+
# Throws an exception for responses with an HTTP error code.
|
21
|
+
def on_complete(env)
|
22
|
+
message = error_message(env)
|
23
|
+
|
24
|
+
case env[:status]
|
25
|
+
when 400
|
26
|
+
raise Virtuous::BadRequest, message
|
27
|
+
when 401
|
28
|
+
raise Virtuous::Unauthorized, message
|
29
|
+
when 403
|
30
|
+
raise Virtuous::Forbidden, message
|
31
|
+
when 404
|
32
|
+
raise Virtuous::NotFound, message
|
33
|
+
when 500
|
34
|
+
raise Virtuous::InternalServerError, message
|
35
|
+
when 502
|
36
|
+
raise Virtuous::BadGateway, message
|
37
|
+
when 503
|
38
|
+
raise Virtuous::ServiceUnavailable, message
|
39
|
+
when 504
|
40
|
+
raise Virtuous::GatewayTimeout, message
|
41
|
+
when 520
|
42
|
+
raise Virtuous::CloudflareError, message
|
43
|
+
when ERROR_STATUSES
|
44
|
+
raise Virtuous::Error, message
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def error_message(env)
|
51
|
+
"#{env[:status]}: #{env[:url]} #{env[:body]}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Virtuous
|
2
|
+
##
|
3
|
+
# Helper methods for hashes
|
4
|
+
class HashHelper
|
5
|
+
##
|
6
|
+
# Recieves a block which is called on all of the keys of the hash,
|
7
|
+
# including the keys nested hashes inside.
|
8
|
+
# @param hash [Hash] the hash to call the block upon.
|
9
|
+
# @return [Hash] A new hash with the transformed keys.
|
10
|
+
def self.deep_transform_keys(hash, &transform)
|
11
|
+
new_hash = {}
|
12
|
+
hash.each_key do |key|
|
13
|
+
value = hash[key]
|
14
|
+
new_key = transform.call(key)
|
15
|
+
new_hash[new_key] = if value.is_a?(Array)
|
16
|
+
value.map do |item|
|
17
|
+
item.is_a?(Hash) ? deep_transform_keys(item, &transform) : item
|
18
|
+
end
|
19
|
+
elsif value.is_a?(Hash)
|
20
|
+
deep_transform_keys(value, &transform)
|
21
|
+
else
|
22
|
+
value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
new_hash
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Virtuous
|
2
|
+
##
|
3
|
+
# Helper methods for strings
|
4
|
+
class StringHelper
|
5
|
+
class << self
|
6
|
+
##
|
7
|
+
# Transform a String from `CamelCase` to `snake_case`.
|
8
|
+
# @param string [String] the string to transform.
|
9
|
+
# @return [String] the `snake_case` string.
|
10
|
+
def underscore(string)
|
11
|
+
string.gsub('::', '/')
|
12
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
13
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
14
|
+
.tr('-', '_')
|
15
|
+
.downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Transform a String from `snake_case` to `CamelCase`.
|
20
|
+
# @param string [String] the string to transform.
|
21
|
+
# @return [String] the `CamelCase` string.
|
22
|
+
def camelize(string)
|
23
|
+
string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/, &:downcase)
|
24
|
+
|
25
|
+
string.gsub(%r{(?:_|(/))([a-z\d]*)}) do
|
26
|
+
"#{::Regexp.last_match(1)}#{::Regexp.last_match(2).capitalize}"
|
27
|
+
end.gsub('/', '::')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'oj'
|
2
|
+
|
3
|
+
module FaradayMiddleware
|
4
|
+
class ParseOj < Faraday::Middleware
|
5
|
+
##
|
6
|
+
# Parses JSON responses.
|
7
|
+
def on_complete(env)
|
8
|
+
body = env[:body]
|
9
|
+
env[:body] = if empty_body?(body.strip)
|
10
|
+
nil
|
11
|
+
else
|
12
|
+
Oj.load(body, mode: :compat)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def empty_body?(body)
|
19
|
+
body.empty? && body == ''
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Faraday::Response.register_middleware(oj: FaradayMiddleware::ParseOj)
|
data/lib/virtuous.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative 'virtuous/version'
|
2
|
+
require_relative 'virtuous/error'
|
3
|
+
require_relative 'virtuous/parse_oj'
|
4
|
+
Dir[File.expand_path('virtuous/helpers/*.rb', __dir__)].sort.each { |f| require f }
|
5
|
+
require_relative 'virtuous/client'
|
6
|
+
|
7
|
+
##
|
8
|
+
# Virtuous module documentation.
|
9
|
+
#
|
10
|
+
# Go to Client to see the documentation of the client.
|
11
|
+
module Virtuous
|
12
|
+
end
|
data/logo/virtuous.svg
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372.4 66.52"><defs><style>.cls-1{fill:#00a3e0;}.cls-2{fill:#f0f8ff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Logo_Variations" data-name="Logo Variations"><path class="cls-1" d="M27.76,45.66V66.52C3,45.5,0,53.55,0,27.6V6.71C24.79,27.75,27.76,19.69,27.76,45.66Z"></path><path class="cls-1" d="M59.82,6.71V27.6c0,25.95-3,17.9-27.77,38.92V45.66C32.05,19.69,35,27.75,59.82,6.71Z"></path><path class="cls-2" d="M132.58,16,116.29,56.86h-9.91L90.06,16h9.28a4,4,0,0,1,2.32.68A3.52,3.52,0,0,1,103,18.48l7.52,20.68c.37.92.66,1.84.93,2.76a24.29,24.29,0,0,1,.92-2.81l7.7-20.7a3.63,3.63,0,0,1,1.31-1.73,3.86,3.86,0,0,1,2.26-.64Z"></path><path class="cls-2" d="M148.83,16V53.35a3.51,3.51,0,0,1-3.5,3.47h-8V19.48a3.5,3.5,0,0,1,3.5-3.49Z"></path><path class="cls-2" d="M148.83,3.48V10h-8a3.49,3.49,0,0,1-3.5-3.48V0h8A3.51,3.51,0,0,1,148.83,3.48Z"></path><path class="cls-2" d="M371.48,39.05a10.08,10.08,0,0,0-2.4-3.17,13.27,13.27,0,0,0-3.32-2.14c-1.17-.52-2.38-1-3.59-1.41s-2.38-.78-3.53-1.13a20,20,0,0,1-2.91-1.09,6.21,6.21,0,0,1-1.84-1.28,1.8,1.8,0,0,1-.53-1.38,2.59,2.59,0,0,1,1.17-2.23,6.23,6.23,0,0,1,3.75-1,10.31,10.31,0,0,1,2.77.34,16.81,16.81,0,0,1,2.19.79l1.79.85a3.76,3.76,0,0,0,3.51.09,3.42,3.42,0,0,0,1.14-1.19l2.6-4-.6-.59a17.69,17.69,0,0,0-5.85-3.67,22.26,22.26,0,0,0-14.43-.38,14.9,14.9,0,0,0-4.93,2.76,11.66,11.66,0,0,0-3.05,4.11,12.24,12.24,0,0,0-1,4.95,11.64,11.64,0,0,0,.93,4.8,10.26,10.26,0,0,0,2.4,3.39,13,13,0,0,0,3.34,2.24,32.52,32.52,0,0,0,3.69,1.46c1.23.41,2.42.78,3.57,1.13a20.08,20.08,0,0,1,2.92,1.09,6.29,6.29,0,0,1,1.85,1.29,2.12,2.12,0,0,1,.53,1.52,3.34,3.34,0,0,1-.27,1.36,2.87,2.87,0,0,1-.85,1.1,5.13,5.13,0,0,1-1.64.85,8.17,8.17,0,0,1-2.55.34,9.4,9.4,0,0,1-3.16-.43,15.06,15.06,0,0,1-2.25-1c-.67-.37-1.25-.71-1.75-1a4.19,4.19,0,0,0-4.19-.16,4.06,4.06,0,0,0-1.34,1.33L341,51.85l.64.57a17,17,0,0,0,2.76,2A21.58,21.58,0,0,0,347.84,56,23.21,23.21,0,0,0,351.68,57a23.84,23.84,0,0,0,11.07-.64A15.11,15.11,0,0,0,368,53.5a12.32,12.32,0,0,0,3.3-4.43,13.94,13.94,0,0,0,1.11-5.56A10,10,0,0,0,371.48,39.05Z"></path><path class="cls-2" d="M236.88,19.47V38.74c0,3.35-.65,6.43-1.66,7.8a5.57,5.57,0,0,1-4.82,2.13,5.64,5.64,0,0,1-4.9-2.14c-1-1.44-1.66-4.46-1.66-7.79l0-19.27A3.49,3.49,0,0,0,220.3,16h-8V39.81a21.12,21.12,0,0,0,1.24,6.58,16.85,16.85,0,0,0,3.69,5.91A16,16,0,0,0,223,56.08a21.52,21.52,0,0,0,14.76,0,16,16,0,0,0,5.66-3.78,16.81,16.81,0,0,0,3.64-5.91,21.65,21.65,0,0,0,1.24-6.57V16h-8A3.48,3.48,0,0,0,236.88,19.47Z"></path><path class="cls-2" d="M155.31,30.28V56.82h8a3.49,3.49,0,0,0,3.49-3.48V34.62c0-3.35.65-6.42,1.64-7.8a5.62,5.62,0,0,1,4.83-2.13h1.4a3.5,3.5,0,0,0,3.5-3.47V16h-8.54a16.05,16.05,0,0,0-5.82,1,12.78,12.78,0,0,0-4.61,3,13.51,13.51,0,0,0-2.87,4.73,17.07,17.07,0,0,0-1,5,2.94,2.94,0,0,0,0,.42"></path><path class="cls-2" d="M268,27.44c1.37-1.92,3.44-2.85,6.35-2.85s5.06.92,6.45,2.87,2.16,5,2.16,8.95-.73,6.91-2.16,8.9-3.48,2.84-6.45,2.84-5-.92-6.36-2.82-2.14-5-2.14-8.92S266.62,29.45,268,27.44Zm-12.42.26a24.86,24.86,0,0,0-1.43,8.63A24.86,24.86,0,0,0,255.57,45a19,19,0,0,0,4.12,6.7,17.91,17.91,0,0,0,6.46,4.25,24.17,24.17,0,0,0,16.6,0,18,18,0,0,0,6.46-4.25A19.12,19.12,0,0,0,293.3,45a24.64,24.64,0,0,0,1.44-8.63,24.48,24.48,0,0,0-1.45-8.64,18.79,18.79,0,0,0-4.13-6.58,18.39,18.39,0,0,0-6.46-4.21,24.28,24.28,0,0,0-16.6,0,18.06,18.06,0,0,0-10.53,10.8Z"></path><path class="cls-2" d="M312,19.47V38.74c0,3.35.65,6.43,1.65,7.8a6.61,6.61,0,0,0,9.73,0c1-1.44,1.66-4.46,1.66-7.79l0-19.27a3.49,3.49,0,0,1,3.5-3.47h8V39.81a21.11,21.11,0,0,1-1.23,6.58,16.85,16.85,0,0,1-3.69,5.91,15.91,15.91,0,0,1-5.74,3.78,21.49,21.49,0,0,1-14.75,0,16,16,0,0,1-5.67-3.78,16.81,16.81,0,0,1-3.64-5.91,21.06,21.06,0,0,1-1.24-6.57V16h8A3.48,3.48,0,0,1,312,19.47Z"></path><path class="cls-2" d="M195.22,24.7h7.46A2.75,2.75,0,0,0,205.44,22V16H195.22V8.87a2.77,2.77,0,0,0-2.77-2.76h-8.66V45.66c0,3.61,1,6.5,3,8.59s4.89,3.17,8.53,3.17a18.6,18.6,0,0,0,5.83-.91,15.17,15.17,0,0,0,5.05-2.76l.69-.56-3.28-5.3,0-.07a4.23,4.23,0,0,0-.76-.82,2.26,2.26,0,0,0-2.56-.07c-.21.13-.45.25-.72.39a8.1,8.1,0,0,1-.92.38,3.2,3.2,0,0,1-1.11.14,2,2,0,0,1-1.66-.67,3.11,3.11,0,0,1-.64-2.14Z"></path></g></g></svg>
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(__FILE__, '..', '..', 'lib'))
|
2
|
+
|
3
|
+
SPEC_DIR = File.dirname(__FILE__)
|
4
|
+
FIXTURES_DIR = File.join(SPEC_DIR, 'support', 'fixtures')
|
5
|
+
|
6
|
+
require 'virtuous'
|
7
|
+
require 'webmock/rspec'
|
8
|
+
require_relative 'support/fixtures_helper'
|
9
|
+
require_relative 'support/virtuous_mock'
|
10
|
+
require_relative 'support/client_factory'
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.before :suite do
|
14
|
+
WebMock.disable_net_connect!
|
15
|
+
end
|
16
|
+
|
17
|
+
config.before :each do
|
18
|
+
stub_request(:any, /virtuoussoftware/).to_rack(VirtuousMock)
|
19
|
+
end
|
20
|
+
|
21
|
+
config.run_all_when_everything_filtered = true
|
22
|
+
config.filter_run :focus
|
23
|
+
|
24
|
+
config.order = 'random'
|
25
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
{
|
2
|
+
"id": 1,
|
3
|
+
"contactType": "Organization",
|
4
|
+
"isPrivate": true,
|
5
|
+
"name": "Test Contact",
|
6
|
+
"informalName": "Test Contact",
|
7
|
+
"formalContactName": "Test Contact",
|
8
|
+
"alternateContactName": "Test Contact",
|
9
|
+
"preferredSalutationName": "Test Contact",
|
10
|
+
"preferredAddresseeName": "Test Contact",
|
11
|
+
"description": null,
|
12
|
+
"website": null,
|
13
|
+
"maritalStatus": null,
|
14
|
+
"anniversaryMonth": null,
|
15
|
+
"anniversaryDay": null,
|
16
|
+
"anniversaryYear": null,
|
17
|
+
"mergedIntoContactId": null,
|
18
|
+
"address": {
|
19
|
+
"id": 800,
|
20
|
+
"label": "Test address",
|
21
|
+
"address1": "123 test street",
|
22
|
+
"address2": null,
|
23
|
+
"city": null,
|
24
|
+
"state": "",
|
25
|
+
"postal": null,
|
26
|
+
"country": "",
|
27
|
+
"isPrimary": true,
|
28
|
+
"canBePrimary": false,
|
29
|
+
"startMonth": 1,
|
30
|
+
"startDay": 1,
|
31
|
+
"endMonth": 12,
|
32
|
+
"endDay": 31,
|
33
|
+
"createDateTimeUtc": "2023-11-30T14:20:56.34",
|
34
|
+
"createdByUser": "Test User",
|
35
|
+
"modifiedDateTimeUtc": "2023-11-30T14:20:56.3533333",
|
36
|
+
"modifiedByUser": "Test User"
|
37
|
+
},
|
38
|
+
"giftAskAmount": "$1,200",
|
39
|
+
"giftAskType": null,
|
40
|
+
"lifeToDateGiving": "$0",
|
41
|
+
"yearToDateGiving": "$0",
|
42
|
+
"lastGiftAmount": "$0",
|
43
|
+
"lastGiftDate": "Unavailable",
|
44
|
+
"contactIndividuals": [
|
45
|
+
{
|
46
|
+
"id": 2,
|
47
|
+
"contactId": 1,
|
48
|
+
"prefix": "Mr",
|
49
|
+
"prefix2": null,
|
50
|
+
"firstName": "Test",
|
51
|
+
"middleName": null,
|
52
|
+
"lastName": "Individual",
|
53
|
+
"preMarriageName": null,
|
54
|
+
"suffix": null,
|
55
|
+
"nickname": null,
|
56
|
+
"gender": "Male",
|
57
|
+
"isPrimary": true,
|
58
|
+
"canBePrimary": false,
|
59
|
+
"isSecondary": false,
|
60
|
+
"canBeSecondary": false,
|
61
|
+
"birthMonth": 9,
|
62
|
+
"birthDay": 4,
|
63
|
+
"birthYear": 1998,
|
64
|
+
"birthDate": "9/4/1998",
|
65
|
+
"approximateAge": 25,
|
66
|
+
"isDeceased": false,
|
67
|
+
"deceasedDate": "",
|
68
|
+
"passion": null,
|
69
|
+
"avatarUrl": null,
|
70
|
+
"contactMethods": [
|
71
|
+
{
|
72
|
+
"id": 2081,
|
73
|
+
"type": "Work Email",
|
74
|
+
"value": "email@test.com",
|
75
|
+
"isOptedIn": false,
|
76
|
+
"isPrimary": true,
|
77
|
+
"canBePrimary": false,
|
78
|
+
"createDateTimeUtc": "2023-11-30T14:20:56.3233333",
|
79
|
+
"createdByUser": "Test User",
|
80
|
+
"modifiedDateTimeUtc": "2023-11-30T14:20:56.37",
|
81
|
+
"modifiedByUser": "Test User"
|
82
|
+
}
|
83
|
+
],
|
84
|
+
"createDateTimeUtc": "2023-11-30T14:20:56.2933333",
|
85
|
+
"createdByUser": "Test User",
|
86
|
+
"modifiedDateTimeUtc": "2023-11-30T14:20:56.3533333",
|
87
|
+
"modifiedByUser": "Test User",
|
88
|
+
"customFields": [],
|
89
|
+
"customCollections": []
|
90
|
+
}
|
91
|
+
],
|
92
|
+
"contactGiftsUrl": "/api/Gift/ByContact/1",
|
93
|
+
"contactPassthroughGiftsUrl": "/api/Gift/Passthrough/ByContact/1",
|
94
|
+
"contactPlannedGiftsUrl": "/api/PlannedGift/ByContact/1",
|
95
|
+
"contactRecurringGiftsUrl": "/api/RecurringGift/ByContact/1",
|
96
|
+
"contactImportantNotesUrl": "/api/ContactNote/Important/ByContact/1",
|
97
|
+
"contactNotesUrl": "/api/ContactNote/ByContact/1",
|
98
|
+
"contactTagsUrl": "/api/ContactTag/ByContact/1",
|
99
|
+
"contactRelationshipsUrl": "/api/Relationship/ByContact/1",
|
100
|
+
"primaryAvatarUrl": null,
|
101
|
+
"contactReferences": [],
|
102
|
+
"originSegmentId": null,
|
103
|
+
"originSegment": null,
|
104
|
+
"createDateTimeUtc": "2023-11-30T14:20:56.277",
|
105
|
+
"createdByUser": "Test User",
|
106
|
+
"modifiedDateTimeUtc": "2023-11-30T14:20:56.353",
|
107
|
+
"modifiedByUser": "Test User",
|
108
|
+
"tags": [],
|
109
|
+
"organizationGroups": [],
|
110
|
+
"customFields": [],
|
111
|
+
"customCollections": []
|
112
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"id": 1,
|
3
|
+
"label": "Home address",
|
4
|
+
"address1": "324 Frank Island",
|
5
|
+
"address2": "Apt. 366",
|
6
|
+
"city": "Antonioborough",
|
7
|
+
"state": "MA",
|
8
|
+
"postal": "27516",
|
9
|
+
"country": "United States",
|
10
|
+
"is_primary": true,
|
11
|
+
"can_be_primary": false,
|
12
|
+
"start_month": 1,
|
13
|
+
"start_day": 1,
|
14
|
+
"end_month": 12,
|
15
|
+
"end_day": 31,
|
16
|
+
"create_date_time_utc": "2023-11-30T14:20:56.34",
|
17
|
+
"created_by_user": "Test User",
|
18
|
+
"modified_date_time_utc": "2023-12-27T15:33:09.9",
|
19
|
+
"modified_by_user": "Test User"
|
20
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"id": 1,
|
4
|
+
"label": "Home address",
|
5
|
+
"address1": "324 Frank Island",
|
6
|
+
"address2": "Apt. 366",
|
7
|
+
"city": "Antonioborough",
|
8
|
+
"state": "MA",
|
9
|
+
"postal": "27516",
|
10
|
+
"country": "United States",
|
11
|
+
"is_primary": true,
|
12
|
+
"can_be_primary": false,
|
13
|
+
"start_month": 1,
|
14
|
+
"start_day": 1,
|
15
|
+
"end_month": 12,
|
16
|
+
"end_day": 31,
|
17
|
+
"create_date_time_utc": "2023-11-30T14:20:56.34",
|
18
|
+
"created_by_user": "Test User",
|
19
|
+
"modified_date_time_utc": "2023-12-27T15:33:09.9",
|
20
|
+
"modified_by_user": "Test User"
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"id": 2,
|
24
|
+
"label": "Home address",
|
25
|
+
"address1": "324 Frank Island",
|
26
|
+
"address2": "Apt. 366",
|
27
|
+
"city": "Antonioborough",
|
28
|
+
"state": "MA",
|
29
|
+
"postal": "27516",
|
30
|
+
"country": "United States",
|
31
|
+
"is_primary": false,
|
32
|
+
"can_be_primary": true,
|
33
|
+
"start_month": 1,
|
34
|
+
"start_day": 1,
|
35
|
+
"end_month": 12,
|
36
|
+
"end_day": 31,
|
37
|
+
"create_date_time_utc": "2023-12-27T15:35:02.447",
|
38
|
+
"created_by_user": "Test User",
|
39
|
+
"modified_date_time_utc": "2023-12-27T15:35:02.967",
|
40
|
+
"modified_by_user": "Test User"
|
41
|
+
}
|
42
|
+
]
|