docusign_rest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rvmrc
19
+ test.rb
20
+ test/fixtures/vcr/*
21
+ test/docusign_login_config.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in docusign_rest.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jon Kinney
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,193 @@
1
+ # DocusignRest
2
+
3
+ This 'wrapper gem' hooks a Ruby app (currently only tested with Rails) up to the DocuSign REST API to allow for embedded signing.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'docusign_rest'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install docusign_rest
18
+
19
+ ## Configuration
20
+
21
+ There is a bundled rake task that will prompt you for your DocuSign credentials including:
22
+
23
+ * Username
24
+ * Password
25
+ * Integrator Key
26
+
27
+ and create the `config/initializers/docusign\_rest.rb` file in your Rails app for you. If the file was unable to be created, the rake task will output the config block for you to manually add to an initializer.
28
+
29
+ **Note** please run the below task and ensure your initializer is in place before attempting to use any of the methods in this gem. Without the initializer this gem will not be able to properly authenticate you to the DocuSign REST API.
30
+
31
+ $ bundle exec rake docusign_rest:generate_config
32
+
33
+ outputs:
34
+
35
+ Please do the following:
36
+ ------------------------
37
+ 1) Login or register for an account at demo.docusign.net
38
+ ...or their production url if applicable
39
+ 2) Click 'Preferences' in the upper right corner of the page
40
+ 3) Click 'API' in far lower left corner of the menu
41
+ 4) Request a new 'Integrator Key' via the web interface
42
+ * You will use this key in one of the next steps to retrieve your 'accountId'
43
+
44
+ Please enter your DocuSign username: someone@gmail.com
45
+ Please enter your DocuSign password: p@ssw0rd1
46
+ Please enter your DocuSign integrator_key: KEYS-19ddd1cc-cb56-4ca6-87ec-38db47d14b32
47
+
48
+ The following block of code was added to config/initializers/docusign_rest.rb
49
+
50
+ require 'docusign_rest'
51
+
52
+ DocusignRest.configure do |config|
53
+ config.username = 'someone@gmail.com'
54
+ config.password = 'p@ssw0rd1'
55
+ config.integrator_key = 'KEYS-19ddd1cc-cb56-4ca6-87ec-38db47d14b32'
56
+ config.account_id = '123456'
57
+ end
58
+
59
+ ## Usage
60
+
61
+ The docusign\_rest gem makes creating multipart POST (aka file upload) requests to the DocuSign REST API dead simple. It's built on top of Net:HTTP and utilizes the [multipart-post](https://github.com/nicksieger/multipart-post) gem to assist with formatting the multipart requests for the DocuSign REST API. The DocuSign REST API requires that all files be embedded as JSON directly in the request body (not the body\_stream like multipart-post does by default) so the docusign\_rest gem takes care of setting that up for you.
62
+
63
+ This gem also monkey patches one small part of multipart-post to inject some header values and formatting that DocuSign requires. If you would like to see the monkey patched code please take a look at [lib/multipart-post/parts.rb](https://github.com/j2fly/docusign_rest/blob/master/lib/multipart_post/parts.rb). It's only re-opening one method, but feel free to make sure you understand that monkey patch if it concerns you.
64
+
65
+ ### Examples
66
+
67
+ * These examples assume you have already run the `docusign_rest:generate_config` rake task and have the configure block properly setup in an initializer with your username, password, integrator\_key, and account\_id.
68
+
69
+ **Getting login information:**
70
+
71
+ ```ruby
72
+ client = DocusignRest::Client.new
73
+ puts client.get_account_id
74
+ ```
75
+
76
+
77
+ **Creating an envelope from a document:**
78
+
79
+ ```ruby
80
+ client = DocusignRest::Client.new
81
+ response = client.create_envelope_from_document(
82
+ email: {
83
+ subject: "test email subject",
84
+ body: "this is the email body and it's large!"
85
+ },
86
+ # If embedded is set to true in the signers array below, emails
87
+ # don't go out and you can embed the signature page in an iFrame
88
+ # by using the get_recipient_view method
89
+ signers: [
90
+ {
91
+ #embedded: true,
92
+ name: 'Test Guy',
93
+ email: 'someone@gmail.com'
94
+ },
95
+ {
96
+ #embedded: true,
97
+ name: 'Test Girl',
98
+ email: 'someone@gmail.com'
99
+ }
100
+ ],
101
+ files: [
102
+ {path: 'test.pdf', name: 'test.pdf'},
103
+ {path: 'test2.pdf', name: 'test2.pdf'}
104
+ ],
105
+ status: 'sent'
106
+ )
107
+ response = JSON.parse(response.body)
108
+ response["status"].must_equal "sent"
109
+ ```
110
+
111
+
112
+ **Creating a template:**
113
+
114
+ ```ruby
115
+ client = DocusignRest::Client.new
116
+ response = client.create_template(
117
+ description: 'Cool Description',
118
+ name: "Cool Template Name",
119
+ signers: [
120
+ {
121
+ embedded: true,
122
+ name: 'jon',
123
+ email: 'someone@gmail.com',
124
+ role_name: 'Issuer',
125
+ anchor_string: 'sign here'
126
+ }
127
+ ],
128
+ files: [
129
+ {path: 'test.pdf', name: 'test.pdf'}
130
+ ]
131
+ )
132
+ @template_response = JSON.parse(response.body)
133
+ ```
134
+
135
+
136
+ **Creating an envelope from a template:**
137
+
138
+ ```ruby
139
+ client = DocusignRest::Client.new
140
+ response = client.create_envelope_from_template(
141
+ status: 'sent',
142
+ email: {
143
+ subject: "The test email subject envelope",
144
+ body: "Envelope body content here"
145
+ },
146
+ template_id: @template_response["templateId"],
147
+ signers: [
148
+ {
149
+ embedded: true,
150
+ name: 'jon',
151
+ email: 'someone@gmail.com',
152
+ role_name: 'Issuer'
153
+ }
154
+ ]
155
+ )
156
+ @envelope_response = JSON.parse(response.body)
157
+ ```
158
+
159
+
160
+ **Retrieving the url for embedded signing**
161
+
162
+ ```ruby
163
+ client = DocusignRest::Client.new
164
+ response = client.get_recipient_view(
165
+ envelope_id: @envelope_response["envelopeId"],
166
+ name: 'jon',
167
+ email: 'someone@gmail.com',
168
+ return_url: 'http://google.com'
169
+ )
170
+ @view_recipient_response = JSON.parse(response.body)
171
+ puts @view_recipient_response["url"]
172
+ ```
173
+
174
+
175
+ ## Contributing
176
+
177
+ 1. Fork it
178
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
179
+ 3. Commit your changes (`git commit -am 'Added some feature'`) making sure to write tests to ensure nothing breaks
180
+ 4. Push to the branch (`git push origin my-new-feature`)
181
+ 5. Create new Pull Request
182
+
183
+ ### Running the tests
184
+
185
+ In order to run the tests you'll need to register for a (free) DocuSign developer account. After doing so you'll have a username, password, and integrator key. Armed with that information execute the following ruby file:
186
+
187
+ $ ruby lib/tasks/docusign_task.rb
188
+
189
+ This calls a rake task which adds a non-version controlled file in the test folder called 'docusign_login_config.rb' which holds your account specific credentials and is required in order to hit the test API through the test suite.
190
+
191
+ **VCR**
192
+
193
+ The test suite uses VCR and is configured to record only the first request by using the 'once' configuration option surrounding each API request. If you want to experiment with the API or are getting several errors with the test suite, you may want to change the VCR config record setting to 'all' temporarily which will write a new YAML file for each request each time you hit the API. However, this significantly slow down tests and essentially negates the benefit of VCR which is to mock out the API entirely and keep the tests speedy.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.ruby_opts << "-rubygems"
8
+ test.pattern = 'test/**/*_test.rb'
9
+ test.verbose = true
10
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/docusign_rest/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jon Kinney"]
6
+ gem.email = ["jonkinney@gmail.com"]
7
+ gem.description = %q{Hooks a Rails app up to the DocuSign service through the DocuSign REST API}
8
+ gem.summary = %q{Use this gem to embed signing of documents in a Rails app through the DocuSign REST API}
9
+ gem.homepage = "https://github.com/j2fly/docusign_rest"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "docusign_rest"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = DocusignRest::VERSION
17
+
18
+ gem.add_dependency('multipart-post', '>= 1.1.5')
19
+ gem.add_dependency('json')
20
+ gem.add_development_dependency('rake')
21
+ gem.add_development_dependency('minitest')
22
+ gem.add_development_dependency('turn')
23
+ gem.add_development_dependency('pry')
24
+ gem.add_development_dependency('vcr')
25
+ gem.add_development_dependency('fakeweb')
26
+ end
@@ -0,0 +1,40 @@
1
+ require 'docusign_rest'
2
+
3
+ DocusignRest.configure do |config|
4
+ config.username = "someone@gmail.com"
5
+ config.password = "password"
6
+ config.integrator_key = "KEYS-16dbc1bc-ca56-4ea6-87ec-29db47d94b32"
7
+ config.account_id = "123456"
8
+ end
9
+
10
+ client = DocusignRest::Client.new
11
+
12
+ response = client.create_envelope_from_document(
13
+ email: {
14
+ subject: "test email subject",
15
+ body: "this is the email body and it's large!"
16
+ },
17
+ # If embedded is set to true in the signers array below, emails
18
+ # don't go out and you can embed the signature page in an iFrame
19
+ # by using the get_recipient_view method
20
+ signers: [
21
+ {
22
+ #embedded: true,
23
+ name: 'Test Guy',
24
+ email: 'someone@gmail.com'
25
+ },
26
+ {
27
+ #embedded: true,
28
+ name: 'Test Girl',
29
+ email: 'someone+else@gmail.com'
30
+ }
31
+ ],
32
+ files: [
33
+ {path: 'test.pdf', name: 'test.pdf'},
34
+ {path: 'test2.pdf', name: 'test2.pdf'}
35
+ ],
36
+ status: 'sent'
37
+ )
38
+
39
+ response = JSON.parse(response.body)
40
+ puts response.body
@@ -0,0 +1,14 @@
1
+ require 'docusign_rest/version'
2
+ require 'docusign_rest/configuration'
3
+ require 'docusign_rest/client'
4
+ require 'multipart_post' #require the multipart-post gem itself
5
+ require 'net/http/post/multipart' #require the multipart-post net/http/post/multipart monkey patch
6
+ require 'multipart_post/parts' #require my monkey patched parts.rb which adjusts the build_part method
7
+ require 'net/http'
8
+ require 'json'
9
+
10
+ module DocusignRest
11
+ require "docusign_rest/railtie" if defined?(Rails)
12
+
13
+ extend Configuration
14
+ end
@@ -0,0 +1,609 @@
1
+ module DocusignRest
2
+
3
+ class Client
4
+ # Define the same set of accessors as the DocusignRest module
5
+ attr_accessor *Configuration::VALID_CONFIG_KEYS
6
+ attr_accessor :docusign_authentication_headers, :acct_id
7
+
8
+ def initialize(options={})
9
+ # Merge the config values from the module and those passed to the client.
10
+ merged_options = DocusignRest.options.merge(options)
11
+
12
+ # Copy the merged values to this client and ignore those not part
13
+ # of our configuration
14
+ Configuration::VALID_CONFIG_KEYS.each do |key|
15
+ send("#{key}=", merged_options[key])
16
+ end
17
+
18
+ # Set up the DocuSign Authentication headers with the values passed from
19
+ # our config block
20
+ @docusign_authentication_headers = {
21
+ "X-DocuSign-Authentication" => "" \
22
+ "<DocuSignCredentials>" \
23
+ "<Username>#{DocusignRest.username}</Username>" \
24
+ "<Password>#{DocusignRest.password}</Password>" \
25
+ "<IntegratorKey>#{DocusignRest.integrator_key}</IntegratorKey>" \
26
+ "</DocuSignCredentials>"
27
+ }
28
+
29
+ # Set the account_id from the configure block if present, but can't call
30
+ # the instance var @account_id because that'll override the attr_accessor
31
+ # that is automatically configured for the configure block
32
+ @acct_id = DocusignRest.account_id
33
+ end
34
+
35
+
36
+ # Internal: sets the default request headers allowing for user overrides
37
+ # via options[:headers] from within other requests. Additionally injects
38
+ # the X-DocuSign-Authentication header to authorize the request.
39
+ #
40
+ # Client can pass in header options to any given request:
41
+ # headers: {"Some-Key" => "some/value", "Another-Key" => "another/value"}
42
+ #
43
+ # Then we pass them on to this method to merge them with the other
44
+ # required headers
45
+ #
46
+ # Example:
47
+ #
48
+ # headers(options[:headers])
49
+ #
50
+ # Returns a merged hash of headers overriding the default Accept header if
51
+ # the user passes in a new "Accept" header key and adds any other
52
+ # user-defined headers along with the X-DocuSign-Authentication headers
53
+ def headers(user_defined_headers={})
54
+ default = {
55
+ "Accept" => "application/json" #this seems to get added automatically, so I can probably remove this
56
+ }
57
+
58
+ default.merge!(user_defined_headers) if user_defined_headers
59
+
60
+ @docusign_authentication_headers.merge(default)
61
+ end
62
+
63
+
64
+ # Internal: builds a URI based on the configurable endpoint, api_version,
65
+ # and the passed in relative url
66
+ #
67
+ # url - a relative url requiring a leading forward slash
68
+ #
69
+ # Example:
70
+ #
71
+ # build_uri("/login_information")
72
+ #
73
+ # Returns a parsed URI object
74
+ def build_uri(url)
75
+ URI.parse("#{DocusignRest.endpoint}/#{DocusignRest.api_version}#{url}")
76
+ end
77
+
78
+
79
+ # Internal: configures Net:HTTP with some default values that are required
80
+ # for every request to the DocuSign API
81
+ #
82
+ # Returns a configured Net::HTTP object into which a request can be passed
83
+ def initialize_net_http_ssl(uri)
84
+ http = Net::HTTP.new(uri.host, uri.port)
85
+ http.use_ssl = true
86
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
87
+ http
88
+ end
89
+
90
+
91
+ # Public: gets info necessary to make additional requests to the DocuSign API
92
+ #
93
+ # options - hash of headers if the client wants to override something
94
+ #
95
+ # Examples:
96
+ #
97
+ # client = DocusignRest::Client.new
98
+ # response = client.login_information
99
+ # puts response.body
100
+ #
101
+ # Returns:
102
+ # accountId - For the username, password, and integrator_key specified
103
+ # baseUrl - The base URL for all future DocuSign requests
104
+ # email - The email used when signing up for DocuSign
105
+ # isDefault - # TODO identify what this is
106
+ # name - The account name provided when signing up for DocuSign
107
+ # userId - # TODO determine what this is used for, if anything
108
+ # userName - Full name provided when signing up for DocuSign
109
+ def get_login_information(options={})
110
+ uri = build_uri("/login_information")
111
+ request = Net::HTTP::Get.new(uri.request_uri, headers(options[:headers]))
112
+ http = initialize_net_http_ssl(uri)
113
+ http.request(request)
114
+ end
115
+
116
+
117
+ # Internal: uses the get_login_information method to determine the client's
118
+ # accountId and then caches that value into an instance variable so we
119
+ # don't end up hitting the API for login_information more than once per
120
+ # request.
121
+ #
122
+ # This is used by the rake task in lib/tasks/docusign_task.rake to add
123
+ # the config/initialzers/docusign_rest.rb file with the proper config block
124
+ # which includes the account_id in it. That way we don't require hitting
125
+ # the /login_information URI in normal requests
126
+ #
127
+ # Returns the accountId string
128
+ def get_account_id
129
+ unless @acct_id
130
+ response = get_login_information.body
131
+ hashed_response = JSON.parse(response)
132
+ login_accounts = hashed_response['loginAccounts']
133
+ @acct_id ||= login_accounts.first['accountId']
134
+ end
135
+
136
+ @acct_id
137
+ end
138
+
139
+ def check_embedded_signer(embedded, email)
140
+ if embedded && embedded == true
141
+ "\"clientUserId\" : \"#{email}\","
142
+ end
143
+ end
144
+
145
+ # Internal: takes in an array of hashes of signers and calculates the
146
+ # recipientId then concatenates all the hashes with commas
147
+ #
148
+ # embedded - Tells DocuSign if this is an embedded signer which determines
149
+ # weather or not to deliver emails. Also lets us authenticate
150
+ # them when they go to do embedded signing. Behind the scenes
151
+ # this is setting the clientUserId value to the signer's email.
152
+ # name - The name of the signer
153
+ # email - The email of the signer
154
+ #
155
+ # Returns a hash of users that need to sign the document
156
+ def get_signers(signers)
157
+ doc_signers = []
158
+ signers.each_with_index do |signer, index|
159
+ doc_signers << "{
160
+ #{check_embedded_signer(signer[:embedded], signer[:email])}
161
+ \"name\" : \"#{signer[:name]}\",
162
+ \"email\" : \"#{signer[:email]}\",
163
+ \"recipientId\" : \"#{index+1}\"
164
+ }"
165
+ end
166
+ doc_signers.join(",")
167
+ end
168
+
169
+
170
+ # Internal: takes in an array of hashes of signers and concatenates all the
171
+ # hashes with commas
172
+ #
173
+ # embedded - Tells DocuSign if this is an embedded signer which determines
174
+ # weather or not to deliver emails. Also lets us authenticate
175
+ # them when they go to do embedded signing. Behind the scenes
176
+ # this is setting the clientUserId value to the signer's email.
177
+ # name - The name of the signer
178
+ # email - The email of the signer
179
+ # role_name - The role name of the signer ('Attorney', 'Client', etc.).
180
+ #
181
+ # Returns a hash of users that need to be embedded in the template to
182
+ # create an envelope
183
+ def get_template_roles(signers)
184
+ template_roles = []
185
+ signers.each_with_index do |signer, index|
186
+ template_roles << "{
187
+ #{check_embedded_signer(signer[:embedded], signer[:email])}
188
+ \"name\" : \"#{signer[:name]}\",
189
+ \"email\" : \"#{signer[:email]}\",
190
+ \"roleName\" : \"#{signer[:role_name]}\"
191
+ }"
192
+ end
193
+ template_roles.join(",")
194
+ end
195
+
196
+
197
+ # Internal: takes an array of hashes of signers required to complete a
198
+ # document and allows for setting several options. Not all options are
199
+ # currently dynamic but that's easy to change/add which I (and I'm
200
+ # sure others) will be doing in the future.
201
+ #
202
+ # embedded - Tells DocuSign if this is an embedded signer which
203
+ # determines weather or not to deliver emails. Also
204
+ # lets us authenticate them when they go to do
205
+ # embedded signing. Behind the scenes this is setting
206
+ # the clientUserId value to the signer's email.
207
+ # email_notification - Send an email or not
208
+ # role_name - The signer's role, like 'Attorney' or 'Client', etc.
209
+ # template_locked - Doesn't seem to work/do anything
210
+ # template_required - Doesn't seem to work/do anything
211
+ # email - The signer's email
212
+ # name - The signer's name
213
+ # anchor_string - The string of text to anchor the 'sign here' tab to
214
+ # document_id - If the doc you want signed isn't the first doc in
215
+ # the files options hash
216
+ # page_number - Page number of the sign here tab
217
+ # x_position - Distance horizontally from the anchor string for the
218
+ # 'sign here' tab to appear. Note: doesn't seem to
219
+ # currently work.
220
+ # y_position - Distance vertically from the anchor string for the
221
+ # 'sign here' tab to appear. Note: doesn't seem to
222
+ # currently work.
223
+ # sign_here_tab_text - Instead of 'sign here'. Note: doesn't work
224
+ # tab_label - TODO: figure out what this is
225
+ def get_template_signers(signers)
226
+ doc_signers = []
227
+ signers.each_with_index do |signer, index|
228
+ doc_signers << "{
229
+ \"accessCode\":\"\",
230
+ \"addAccessCodeToEmail\":false,
231
+ #{check_embedded_signer(signer[:embedded], signer[:email])}
232
+ \"customFields\":null,
233
+ \"emailNotification\":#{signer[:email_notification] || 'null'},
234
+ \"idCheckConfigurationName\":null,
235
+ \"idCheckInformationInput\":null,
236
+ \"inheritEmailNotificationConfiguration\":false,
237
+ \"note\":\"\",
238
+ \"phoneAuthentication\":null,
239
+ \"recipientAttachments\":null,
240
+ \"recipientId\":\"#{index+1}\",
241
+ \"requireIdLookup\":false,
242
+ \"roleName\":\"#{signer[:role_name]}\",
243
+ \"routingOrder\":#{index+1},
244
+ \"socialAuthentications\":null,
245
+ \"templateAccessCodeRequired\":false,
246
+ \"templateLocked\":#{signer[:template_locked] || 'false'},
247
+ \"templateRequired\":#{signer[:template_required] || 'false'},
248
+ \"email\":\"#{signer[:email]}\",
249
+ \"name\":\"#{signer[:name]}\",
250
+ \"autoNavigation\":false,
251
+ \"defaultRecipient\":false,
252
+ \"signatureInfo\":null,
253
+ \"tabs\":{
254
+ \"approveTabs\":null,
255
+ \"checkboxTabs\":null,
256
+ \"companyTabs\":null,
257
+ \"dateSignedTabs\":null,
258
+ \"dateTabs\":null,
259
+ \"declineTabs\":null,
260
+ \"emailTabs\":null,
261
+ \"envelopeIdTabs\":null,
262
+ \"fullNameTabs\":null,
263
+ \"initialHereTabs\":null,
264
+ \"listTabs\":null,
265
+ \"noteTabs\":null,
266
+ \"numberTabs\":null,
267
+ \"radioGroupTabs\":null,
268
+ \"signHereTabs\":[
269
+ {
270
+ \"anchorString\":\"#{signer[:anchor_string]}\",
271
+ \"conditionalParentLabel\":null,
272
+ \"conditionalParentValue\":null,
273
+ \"documentId\":\"#{signer[:document_id] || '1'}\",
274
+ \"pageNumber\":\"#{signer[:page_number] || '1'}\",
275
+ \"recipientId\":\"#{index+1}\",
276
+ \"templateLocked\":#{signer[:template_locked] || 'false'},
277
+ \"templateRequired\":#{signer[:template_required] || 'false'},
278
+ \"xPosition\":\"#{signer[:x_position] || '0'}\",
279
+ \"yPosition\":\"#{signer[:y_position] || '0'}\",
280
+ \"name\":\"#{signer[:sign_here_tab_text] || 'Sign Here'}\",
281
+ \"optional\":false,
282
+ \"scaleValue\":1,
283
+ \"tabLabel\":\"#{signer[:tab_label] || 'Signature 1'}\"
284
+ }
285
+ ],
286
+ \"signerAttachmentTabs\":null,
287
+ \"ssnTabs\":null,
288
+ \"textTabs\":null,
289
+ \"titleTabs\":null,
290
+ \"zipTabs\":null
291
+ }
292
+ }"
293
+ end
294
+ doc_signers.join(",")
295
+ end
296
+
297
+
298
+ # Internal: sets up the file ios array
299
+ #
300
+ # files - a hash of file params
301
+ #
302
+ # Returns the properly formatted ios used to build the file_params hash
303
+ def create_file_ios(files)
304
+ # UploadIO is from the multipart-post gem's lib/composite_io.rb:57
305
+ # where it has this documentation:
306
+ #
307
+ # ********************************************************************
308
+ # Create an upload IO suitable for including in the params hash of a
309
+ # Net::HTTP::Post::Multipart.
310
+ #
311
+ # Can take two forms. The first accepts a filename and content type, and
312
+ # opens the file for reading (to be closed by finalizer).
313
+ #
314
+ # The second accepts an already-open IO, but also requires a third argument,
315
+ # the filename from which it was opened (particularly useful/recommended if
316
+ # uploading directly from a form in a framework, which often save the file to
317
+ # an arbitrarily named RackMultipart file in /tmp).
318
+ #
319
+ # Usage:
320
+ #
321
+ # UploadIO.new("file.txt", "text/plain")
322
+ # UploadIO.new(file_io, "text/plain", "file.txt")
323
+ # ********************************************************************
324
+ #
325
+ # There is also a 4th undocumented argument, opts={}, which allows us
326
+ # to send in not only the Content-Disposition of 'file' as required by
327
+ # DocuSign, but also the documentId parameter which is required as well
328
+ #
329
+ ios = []
330
+ files.each_with_index do |file, index|
331
+ ios << UploadIO.new(
332
+ file[:io] || file[:path],
333
+ file[:content_type] || "application/pdf",
334
+ file[:name],
335
+ "Content-Disposition" => "file; documentid=#{index+1}"
336
+ )
337
+ end
338
+ ios
339
+ end
340
+
341
+
342
+ # Internal: sets up the file_params for inclusion in a multipart post request
343
+ #
344
+ # ios - An array of UploadIO formatted file objects
345
+ #
346
+ # Returns a hash of files params suitable for inclusion in a multipart
347
+ # post request
348
+ def create_file_params(ios)
349
+ # multi-doc uploading capabilities, each doc needs to be it's own param
350
+ file_params = {}
351
+ ios.each_with_index do |io,index|
352
+ file_params.merge!("file#{index+1}" => io)
353
+ end
354
+ file_params
355
+ end
356
+
357
+
358
+ # Internal: takes in an array of hashes of documents and calculates the
359
+ # documentId then concatenates all the hashes with commas
360
+ #
361
+ # Returns a hash of documents that are to be uploaded
362
+ def get_documents(ios)
363
+ documents = []
364
+ ios.each_with_index do |io, index|
365
+ documents << "{
366
+ \"documentId\" : \"#{index+1}\",
367
+ \"name\" : \"#{io.original_filename}\"
368
+ }"
369
+ end
370
+ documents.join(",")
371
+ end
372
+
373
+
374
+ # Internal sets up the Net::HTTP request
375
+ #
376
+ # uri - The fully qualified final URI
377
+ # post_body - The custom post body including the signers, etc
378
+ # file_params - Formatted hash of ios to merge into the post body
379
+ # headers - Allows for passing in custom headers
380
+ #
381
+ # Returns a request object suitable for embedding in a request
382
+ def initialize_net_http_multipart_post_request(uri, post_body, file_params, headers)
383
+ # Net::HTTP::Post::Multipart is from the multipart-post gem's lib/multipartable.rb
384
+ #
385
+ # path - The fully qualified URI for the request
386
+ # params - A hash of params (including files for uploading and a
387
+ # customized request body)
388
+ # headers={} - The fully merged, final request headers
389
+ # boundary - Optional: you can give the request a custom boundary
390
+ #
391
+ request = Net::HTTP::Post::Multipart.new(
392
+ uri.request_uri,
393
+ {post_body: post_body}.merge(file_params),
394
+ headers
395
+ )
396
+
397
+ # DocuSign requires that we embed the document data in the body of the
398
+ # JSON request directly so we need to call ".read" on the multipart-post
399
+ # provided body_stream in order to serialize all the files into a
400
+ # compatible JSON string.
401
+ request.body = request.body_stream.read
402
+ request
403
+ end
404
+
405
+
406
+ # Public: creates an envelope from a document directly without a template
407
+ #
408
+ # file_io - Optional: an opened file stream of data (if you don't
409
+ # want to save the file to the file system as an incremental
410
+ # step)
411
+ # file_path - Required if you don't provide a file_io stream, this is
412
+ # the local path of the file you wish to upload. Absolute
413
+ # paths recommended.
414
+ # file_name - The name you want to give to the file you are uploading
415
+ # content_type - (for the request body) application/json is what DocuSign
416
+ # is expecting
417
+ # email_subject - (Optional) short subject line for the email
418
+ # email_body - (Optional) custom text that will be injected into the
419
+ # DocuSign generated email
420
+ # signers - A hash of users who should receive the document and need
421
+ # to sign it. More info about the options available for
422
+ # this method are documented above it's method definition.
423
+ # status - Options include: 'sent', 'created', 'voided' and determine
424
+ # if the envelope is sent out immediately or stored for
425
+ # sending at a later time
426
+ # headers - Allows a client to pass in some
427
+ #
428
+ # Returns a response object containing:
429
+ # envelopeId - The envelope's ID
430
+ # status - Sent, created, or voided
431
+ # statusDateTime - The date/time the envelope was created
432
+ # uri - The relative envelope uri
433
+ def create_envelope_from_document(options={})
434
+ ios = create_file_ios(options[:files])
435
+ file_params = create_file_params(ios)
436
+
437
+ post_body = "{
438
+ \"emailBlurb\" : \"#{options[:email][:body] if options[:email]}\",
439
+ \"emailSubject\" : \"#{options[:email][:subject] if options[:email]}\",
440
+ \"documents\" : [#{get_documents(ios)}],
441
+ \"recipients\" : {
442
+ \"signers\" : [#{get_signers(options[:signers])}]
443
+ },
444
+ \"status\" : \"#{options[:status]}\"
445
+ }
446
+ "
447
+
448
+ uri = build_uri("/accounts/#{@acct_id}/envelopes")
449
+
450
+ http = initialize_net_http_ssl(uri)
451
+
452
+ request = initialize_net_http_multipart_post_request(
453
+ uri, post_body, file_params, headers(options[:headers])
454
+ )
455
+
456
+ # Finally do the Net::HTTP request!
457
+ http.request(request)
458
+ end
459
+
460
+
461
+ # Public: allows a template to be dynamically created with several options.
462
+ #
463
+ # files - An array of hashes of file parameters which will be used
464
+ # to create actual files suitable for upload in a multipart
465
+ # request.
466
+ #
467
+ # Options: io, path, name. The io is optional and would
468
+ # require creating a file_io object to embed as the first
469
+ # argument of any given file hash. See the create_file_ios
470
+ # method definition above for more details.
471
+ #
472
+ # email/body - (Optional) sets the text in the email. Note: the envelope
473
+ # seems to override this, not sure why it needs to be
474
+ # configured here as well. I usually leave it blank.
475
+ # email/subject - (Optional) sets the text in the email. Note: the envelope
476
+ # seems to override this, not sure why it needs to be
477
+ # configured here as well. I usually leave it blank.
478
+ # signers - An array of hashes of signers. See the
479
+ # get_template_signers method definition for options.
480
+ # description - The template description
481
+ # name - The template name
482
+ # headers - Optional hash of headers to merge into the existing
483
+ # required headers for a multipart request.
484
+ #
485
+ # Returns a response body containing the template's:
486
+ # name - Name given above
487
+ # templateId - The auto-generated ID provided by DocuSign
488
+ # Uri - the URI where the template is located on the DocuSign servers
489
+ def create_template(options={})
490
+ ios = create_file_ios(options[:files])
491
+ file_params = create_file_params(ios)
492
+
493
+ post_body = "{
494
+ \"emailBlurb\" : \"#{options[:email][:body] if options[:email]}\",
495
+ \"emailSubject\" : \"#{options[:email][:subject] if options[:email]}\",
496
+ \"documents\" : [#{get_documents(ios)}],
497
+ \"recipients\" : {
498
+ \"signers\" : [#{get_template_signers(options[:signers])}]
499
+ },
500
+ \"envelopeTemplateDefinition\" : {
501
+ \"description\" : \"#{options[:description]}\",
502
+ \"name\" : \"#{options[:name]}\",
503
+ \"pageCount\" : 1,
504
+ \"password\" : \"\",
505
+ \"shared\" : false
506
+ }
507
+ }
508
+ "
509
+
510
+ uri = build_uri("/accounts/#{@acct_id}/templates")
511
+
512
+ http = initialize_net_http_ssl(uri)
513
+
514
+ request = initialize_net_http_multipart_post_request(
515
+ uri, post_body, file_params, headers(options[:headers])
516
+ )
517
+
518
+ # Finally do the Net::HTTP request!
519
+ http.request(request)
520
+ end
521
+
522
+
523
+ # Public: create an envelope for delivery from a template
524
+ #
525
+ # headers - Optional hash of headers to merge into the existing
526
+ # required headers for a POST request.
527
+ # status - Options include: 'sent', 'created', 'voided' and
528
+ # determine if the envelope is sent out immediately or
529
+ # stored for sending at a later time
530
+ # email/body - Sets the text in the email body
531
+ # email/subject - Sets the text in the email subject line
532
+ # template_id - The id of the template upon which we want to base this
533
+ # envelope
534
+ # template_roles - See the get_template_roles method definition for a list
535
+ # of options to pass. Note: for consistency sake we call
536
+ # this 'signers' and not 'templateRoles' when we build up
537
+ # the request in client code.
538
+ # headers - Optional hash of headers to merge into the existing
539
+ # required headers for a multipart request.
540
+ #
541
+ # Returns a response body containing the envelope's:
542
+ # name - Name given above
543
+ # templateId - The auto-generated ID provided by DocuSign
544
+ # Uri - the URI where the template is located on the DocuSign servers
545
+ def create_envelope_from_template(options={})
546
+ content_type = {'Content-Type' => 'application/json'}
547
+ content_type.merge(options[:headers]) if options[:headers]
548
+
549
+ post_body = "{
550
+ \"status\" : \"#{options[:status]}\",
551
+ \"emailBlurb\" : \"#{options[:email][:body]}\",
552
+ \"emailSubject\" : \"#{options[:email][:subject]}\",
553
+ \"templateId\" : \"#{options[:template_id]}\",
554
+ \"templateRoles\" : [#{get_template_roles(options[:signers])}],
555
+ }"
556
+
557
+ uri = build_uri("/accounts/#{@acct_id}/envelopes")
558
+
559
+ http = initialize_net_http_ssl(uri)
560
+
561
+ request = Net::HTTP::Post.new(
562
+ uri.request_uri,
563
+ headers(content_type)
564
+ )
565
+ request.body = post_body
566
+
567
+ http.request(request)
568
+ end
569
+
570
+
571
+ # Public returns the URL for embedded signing
572
+ #
573
+ # envelope_id - the ID of the envelope you wish to use for embedded signing
574
+ # name - the name of the signer
575
+ # email - the email of the recipient
576
+ # return_url - the URL you want the user to be directed to after he or she
577
+ # completes the document signing
578
+ # headers - optional hash of headers to merge into the existing
579
+ # required headers for a multipart request.
580
+ #
581
+ # Returns the URL for embedded signing which needs to be put in an iFrame
582
+ def get_recipient_view(options={})
583
+ content_type = {'Content-Type' => 'application/json'}
584
+ content_type.merge(options[:headers]) if options[:headers]
585
+
586
+ post_body = "{
587
+ \"authenticationMethod\" : \"email\",
588
+ \"clientUserId\" : \"#{options[:email]}\",
589
+ \"email\" : \"#{options[:email]}\",
590
+ \"returnUrl\" : \"#{options[:return_url]}\",
591
+ \"userName\" : \"#{options[:name]}\",
592
+ }"
593
+
594
+ uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/views/recipient")
595
+
596
+ http = initialize_net_http_ssl(uri)
597
+
598
+ request = Net::HTTP::Post.new(
599
+ uri.request_uri,
600
+ headers(content_type)
601
+ )
602
+ request.body = post_body
603
+
604
+ http.request(request)
605
+ end
606
+
607
+ end
608
+
609
+ end