virtuous 0.0.1
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 +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
|
+
]
|