├── .gitignore
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── cacert.pem
├── docusign_rest.gemspec
├── examples
├── request_via_gem.rb
└── request_via_raw_net_http.rb
├── lib
├── docusign_rest.rb
├── docusign_rest
│ ├── client.rb
│ ├── configuration.rb
│ ├── railtie.rb
│ ├── utility.rb
│ └── version.rb
└── tasks
│ ├── docusign_task.rake
│ └── docusign_task.rb
├── test.pdf
├── test
├── docusign_rest
│ ├── client_test.rb
│ ├── configuration_test.rb
│ └── docusign_rest_test.rb
└── helper.rb
└── test2.pdf
/.gitignore:
--------------------------------------------------------------------------------
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/docusign_login_config.rb
16 | test/tmp
17 | test/version_tmp
18 | test/fixtures/vcr/*
19 | tmp
20 | .rvmrc
21 | test.rb
22 | .DS_Store
23 | docusign_docs/*
24 | spike_files/*
25 | .ruby-gemset
26 | .ruby-version
27 | .idea/*
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.4.4 Nov 8 2018
4 | * Allow email_settings in create_envelope_from_document (Kevin Coleman)
5 |
6 | ## v0.4.3 Oct 27 2018
7 | * Implement Docusign::Client#get_users_list (Hendrik Kleinwaechter)
8 |
9 | ## v0.4.2 Oct 2 2018
10 | * Allow radio buttons tabs to be passed in as part of signer-specific tabs (Iago Pimenta)
11 |
12 | ## v0.4.1 Apr 21 2018
13 | * Allow DocusignRest::Client#add_envelope_document to accept an I/O object vs reading a file
14 |
15 | ## v0.4.0 Apr 16 2018
16 | * Allow text tabs to be passed to DocusignRest::Client#add_recipient_tabs (Tom Copeland)
17 |
18 | ## v0.3.9 Apr 12 2018
19 | * Handle another error condition when logging (Tom Copeland)
20 |
21 | ## v0.3.8 Mar 15 2018
22 | * Allow require_sign_on_paper option in create_envelope_from_document (Micah Iriye)
23 |
24 | ## v0.3.7 Mar 13 2018
25 | * Fix mispeling in parameter to create_envelope_from_document (Micah Iriye)
26 |
27 | ## v0.3.6 Jan 31 2018
28 |
29 | * Add support for radio button groups in DocusignRest::Client#create_envelope_from_composite_template (Tom Copeland)
30 | * Implement DocusignRest::Client#get_document_tabs (Tom Copeland)
31 |
32 | ## v0.3.5 Dec 12 2017
33 |
34 | * Implement DocusignRest::Client#send_envelope (Derek Harrington)
35 |
36 | ## v0.3.4 Nov 20 2017
37 |
38 | * Add open (default 5 seconds) and read (default 10 seconds) timeouts (Tom Copeland)
39 |
40 | ## v0.3.3 Sep 1 2017
41 |
42 | * Support fetching templates by folder name (Tom Copeland)
43 |
44 | ## v0.3.2 July 27 2017
45 |
46 | * Implement DocusignRest::Client#update_signing_group_users (Pramod Chavan)
47 | * Add support for signer id_check_information_input (Pramod Chavan)
48 | * Add support for signer phone authentication (Pramod Chavan)
49 | * Implement DocusignRest::Client#add_envelope_recipients (Pramod Chavan)
50 | * Implement DocusignRest::Client#update_envelope_recipients (Pramod Chavan)
51 | * Implement DocusignRest::Client#get_signing_groups (Pramod Chavan)
52 | * Implement DocusignRest::Client#delete_signing_groups (Pramod Chavan)
53 | * Implement DocusignRest::Client#create_signing_group (Lakshmi Narayana Chitturi)
54 | * Fix parameter name type in DocusignRest::Client#void_envelope (Ryan Wood)
55 | * Implement DocusignRest::Client#get_page_image (Tom Copeland)
56 |
57 | ## v0.3.1 May 16 2017
58 |
59 | ### Features:
60 | * Enable webhooks for DocusignRest::Client#create_envelope_from_composite_template (Tom Copeland)
61 |
62 | ## v0.3.0 May 11 2017
63 |
64 | ### Features:
65 | * Add brandId and allow_reassign options to DocusignRest::Client#create_envelope_from_template and DocusignRest::Client#create_envelope_from_composite_template (Jayan Jacob)
66 | * Implement DocusignRest::Client#add_envelope_certified_deliveries (Moses Dwaram)
67 | * Add envelopeIds option to DocusignRest::Client#get_envelope_statuses (Amit Chakradeo)
68 | * Add recipientEvents option to event notification payload (Guillermo Wu)
69 | * Added logging of each call to support Docusign API certification (Jon Witucki)
70 | * Enable requireSignOnPaper option for a recipient in a composite template (Tom Copeland)
71 | * Support routingOrder option when generating signers (Guillaume Dott)
72 | * Support arbitrary parameters to DocusignRest::Client#get_combined_document_from_envelope (Coley Brown)
73 | * Support event notifications in DocusignRest::Client#get_combined_document_from_envelope (Maxime Orefice)
74 | * Support wet_sign option on DocusignRest::Client#create_envelope_from_document (Sergio Cambra)
75 | * Support signHereTabs on DocusignRest::Client#get_inline_signers (Chris Sturm)
76 | * Support additional tab options (Hoang Le)
77 |
78 | ### Misc:
79 | * Replace monkeypatch with argument usage (Jean-Philippe Moal)
80 | * Bumped minimum Ruby version to 2.1.0. (Tom Copeland)
81 | * DocusignRest::Client#void_envelope now returns a JSON object rather than a request object (Tom Copeland)
82 |
83 | ## v0.2.0 April 28 2017
84 |
85 | ### Features:
86 | * Implement DocusignRest::Client#get_sender_view (Gonzalo Rodríguez)
87 | * Implement DocusignRest::Client#add_envelope_signers (Dan Rench)
88 | * Implement DocusignRest::Client#get_folder_list (Matthew Santeler)
89 | * Implement DocusignRest::Client#get_composite_template (lbspen)
90 | * Implement DocusignRest::Client#create_envelope_from_composite_template (lbspen, Ariel Fox)
91 | * Implement DocusignRest::Client#get_templates_in_envelope (lbspen)
92 | * Implement DocusignRest::Client#get_combined_document_from_envelope (Patrick Logan)
93 | * Implement DocusignRest::Client#get_envelope_audit_events (Sean Woojin Kim)
94 | * Implement DocusignRest::Client#void_envelope (Mike Pence)
95 | * Implement DocusignRest::Client#delete_envelope_recipient (Mike Pence)
96 | * DocusignRest::Client#get_template_roles now supports numberTabs (Mike Pence)
97 | * DocusignRest::Client#get_tabs now supports the "selected" and "optional" options (Shane Stanford, Greg)
98 | * DocusignRest::Client#get_token now requires an integrationKey argument (Joe Heth)
99 | * Added support for adding/removing envelope documents (Andrew Porterfield)
100 | * Added support for adding recipient tabs (Andrew Porterfield)
101 | * DocusignRest::Client#create_envelope_from_document now supports a customFields options (Jon Witucki)
102 | * DocusignRest::Client#create_envelope_from_template now supports a customFields option (Tyler Green)
103 | * DocusignRest::Client#get_signer_tabs now supports locking tabs (Chris Antaki)
104 | * DocusignRest::Client#get_inline_signers now supports a client id as well as email address (Patrick Logan)
105 |
106 | ### Bug fixes:
107 | * A tab's scaleValue can now be set (Jon Witucki)
108 | * tab height is no longer improperly set to tab width (mesbahmilad)
109 | * DocusignRest::Client#get_account_id no longer always returns nil (Mark Wilson)
110 |
111 | ### Misc:
112 | * More Rubyish variable naming (Chris Doyle)
113 | * Whitespace cleanup and unnecessary local variable removal (Jon Witucki)
114 | * Updated setup instructions (entrision)
115 | * Fixed header syntax in code example (Paulo Abreu)
116 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | ruby '2.4.0'
4 | #ruby-gemset=docusign_rest
5 |
6 | # Specify your gem's dependencies in docusign_rest.gemspec
7 | gemspec
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2017 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.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DocusignRest
2 |
3 | This 'wrapper gem' hooks a Ruby app (currently only tested with Rails) up to the [DocuSign](http://www.docusign.com/) REST API ([docs](https://docs.docusign.com/esign/), [API explorer](https://apiexplorer.docusign.com/#/esign/restapi)) 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 https://demo.docusign.net
38 | ...or their production url if applicable
39 | 2) From the Avatar menu in the upper right hand corner of the page, click "Go to Admin"
40 | 3) From the left sidebar menu, click "API and Keys"
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 | #config.endpoint = 'https://www.docusign.net/restapi'
58 | #config.api_version = 'v2'
59 | end
60 |
61 |
62 | ### Config Options
63 |
64 | There are several other configuration options available but the two most likely to be needed are:
65 |
66 | ```ruby
67 | config.endpoint = 'https://docusign.net/restapi'
68 | config.api_version = 'v2'
69 | config.open_timeout = 2 # default value is 5
70 | config.read_timeout = 5 # default value is 10
71 | ```
72 |
73 | The above options allow you to change the endpoint (to be able to hit the production DocuSign API, for instance) and to modify the API version you wish to use.
74 |
75 | ## Usage
76 |
77 | 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. 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](https://github.com/j2fly/docusign_rest/blob/master/lib/docusign_rest/client.rb#L397).
78 |
79 | ### Examples
80 |
81 | * Unless noted otherwise, these requests return the JSON parsed body of the response so you can index the returned hash directly. For example: `template_response["templateId"]`.
82 |
83 | #### Situations
84 |
85 | **In the context of a Rails app**
86 |
87 | This is how most people are using this gem - they've got a Rails app that's doing things with the Docusign API. In that case, these examples assume you have already set up a docusign account, have 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.
88 |
89 | **In the context of this gem as a standalone project**
90 |
91 | Ideally this gem will be independent of Rails. If that's your situation, there won't be a Rails initializer so your code will need to load the API authentication credentials. You will want to do something like:
92 |
93 | ```ruby
94 | load 'test/docusign_login_config.rb'
95 | client = DocusignRest::Client.new
96 | client.get_account_id
97 | document_envelope_response = client.create_envelope_from_document( # etc etc
98 | ```
99 |
100 | #### Example code
101 |
102 | **Getting account_id:**
103 |
104 | ```ruby
105 | client = DocusignRest::Client.new
106 | puts client.get_account_id
107 | ```
108 |
109 | **Creating an envelope from a document:**
110 |
111 | Here's how to create an envelope from a local PDF file and open a browser to the URL for the recipient:
112 |
113 | ```ruby
114 | client = DocusignRest::Client.new
115 | document_envelope_response = client.create_envelope_from_document(
116 | email: {
117 | subject: "test email subject",
118 | body: "this is the email body and it's large!"
119 | },
120 | # If embedded is set to true in the signers array below, emails
121 | # don't go out to the signers and you can embed the signature page in an
122 | # iframe by using the client.get_recipient_view method
123 | signers: [
124 | {
125 | embedded: true,
126 | name: 'Joe Dimaggio',
127 | email: 'joe.dimaggio@example.org',
128 | role_name: 'Issuer',
129 | sign_here_tabs: [
130 | {
131 | anchor_string: 'sign here',
132 | anchor_x_offset: '-30',
133 | anchor_y_offset: '35'
134 | }
135 | ]
136 | },
137 | ],
138 | files: [
139 | {path: '/Absolute/path/to/test.pdf', name: 'test.pdf'},
140 | ],
141 | status: 'sent'
142 | )
143 | url = client.get_recipient_view(envelope_id: document_envelope_response['envelopeId'], name: "Joe Dimaggio", email: "joe.dimaggio@example.org", return_url: 'http://google.com')['url']
144 | `open #{url}`
145 | ```
146 |
147 | Note: In the example below there are two sign here tabs for the user with a role of 'Attorney'. There are also two documents attached to the envelope, however, this exact configuration would only allow for signature on the first document. If you need signature for a second document, you'll need to add further options, namely: `document_id: 2` in one of the `sign_here_tabs` so that DocuSign knows where to embed that signature tab.
148 |
149 | ```ruby
150 | client = DocusignRest::Client.new
151 | document_envelope_response = client.create_envelope_from_document(
152 | email: {
153 | subject: "test email subject",
154 | body: "this is the email body and it's large!"
155 | },
156 | # If embedded is set to true in the signers array below, emails
157 | # don't go out to the signers and you can embed the signature page in an
158 | # iframe by using the client.get_recipient_view method
159 | signers: [
160 | {
161 | embedded: true,
162 | name: 'Test Guy',
163 | email: 'someone@gmail.com',
164 | role_name: 'Issuer',
165 | sign_here_tabs: [
166 | {
167 | anchor_string: 'sign here',
168 | anchor_x_offset: '-30',
169 | anchor_y_offset: '35'
170 | }
171 | ]
172 | },
173 | {
174 | embedded: true,
175 | name: 'Test Girl',
176 | email: 'someone+else@gmail.com',
177 | role_name: 'Attorney',
178 | sign_here_tabs: [
179 | {
180 | anchor_string: 'sign_here_2',
181 | anchor_x_offset: '140',
182 | anchor_y_offset: '8'
183 | },
184 | {
185 | anchor_string: 'sign_here_3',
186 | anchor_x_offset: '140',
187 | anchor_y_offset: '8'
188 | }
189 | ]
190 | }
191 | ],
192 | files: [
193 | {path: '/Absolute/path/to/test.pdf', name: 'test.pdf'},
194 | {path: '/Absolute/path/to/test2.pdf', name: 'test2.pdf'}
195 | ],
196 | status: 'sent'
197 | )
198 | ```
199 |
200 |
201 | **Creating a template:**
202 |
203 | ```ruby
204 | client = DocusignRest::Client.new
205 | @template_response = client.create_template(
206 | description: 'Template Description',
207 | name: "Template Name",
208 | signers: [
209 | {
210 | embedded: true,
211 | name: 'jon',
212 | email: 'someone@gmail.com',
213 | role_name: 'Issuer',
214 | sign_here_tabs: [
215 | {
216 | anchor_string: 'issuer_sig',
217 | anchor_x_offset: '140',
218 | anchor_y_offset: '8'
219 | }
220 | ]
221 | },
222 | {
223 | embedded: true,
224 | name: 'tim',
225 | email: 'someone+else@gmail.com',
226 | role_name: 'Attorney',
227 | sign_here_tabs: [
228 | {
229 | anchor_string: 'attorney_sig',
230 | anchor_x_offset: '140',
231 | anchor_y_offset: '8'
232 | }
233 | ]
234 | }
235 | ],
236 | files: [
237 | {path: '/Absolute/path/to/test.pdf', name: 'test.pdf'}
238 | ]
239 | )
240 | ```
241 |
242 |
243 | **Creating an envelope from a template:**
244 |
245 | ```ruby
246 | client = DocusignRest::Client.new
247 | @envelope_response = client.create_envelope_from_template(
248 | status: 'sent',
249 | email: {
250 | subject: "The test email subject envelope",
251 | body: "Envelope body content here"
252 | },
253 | template_id: @template_response["templateId"],
254 | signers: [
255 | {
256 | embedded: true,
257 | name: 'jon',
258 | email: 'someone@gmail.com',
259 | role_name: 'Issuer'
260 | },
261 | {
262 | embedded: true,
263 | name: 'tim',
264 | email: 'someone+else@gmail.com',
265 | role_name: 'Attorney'
266 | }
267 | ]
268 | )
269 | ```
270 |
271 | **Creating an envelope from a template using custom tabs:**
272 |
273 | ```ruby
274 | client = DocusignRest::Client.new
275 | @envelope_response = client.create_envelope_from_template(
276 | status: 'sent',
277 | email: {
278 | subject: "The test email subject envelope",
279 | body: "Envelope body content here"
280 | },
281 | template_id: @template_response["templateId"],
282 | signers: [
283 | {
284 | embedded: true,
285 | name: 'jon',
286 | email: 'someone@gmail.com',
287 | role_name: 'Issuer',
288 | text_tabs: [
289 | {
290 | label: 'Seller Full Name',
291 | name: 'Seller Full Name',
292 | value: 'Jon Doe'
293 | }
294 | ]
295 | },
296 | {
297 | embedded: true,
298 | name: 'tim',
299 | email: 'someone+else@gmail.com',
300 | role_name: 'Attorney',
301 | text_tabs: [
302 | {
303 | label: 'Attorney Full Name',
304 | name: 'Attorney Full Name',
305 | value: 'Tim Smith'
306 | }
307 | ]
308 | }
309 | ]
310 | )
311 | ```
312 |
313 |
314 | **Retrieving the url for embedded signing. (Returns a string, not a hash)**
315 |
316 | ```ruby
317 | client = DocusignRest::Client.new
318 | @url = client.get_recipient_view(
319 | envelope_id: @envelope_response["envelopeId"],
320 | name: current_user.full_name,
321 | email: current_user.email,
322 | return_url: 'http://google.com'
323 | )
324 | ```
325 |
326 |
327 | **Check status of an envelope including the signers hash w/ the status of each signer**
328 |
329 | ```ruby
330 | client = DocusignRest::Client.new
331 | response = client.get_envelope_recipients(
332 | envelope_id: @envelope_response["envelopeId"],
333 | include_tabs: true,
334 | include_extended: true
335 | )
336 | ```
337 |
338 | **Retrieve a document from an envelope and store it at a local file path**
339 |
340 | ```ruby
341 | client = DocusignRest::Client.new
342 | client.get_document_from_envelope(
343 | envelope_id: @envelope_response["envelopeId"],
344 | document_id: 1,
345 | local_save_path: "#{Rails.root.join('docusign_docs/file_name.pdf')}"
346 | )
347 | ```
348 |
349 | **Void an envelope**
350 |
351 | ```ruby
352 | client = DocusignRest::Client.new
353 | client.void_envelope(
354 | envelope_id: @envelope_response["envelopeId"],
355 | voided_reason: 'Reason provided by the user'
356 | )
357 | ```
358 |
359 | ## Breaking out of the iframe after signing
360 |
361 | In order to return to your application after the signing process is complete it's important to have a way to evaluate whether or not the signing was successful and then do something about each case. The way I set this up was to render the embedded signing iframe for a controller action called 'embedded_signing' and specify the return_url of the `client.get_recipient_view` API call to be something like: http://myapp.com/docusign_response. Then in the same controller as the embedded_signing method, define the docusign_response method. This is where the signing process will redirect to after the user is done interacting with the DocuSign iframe. DocuSign passes a query string parameter in the return_url named 'event' and you can check like so: `if params[:event] == "signing_complete"` then you'll want to redirect to another spot in your app, not in the iframe. To do so, we need to use JavaScript to access the iframe's parent and set it's location to the path of our choosing. To do this, instantiate the `DocusignRest::Utility` class and call the breakout_path method like this:
362 |
363 | ```ruby
364 | class SomeController < ApplicationController
365 |
366 | # the view corresponding to this action has the iframe in it with the
367 | # @url as it's src. @envelope_response is populated from either:
368 | # @envelope_response = client.create_envelope_from_document
369 | # or
370 | # @envelope_response = client.create_envelope_from_template
371 | def embedded_signing
372 | client = DocusignRest::Client.new
373 | @url = client.get_recipient_view(
374 | envelope_id: @envelope_response["envelopeId"],
375 | name: current_user.display_name,
376 | email: current_user.email,
377 | return_url: "http://localhost:3000/docusign_response"
378 | )
379 | end
380 |
381 | def docusign_response
382 | utility = DocusignRest::Utility.new
383 |
384 | if params[:event] == "signing_complete"
385 | flash[:notice] = "Thanks! Successfully signed"
386 | render :text => utility.breakout_path(some_path), content_type: 'text/html'
387 | else
388 | flash[:notice] = "You chose not to sign the document."
389 | render :text => utility.breakout_path(some_other_path), content_type: 'text/html'
390 | end
391 | end
392 |
393 | end
394 | ```
395 |
396 | ## Contributing
397 |
398 | 1. Fork it
399 | 2. Create your feature branch (`git checkout -b my-new-feature`)
400 | 3. Commit your changes (`git commit -am 'Added some feature'`) making sure to write tests to ensure nothing breaks
401 | 4. Push to the branch (`git push origin my-new-feature`)
402 | 5. Create new Pull Request
403 |
404 | ### Running the tests
405 |
406 | 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:
407 |
408 | $ bundle exec ruby lib/tasks/docusign_task.rb
409 |
410 | 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.
411 |
412 | **VCR**
413 |
414 | The test suite uses VCR and is configured to record all requests by using the 'all' configuration option surrounding each API request. If you want to speed up the test suite locally for new feature development, you may want to change the VCR config record setting to 'once' temporarily which will not write a new YAML file for each request each time you hit the API and significantly speed up the tests. However, this can lead to false passing tests as the gem changes so it's recommended that you ensure all tests pass by actually hitting the API before submitting a pull request.
415 |
416 | **SSL Issue**
417 |
418 | In the event that you have an SSL error running the tests, such as;
419 |
420 | SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
421 |
422 | there is a sample cert 'cacert.pem' you can use when executing the
423 | test suite.
424 |
425 | SSL_CERT_FILE=cacert.pem guard
426 | SSL_CERT_FILE=cacert.pem ruby lib/tasks/docusign_task.rb
427 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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
11 |
12 | task default: [:"test"]
13 |
--------------------------------------------------------------------------------
/docusign_rest.gemspec:
--------------------------------------------------------------------------------
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', 'Tom Copeland']
6 | gem.email = ['jonkinney@gmail.com', 'tom@thomasleecopeland.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/jondkinney/docusign_rest"
10 |
11 | gem.files = `git ls-files -z`.split("\x0").reject {|p| p.match(%r{^(test/|test.*pdf|cacert.pem|.gitignore)}) }
12 | gem.name = 'docusign_rest'
13 | gem.require_paths = ['lib']
14 | gem.version = DocusignRest::VERSION
15 | gem.licenses = ['MIT']
16 |
17 | gem.required_ruby_version = '>= 2.1.0'
18 |
19 | gem.add_dependency('multipart-post', '>= 1.2')
20 | gem.add_dependency('json')
21 | gem.add_development_dependency('rake')
22 | gem.add_development_dependency('byebug')
23 | gem.add_development_dependency('minitest', '~> 4.0')
24 | gem.add_development_dependency('rb-fsevent', '~> 0.9')
25 | gem.add_development_dependency('turn')
26 | gem.add_development_dependency('pry')
27 | gem.add_development_dependency('vcr')
28 | gem.add_development_dependency('webmock')
29 | gem.add_development_dependency('safe_yaml')
30 | end
31 |
--------------------------------------------------------------------------------
/examples/request_via_gem.rb:
--------------------------------------------------------------------------------
1 | require_relative '../lib/docusign_rest'
2 |
3 | DocusignRest.configure do |config|
4 | config.username = 'jonkinney@gmail.com'
5 | config.password = 'MnUWneAH3xqL2G'
6 | config.integrator_key = 'NAXX-93c39e8c-36c4-4cb5-8099-c4fcedddd7ad'
7 | config.account_id = '327367'
8 | config.endpoint = 'https://demo.docusign.net/restapi'
9 | config.api_version = 'v2'
10 | end
11 |
12 | client = DocusignRest::Client.new
13 |
14 | response = client.create_envelope_from_document(
15 | email: {
16 | subject: 'Test email subject',
17 | body: 'This is the email body.'
18 | },
19 | # If embedded is set to true in the signers array below, emails don't go out
20 | # and you can embed the signature page in an iFrame by using the
21 | # get_recipient_view method
22 | signers: [
23 | {
24 | #embedded: true,
25 | name: 'Test Guy',
26 | email: 'someone@example.com'
27 | },
28 | {
29 | #embedded: true,
30 | name: 'Test Girl',
31 | email: 'someone+else@example.com'
32 | }
33 | ],
34 | files: [
35 | { path: 'test.pdf', name: 'test.pdf' },
36 | { path: 'test2.pdf', name: 'test2.pdf' }
37 | ],
38 | status: 'sent'
39 | )
40 |
41 | puts response #the response is a parsed JSON string
42 |
--------------------------------------------------------------------------------
/examples/request_via_raw_net_http.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 | require 'uri'
3 | require 'openssl'
4 | require 'json'
5 |
6 | # Token used to terminate the file in the post body. Make sure it is not
7 | # present in the file you're uploading.
8 | BOUNDARY = 'myboundary'
9 |
10 | uri = URI.parse('https://demo.docusign.net/restapi/v2/accounts/327367/envelopes')
11 | file = 'test.pdf'
12 |
13 | request_hash = {
14 | emailBlurb: 'eblurb',
15 | emailSubject: 'esubj',
16 | documents: [
17 | {
18 | documentId: '1',
19 | name: "#{File.basename(file)}"
20 | }
21 | ],
22 | recipients: {
23 | signers: [
24 | {
25 | email: 'someone@example.com',
26 | name: 'Test Guy',
27 | recipientId: '1'
28 | }
29 | ]
30 | },
31 | status: 'sent'
32 | }
33 |
34 | post_body = ''
35 | post_body << "\r\n"
36 | post_body << "--#{BOUNDARY}\r\n"
37 | post_body << "Content-Type: application/json\r\n"
38 | post_body << "Content-Disposition: form-data\r\n"
39 | post_body << "\r\n"
40 | post_body << request_hash.to_json
41 | post_body << "\r\n"
42 | post_body << "--#{BOUNDARY}\r\n"
43 | post_body << "Content-Type: application/pdf\r\n"
44 | post_body << "Content-Disposition: file; filename=\"#{File.basename(file)}\"; documentid=1\r\n"
45 | post_body << "\r\n"
46 | post_body << IO.read(file) #this includes the %PDF-1.3 and %%EOF wrapper
47 | post_body << "\r\n"
48 | post_body << "--#{BOUNDARY}--\r\n"
49 |
50 | http = Net::HTTP.new(uri.host, uri.port)
51 | http.use_ssl = true
52 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER
53 |
54 | docusign_headers = %{
55 |
56 | jonkinney@gmail.com
57 | MnUWneAH3xqL2G
58 | NAXX-93c39e8c-36c4-4cb5-8099-c4fcedddd7ad
59 |
60 | }
61 |
62 | headers = {
63 | 'X-DocuSign-Authentication' => "#{docusign_headers}",
64 | 'Content-Type' => "multipart/form-data; boundary=#{BOUNDARY}",
65 | 'Accept' => 'application/json',
66 | 'Content-Length' => "#{post_body.length}"
67 | }
68 |
69 | request = Net::HTTP::Post.new(uri.request_uri, headers)
70 |
71 | request.body = post_body
72 |
73 | response = http.request(request)
74 |
75 | puts response.body
76 |
--------------------------------------------------------------------------------
/lib/docusign_rest.rb:
--------------------------------------------------------------------------------
1 | require_relative 'docusign_rest/version'
2 | require_relative 'docusign_rest/configuration'
3 | require_relative 'docusign_rest/client'
4 | require_relative 'docusign_rest/utility'
5 | require 'multipart_post' #require the multipart-post gem itself
6 | require 'net/http/post/multipart' #require the multipart-post net/http/post/multipart monkey patch
7 | require 'net/http'
8 | require 'json'
9 |
10 | module DocusignRest
11 | require_relative "docusign_rest/railtie" if defined?(Rails)
12 |
13 | extend Configuration
14 | end
15 |
--------------------------------------------------------------------------------
/lib/docusign_rest/client.rb:
--------------------------------------------------------------------------------
1 | require 'openssl'
2 | require 'open-uri'
3 |
4 | module DocusignRest
5 |
6 | class Client
7 | # Define the same set of accessors as the DocusignRest module
8 | attr_accessor *Configuration::VALID_CONFIG_KEYS
9 | attr_accessor :docusign_authentication_headers, :acct_id
10 | attr_accessor :previous_call_log
11 |
12 | def initialize(options={})
13 | # Merge the config values from the module and those passed to the client.
14 | merged_options = DocusignRest.options.merge(options)
15 |
16 | # Copy the merged values to this client and ignore those not part
17 | # of our configuration
18 | Configuration::VALID_CONFIG_KEYS.each do |key|
19 | send("#{key}=", merged_options[key])
20 | end
21 |
22 | # Set up the DocuSign Authentication headers with the values passed from
23 | # our config block
24 | if access_token.nil?
25 | @docusign_authentication_headers = {
26 | 'X-DocuSign-Authentication' => {
27 | 'Username' => username,
28 | 'Password' => password,
29 | 'IntegratorKey' => integrator_key
30 | }.to_json
31 | }
32 | else
33 | @docusign_authentication_headers = {
34 | 'Authorization' => "Bearer #{access_token}"
35 | }
36 | end
37 |
38 | # Set the account_id from the configure block if present, but can't call
39 | # the instance var @account_id because that'll override the attr_accessor
40 | # that is automatically configured for the configure block
41 | @acct_id = account_id
42 |
43 | #initialize the log cache
44 | @previous_call_log = []
45 | end
46 |
47 |
48 | # Internal: sets the default request headers allowing for user overrides
49 | # via options[:headers] from within other requests. Additionally injects
50 | # the X-DocuSign-Authentication header to authorize the request.
51 | #
52 | # Client can pass in header options to any given request:
53 | # headers: {'Some-Key' => 'some/value', 'Another-Key' => 'another/value'}
54 | #
55 | # Then we pass them on to this method to merge them with the other
56 | # required headers
57 | #
58 | # Example:
59 | #
60 | # headers(options[:headers])
61 | #
62 | # Returns a merged hash of headers overriding the default Accept header if
63 | # the user passes in a new 'Accept' header key and adds any other
64 | # user-defined headers along with the X-DocuSign-Authentication headers
65 | def headers(user_defined_headers={})
66 | default = {
67 | 'Accept' => 'json' #this seems to get added automatically, so I can probably remove this
68 | }
69 |
70 | default.merge!(user_defined_headers) if user_defined_headers
71 |
72 | @docusign_authentication_headers.merge(default)
73 | end
74 |
75 |
76 | # Internal: builds a URI based on the configurable endpoint, api_version,
77 | # and the passed in relative url
78 | #
79 | # url - a relative url requiring a leading forward slash
80 | #
81 | # Example:
82 | #
83 | # build_uri('/login_information')
84 | #
85 | # Returns a parsed URI object
86 | def build_uri(url)
87 | URI.parse("#{endpoint}/#{api_version}#{url}")
88 | end
89 |
90 |
91 | # Internal: configures Net:HTTP with some default values that are required
92 | # for every request to the DocuSign API
93 | #
94 | # Returns a configured Net::HTTP object into which a request can be passed
95 | def initialize_net_http_ssl(uri)
96 | http = Net::HTTP.new(uri.host, uri.port)
97 |
98 | http.use_ssl = uri.scheme == 'https'
99 |
100 | if defined?(Rails) && Rails.env.test?
101 | in_rails_test_env = true
102 | else
103 | in_rails_test_env = false
104 | end
105 |
106 | if http.use_ssl? && !in_rails_test_env
107 | if ca_file
108 | if File.exists?(ca_file)
109 | http.ca_file = ca_file
110 | else
111 | raise 'Certificate path not found.'
112 | end
113 | end
114 |
115 | # Explicitly verifies that the certificate matches the domain.
116 | # Requires that we use www when calling the production DocuSign API
117 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER
118 | http.verify_depth = 5
119 | else
120 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE
121 | end
122 |
123 | http.open_timeout = open_timeout
124 | http.read_timeout = read_timeout
125 |
126 | http
127 | end
128 |
129 |
130 | # Public: creates an OAuth2 authorization server token endpoint.
131 | #
132 | # email - email of user authenticating
133 | # password - password of user authenticating
134 | #
135 | # Examples:
136 | #
137 | # client = DocusignRest::Client.new
138 | # response = client.get_token(integrator_key, 'someone@example.com', 'p@ssw0rd01')
139 | #
140 | # Returns:
141 | # access_token - Access token information
142 | # scope - This should always be "api"
143 | # token_type - This should always be "bearer"
144 | def get_token(integrator_key, email, password)
145 | content_type = { 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json' }
146 | uri = build_uri('/oauth2/token')
147 |
148 | request = Net::HTTP::Post.new(uri.request_uri, content_type)
149 | request.body = "grant_type=password&client_id=#{integrator_key}&username=#{email}&password=#{password}&scope=api"
150 |
151 | http = initialize_net_http_ssl(uri)
152 | response = http.request(request)
153 | generate_log(request, response, uri)
154 | JSON.parse(response.body)
155 | end
156 |
157 |
158 | # Public: gets info necessary to make additional requests to the DocuSign API
159 | #
160 | # options - hash of headers if the client wants to override something
161 | #
162 | # Examples:
163 | #
164 | # client = DocusignRest::Client.new
165 | # response = client.login_information
166 | # puts response.body
167 | #
168 | # Returns:
169 | # accountId - For the username, password, and integrator_key specified
170 | # baseUrl - The base URL for all future DocuSign requests
171 | # email - The email used when signing up for DocuSign
172 | # isDefault - # TODO identify what this is
173 | # name - The account name provided when signing up for DocuSign
174 | # userId - # TODO determine what this is used for, if anything
175 | # userName - Full name provided when signing up for DocuSign
176 | def get_login_information(options={})
177 | uri = build_uri('/login_information')
178 | request = Net::HTTP::Get.new(uri.request_uri, headers(options[:headers]))
179 | http = initialize_net_http_ssl(uri)
180 | response = http.request(request)
181 | generate_log(request, response, uri)
182 | response
183 | end
184 |
185 |
186 | # Internal: uses the get_login_information method to determine the client's
187 | # accountId and then caches that value into an instance variable so we
188 | # don't end up hitting the API for login_information more than once per
189 | # request.
190 | #
191 | # This is used by the rake task in lib/tasks/docusign_task.rake to add
192 | # the config/initialzers/docusign_rest.rb file with the proper config block
193 | # which includes the account_id in it. That way we don't require hitting
194 | # the /login_information URI in normal requests
195 | #
196 | # Returns the accountId string
197 | def get_account_id
198 | unless acct_id
199 | response = get_login_information.body
200 | hashed_response = JSON.parse(response)
201 | login_accounts = hashed_response['loginAccounts']
202 | @acct_id ||= login_accounts.first['accountId']
203 | end
204 |
205 | acct_id
206 | end
207 |
208 |
209 | # Internal: takes in an array of hashes of signers and concatenates all the
210 | # hashes with commas
211 | #
212 | # embedded - Tells DocuSign if this is an embedded signer which determines
213 | # whether or not to deliver emails. Also lets us authenticate
214 | # them when they go to do embedded signing. Behind the scenes
215 | # this is setting the clientUserId value to the signer's email.
216 | # name - The name of the signer
217 | # email - The email of the signer
218 | # role_name - The role name of the signer ('Attorney', 'Client', etc.).
219 | # tabs - Array of tab pairs grouped by type (Example type: 'textTabs')
220 | # { textTabs: [ { tabLabel: "label", name: "name", value: "value" } ] }
221 | #
222 | # Returns a hash of users that need to be embedded in the template to
223 | # create an envelope
224 | def get_template_roles(signers)
225 | template_roles = []
226 | signers.each_with_index do |signer, index|
227 | template_role = {
228 | name: signer[:name],
229 | email: signer[:email],
230 | roleName: signer[:role_name],
231 | tabs: {
232 | textTabs: get_signer_tabs(signer[:text_tabs]),
233 | checkboxTabs: get_signer_tabs(signer[:checkbox_tabs]),
234 | numberTabs: get_signer_tabs(signer[:number_tabs]),
235 | radioGroupTabs: get_radio_signer_tabs(signer[:radio_group_tabs]),
236 | fullNameTabs: get_signer_tabs(signer[:fullname_tabs]),
237 | dateTabs: get_signer_tabs(signer[:date_tabs])
238 | }
239 | }
240 |
241 | if signer[:email_notification]
242 | template_role[:emailNotification] = signer[:email_notification]
243 | end
244 |
245 | template_role['clientUserId'] = (signer[:client_id] || signer[:email]).to_s if signer[:embedded] == true
246 | template_roles << template_role
247 | end
248 | template_roles
249 | end
250 |
251 | def get_sign_here_tabs(tabs)
252 | Array(tabs).map do |tab|
253 | {
254 | documentId: tab[:document_id],
255 | recipientId: tab[:recipient_id],
256 | anchorString: tab[:anchor_string],
257 | anchorXOffset: tab[:anchorXOffset],
258 | anchorYOffset: tab[:anchorYOffset]
259 | }
260 | end
261 | end
262 |
263 | # TODO (2014-02-03) jonk => document
264 | def get_signer_tabs(tabs)
265 | Array(tabs).map do |tab|
266 | {
267 | 'tabLabel' => tab[:label],
268 | 'name' => tab[:name],
269 | 'value' => tab[:value],
270 | 'documentId' => tab[:document_id],
271 | 'selected' => tab[:selected],
272 | 'locked' => tab[:locked]
273 | }
274 | end
275 | end
276 |
277 | def get_radio_signer_tabs(tabs)
278 | Array(tabs).map do |tab|
279 | {
280 | 'documentId' => tab[:document_id],
281 | 'groupName' => tab[:group_name],
282 | 'radios' => tab[:radios],
283 | }
284 | end
285 | end
286 |
287 | # TODO (2014-02-03) jonk => document
288 | def get_event_notification(event_notification)
289 | return {} unless event_notification
290 | {
291 | useSoapInterface: event_notification[:use_soap_interface] || false,
292 | includeCertificateWithSoap: event_notification[:include_certificate_with_soap] || false,
293 | url: event_notification[:url],
294 | loggingEnabled: event_notification[:logging],
295 | 'envelopeEvents' => Array(event_notification[:envelope_events]).map do |envelope_event|
296 | {
297 | includeDocuments: envelope_event[:include_documents] || false,
298 | envelopeEventStatusCode: envelope_event[:envelope_event_status_code]
299 | }
300 | end,
301 | 'recipientEvents' => Array(event_notification[:recipient_events]).map do |recipient_event|
302 | {
303 | includeDocuments: recipient_event[:include_documents] || false,
304 | recipientEventStatusCode: recipient_event[:recipient_event_status_code]
305 | }
306 | end
307 | }
308 | end
309 |
310 |
311 | # Internal: takes an array of hashes of signers required to complete a
312 | # document and allows for setting several options. Not all options are
313 | # currently dynamic but that's easy to change/add which I (and I'm
314 | # sure others) will be doing in the future.
315 | #
316 | # template - Includes other optional fields only used when
317 | # being called from a template
318 | # email - The signer's email
319 | # name - The signer's name
320 | # embedded - Tells DocuSign if this is an embedded signer which
321 | # determines whether or not to deliver emails. Also
322 | # lets us authenticate them when they go to do
323 | # embedded signing. Behind the scenes this is setting
324 | # the clientUserId value to the signer's email.
325 | # email_notification - Send an email or not
326 | # role_name - The signer's role, like 'Attorney' or 'Client', etc.
327 | # template_locked - Doesn't seem to work/do anything
328 | # template_required - Doesn't seem to work/do anything
329 | # anchor_string - The string of text to anchor the 'sign here' tab to
330 | # document_id - If the doc you want signed isn't the first doc in
331 | # the files options hash
332 | # page_number - Page number of the sign here tab
333 | # x_position - Distance horizontally from the anchor string for the
334 | # 'sign here' tab to appear. Note: doesn't seem to
335 | # currently work.
336 | # y_position - Distance vertically from the anchor string for the
337 | # 'sign here' tab to appear. Note: doesn't seem to
338 | # currently work.
339 | # sign_here_tab_text - Instead of 'sign here'. Note: doesn't work
340 | # tab_label - TODO: figure out what this is
341 | def get_signers(signers, options={})
342 | doc_signers = []
343 |
344 | signers.each_with_index do |signer, index|
345 | doc_signer = {
346 | accessCode: '',
347 | addAccessCodeToEmail: false,
348 | customFields: signer[:custom_fields],
349 | idCheckConfigurationName: signer[:id_check_configuration_name],
350 | idCheckInformationInput: nil,
351 | inheritEmailNotificationConfiguration: false,
352 | note: signer[:note],
353 | phoneAuthentication: nil,
354 | recipientAttachment: nil,
355 | requireIdLookup: signer[:require_id_lookup],
356 | requireSignOnPaper: signer[:require_sign_on_paper] || false,
357 | roleName: signer[:role_name],
358 | routingOrder: signer[:routing_order] || index + 1,
359 | socialAuthentications: nil
360 | }
361 |
362 | recipient_id = signer[:recipient_id] || index + 1
363 | doc_signer[:recipientId] = recipient_id
364 | doc_signer[:clientUserId] = recipient_id if signer[:embedded_signing]
365 |
366 | if signer[:id_check_information_input]
367 | doc_signer[:idCheckInformationInput] =
368 | get_id_check_information_input(signer[:id_check_information_input])
369 | end
370 |
371 | if signer[:phone_authentication]
372 | doc_signer[:phoneAuthentication] =
373 | get_phone_authentication(signer[:phone_authentication])
374 | end
375 |
376 | if signer[:signing_group_id]
377 | doc_signer[:signingGroupId] = signer[:signing_group_id]
378 | else
379 | doc_signer[:email] = signer[:email]
380 | doc_signer[:name] = signer[:name]
381 | end
382 |
383 | if signer[:email_notification]
384 | doc_signer[:emailNotification] = signer[:email_notification]
385 | end
386 |
387 | if signer[:embedded]
388 | doc_signer[:clientUserId] = signer[:client_id] || signer[:email]
389 | end
390 |
391 | if options[:template] == true
392 | doc_signer[:templateAccessCodeRequired] = false
393 | doc_signer[:templateLocked] = signer[:template_locked].nil? ? true : signer[:template_locked]
394 | doc_signer[:templateRequired] = signer[:template_required].nil? ? true : signer[:template_required]
395 | end
396 |
397 | doc_signer[:autoNavigation] = false
398 | doc_signer[:defaultRecipient] = false
399 | doc_signer[:signatureInfo] = nil
400 | doc_signer[:tabs] = {
401 | approveTabs: nil,
402 | checkboxTabs: get_tabs(signer[:checkbox_tabs], options, index),
403 | companyTabs: nil,
404 | dateSignedTabs: get_tabs(signer[:date_signed_tabs], options, index),
405 | dateTabs: nil,
406 | declineTabs: nil,
407 | emailTabs: get_tabs(signer[:email_tabs], options, index),
408 | envelopeIdTabs: nil,
409 | fullNameTabs: get_tabs(signer[:full_name_tabs], options, index),
410 | listTabs: get_tabs(signer[:list_tabs], options, index),
411 | noteTabs: nil,
412 | numberTabs: get_tabs(signer[:number_tabs], options, index),
413 | radioGroupTabs: get_tabs(signer[:radio_group_tabs], options, index),
414 | initialHereTabs: get_tabs(signer[:initial_here_tabs], options.merge!(initial_here_tab: true), index),
415 | signHereTabs: get_tabs(signer[:sign_here_tabs], options.merge!(sign_here_tab: true), index),
416 | signerAttachmentTabs: nil,
417 | ssnTabs: nil,
418 | textTabs: get_tabs(signer[:text_tabs], options, index),
419 | titleTabs: get_tabs(signer[:title_tabs], options, index),
420 | zipTabs: nil
421 | }
422 |
423 | # append the fully build string to the array
424 | doc_signers << doc_signer
425 | end
426 | doc_signers
427 | end
428 |
429 |
430 | # Internal: people to be Carbon Copied on the document that is created
431 | # https://docs.docusign.com/esign/restapi/Envelopes/Envelopes/create/
432 | #
433 | # Expecting options to be an array of hashes, with each hash representing a person to carbon copy
434 | #
435 | # email - The email of the recipient to be copied on the document
436 | # name - The name of the recipient
437 | # signer_count - Used to generate required attributes recipientId and routingOrder which must be unique in the document
438 | #
439 | def get_carbon_copies(options, signer_count)
440 | copies = []
441 | (options || []).each do |cc|
442 | signer_count += 1
443 | raise "Missing required data [:email, :name]" unless (cc[:email] && cc[:name])
444 | cc.merge!(recipient_id: signer_count, routing_order: signer_count)
445 | copies << camelize_keys(cc)
446 | end
447 | copies
448 | end
449 |
450 | # Public: Translate ruby oriented keys to camel cased keys recursively through the hash received
451 | #
452 | # The method expects symbol parameters in ruby form ":access_code" and translates them to camel cased "accessCode"
453 | #
454 | # example [{access_code: '12345', email_notification: {email_body: 'abcdef'}}] -> [{'accessCode': '12345', 'emailNotification': {'emailBody': 'abcdef'}}]
455 | #
456 | def camelize_keys(hash)
457 | new_hash={}
458 | hash.each do |k,v|
459 | new_hash[camelize(k.to_s)] = (v.is_a?(Hash) ? camelize_keys(v) : v)
460 | end
461 | new_hash
462 | end
463 |
464 | # Generic implementation to avoid having to pull in Rails dependencies
465 | #
466 | def camelize(str)
467 | str.gsub(/_([a-z])/) { $1.upcase }
468 | end
469 |
470 | # Internal: takes an array of hashes of certified deliveries
471 | #
472 | # email - The recipient email
473 | # name - The recipient name
474 | # recipient_id - The recipient's id
475 | # embedded - Tells DocuSign if this is an embedded recipient which
476 | # determines whether or not to deliver emails.
477 | def get_certified_deliveries(certified_deliveries)
478 | doc_certified_deliveries = []
479 |
480 | certified_deliveries.each do |certified_delivery|
481 | doc_certified_delivery = {
482 | email: certified_delivery[:email],
483 | name: certified_delivery[:name],
484 | recipientId: certified_delivery[:recipient_id]
485 | }
486 |
487 | if certified_delivery[:embedded]
488 | doc_certified_delivery[:clientUserId] = certified_delivery[:client_id] || certified_delivery[:email]
489 | end
490 |
491 | doc_certified_deliveries << doc_certified_delivery
492 | end
493 | doc_certified_deliveries
494 | end
495 |
496 | # TODO (2014-02-03) jonk => document
497 | def get_tabs(tabs, options, index)
498 | tab_array = []
499 |
500 | Array(tabs).map do |tab|
501 | tab_hash = {}
502 |
503 | if tab[:anchor_string]
504 | tab_hash[:anchorString] = tab[:anchor_string]
505 | tab_hash[:anchorXOffset] = tab[:anchor_x_offset] || '0'
506 | tab_hash[:anchorYOffset] = tab[:anchor_y_offset] || '0'
507 | tab_hash[:anchorIgnoreIfNotPresent] = tab[:ignore_anchor_if_not_present] || false
508 | tab_hash[:anchorUnits] = 'pixels'
509 | end
510 |
511 | tab_hash[:conditionalParentLabel] = tab[:conditional_parent_label] if tab.key?(:conditional_parent_label)
512 | tab_hash[:conditionalParentValue] = tab[:conditional_parent_value] if tab.key?(:conditional_parent_value)
513 | tab_hash[:documentId] = tab[:document_id] || '1'
514 | tab_hash[:pageNumber] = tab[:page_number] || '1'
515 | tab_hash[:recipientId] = index + 1
516 | tab_hash[:required] = tab[:required] || false
517 |
518 | if options[:template] == true
519 | tab_hash[:templateLocked] = tab[:template_locked].nil? ? true : tab[:template_locked]
520 | tab_hash[:templateRequired] = tab[:template_required].nil? ? true : tab[:template_required]
521 | end
522 |
523 | if options[:sign_here_tab] == true || options[:initial_here_tab] == true
524 | tab_hash[:scaleValue] = tab[:scale_value] || 1
525 | end
526 |
527 | tab_hash[:xPosition] = tab[:x_position] || '0'
528 | tab_hash[:yPosition] = tab[:y_position] || '0'
529 | tab_hash[:name] = tab[:name] if tab[:name]
530 | tab_hash[:optional] = tab[:optional] || false
531 | tab_hash[:tabLabel] = tab[:label] || 'Signature 1'
532 | tab_hash[:width] = tab[:width] if tab[:width]
533 | tab_hash[:height] = tab[:height] if tab[:height]
534 | tab_hash[:value] = tab[:value] if tab[:value]
535 | tab_hash[:fontSize] = tab[:font_size] if tab[:font_size]
536 | tab_hash[:fontColor] = tab[:font_color] if tab[:font_color]
537 | tab_hash[:bold] = tab[:bold] if tab[:bold]
538 | tab_hash[:italic] = tab[:italic] if tab[:italic]
539 | tab_hash[:underline] = tab[:underline] if tab[:underline]
540 | tab_hash[:selected] = tab[:selected] if tab[:selected]
541 |
542 | tab_hash[:locked] = tab[:locked] || false
543 |
544 | tab_hash[:list_items] = tab[:list_items] if tab[:list_items]
545 |
546 | tab_hash[:groupName] = tab[:group_name] if tab.key?(:group_name)
547 | tab_hash[:radios] = get_tabs(tab[:radios], options, index) if tab.key?(:radios)
548 |
549 | tab_hash[:validationMessage] = tab[:validation_message] if tab[:validation_message]
550 | tab_hash[:validationPattern] = tab[:validation_pattern] if tab[:validation_pattern]
551 |
552 | tab_array << tab_hash
553 | end
554 | tab_array
555 | end
556 |
557 |
558 | # Internal: sets up the file ios array
559 | #
560 | # files - a hash of file params
561 | #
562 | # Returns the properly formatted ios used to build the file_params hash
563 | def create_file_ios(files)
564 | # UploadIO is from the multipart-post gem's lib/composite_io.rb:57
565 | # where it has this documentation:
566 | #
567 | # ********************************************************************
568 | # Create an upload IO suitable for including in the params hash of a
569 | # Net::HTTP::Post::Multipart.
570 | #
571 | # Can take two forms. The first accepts a filename and content type, and
572 | # opens the file for reading (to be closed by finalizer).
573 | #
574 | # The second accepts an already-open IO, but also requires a third argument,
575 | # the filename from which it was opened (particularly useful/recommended if
576 | # uploading directly from a form in a framework, which often save the file to
577 | # an arbitrarily named RackMultipart file in /tmp).
578 | #
579 | # Usage:
580 | #
581 | # UploadIO.new('file.txt', 'text/plain')
582 | # UploadIO.new(file_io, 'text/plain', 'file.txt')
583 | # ********************************************************************
584 | #
585 | # There is also a 4th undocumented argument, opts={}, which allows us
586 | # to send in not only the Content-Disposition of 'file' as required by
587 | # DocuSign, but also the documentId parameter which is required as well
588 | #
589 | ios = []
590 | files.each_with_index do |file, index|
591 | ios << UploadIO.new(
592 | file[:io] || file[:path],
593 | file[:content_type] || 'application/pdf',
594 | file[:name],
595 | 'Content-Disposition' => "file; documentid=#{index + 1}"
596 | )
597 | end
598 | ios
599 | end
600 |
601 |
602 | # Internal: sets up the file_params for inclusion in a multipart post request
603 | #
604 | # ios - An array of UploadIO formatted file objects
605 | #
606 | # Returns a hash of files params suitable for inclusion in a multipart
607 | # post request
608 | def create_file_params(ios)
609 | # multi-doc uploading capabilities, each doc needs to be it's own param
610 | file_params = {}
611 | ios.each_with_index do |io,index|
612 | file_params.merge!("file#{index + 1}" => io)
613 | end
614 | file_params
615 | end
616 |
617 |
618 | # Internal: takes in an array of hashes of documents and calculates the
619 | # documentId
620 | #
621 | # Returns a hash of documents that are to be uploaded
622 | def get_documents(ios)
623 | ios.each_with_index.map do |io, index|
624 | {
625 | documentId: "#{index + 1}",
626 | name: io.original_filename
627 | }
628 | end
629 | end
630 |
631 | # Internal: takes in an array of server template ids and an array of the signers
632 | # and sets up the composite template
633 | #
634 | # Takes an optional array of files, which consist of documents to be used instead of templates
635 | #
636 | # Returns an array of server template hashes
637 | def get_composite_template(server_template_ids, signers, files)
638 | composite_array = []
639 | server_template_ids.each_with_index do |template_id, idx|
640 | server_template_hash = {
641 | sequence: (idx+1).to_s,
642 | templateId: template_id,
643 | templateRoles: get_template_roles(signers),
644 | }
645 | templates_hash = {
646 | serverTemplates: [server_template_hash],
647 | inlineTemplates: get_inline_signers(signers, (idx+1).to_s)
648 | }
649 | if files
650 | document_hash = {
651 | documentId: (idx+1).to_s,
652 | name: "#{files[idx][:name] if files[idx]}"
653 | }
654 | templates_hash[:document] = document_hash
655 | end
656 | composite_array << templates_hash
657 | end
658 | composite_array
659 | end
660 |
661 |
662 | # Internal: takes signer info and the inline template sequence number
663 | # and sets up the inline template
664 | #
665 | # Returns an array of signers
666 | def get_inline_signers(signers, sequence)
667 | signers_array = []
668 | signers.each do |signer|
669 | signers_hash = {
670 | email: signer[:email],
671 | name: signer[:name],
672 | recipientId: signer[:recipient_id],
673 | roleName: signer[:role_name],
674 | clientUserId: signer[:client_id] || signer[:email],
675 | requireSignOnPaper: signer[:require_sign_on_paper] || false,
676 | tabs: {
677 | textTabs: get_signer_tabs(signer[:text_tabs]),
678 | radioGroupTabs: get_radio_signer_tabs(signer[:radio_group_tabs]),
679 | checkboxTabs: get_signer_tabs(signer[:checkbox_tabs]),
680 | numberTabs: get_signer_tabs(signer[:number_tabs]),
681 | fullNameTabs: get_signer_tabs(signer[:fullname_tabs]),
682 | dateTabs: get_signer_tabs(signer[:date_tabs]),
683 | signHereTabs: get_sign_here_tabs(signer[:sign_here_tabs])
684 | }
685 | }
686 | signers_array << signers_hash
687 | end
688 | template_hash = {sequence: sequence, recipients: { signers: signers_array }}
689 | [template_hash]
690 | end
691 |
692 |
693 | # Internal sets up the Net::HTTP request
694 | #
695 | # uri - The fully qualified final URI
696 | # post_body - The custom post body including the signers, etc
697 | # file_params - Formatted hash of ios to merge into the post body
698 | # headers - Allows for passing in custom headers
699 | #
700 | # Returns a request object suitable for embedding in a request
701 | def initialize_net_http_multipart_post_request(uri, post_body, file_params, headers)
702 | # Net::HTTP::Post::Multipart is from the multipart-post gem's lib/multipartable.rb
703 | #
704 | # path - The fully qualified URI for the request
705 | # params - A hash of params (including files for uploading and a
706 | # customized request body)
707 | # headers={} - The fully merged, final request headers
708 | # boundary - Optional: you can give the request a custom boundary
709 | #
710 |
711 | headers = headers.dup.merge(parts: {post_body: {'Content-Type' => 'application/json'}})
712 |
713 | request = Net::HTTP::Post::Multipart.new(
714 | uri.request_uri,
715 | { post_body: post_body }.merge(file_params),
716 | headers
717 | )
718 |
719 | # DocuSign requires that we embed the document data in the body of the
720 | # JSON request directly so we need to call '.read' on the multipart-post
721 | # provided body_stream in order to serialize all the files into a
722 | # compatible JSON string.
723 | request.body = request.body_stream.read
724 | request
725 | end
726 |
727 |
728 | # Public: creates an envelope from a document directly without a template
729 | #
730 | # file_io - Optional: an opened file stream of data (if you don't
731 | # want to save the file to the file system as an incremental
732 | # step)
733 | # file_path - Required if you don't provide a file_io stream, this is
734 | # the local path of the file you wish to upload. Absolute
735 | # paths recommended.
736 | # file_name - The name you want to give to the file you are uploading
737 | # content_type - (for the request body) application/json is what DocuSign
738 | # is expecting
739 | # email[subject] - (Optional) short subject line for the email
740 | # email[body] - (Optional) custom text that will be injected into the
741 | # DocuSign generated email
742 | # email_settings[bcc_emails] - (Optional) array of emails to BCC.
743 | # email_settings[reply_to_email] - (Optional) override the default reply to email for the account.
744 | # email_settings[reply_to_name] - (Optional) override the default reply to name for the account.
745 | # signers - A hash of users who should receive the document and need
746 | # to sign it. More info about the options available for
747 | # this method are documented above it's method definition.
748 | # carbon_copies - An array of hashes that includes users names and email who
749 | # should receive a copy of the document once it is complete.
750 | # status - Options include: 'sent', 'created', 'voided' and determine
751 | # if the envelope is sent out immediately or stored for
752 | # sending at a later time
753 | # customFields - (Optional) A hash of listCustomFields and textCustomFields.
754 | # Each contains an array of corresponding customField hashes.
755 | # For details, please see: http://bit.ly/1FnmRJx
756 | # headers - Allows a client to pass in some headers
757 | # wet_sign - (Optional) If true, the signer is allowed to print the
758 | # document and sign it on paper. False if not defined.
759 | #
760 | # Returns a JSON parsed response object containing:
761 | # envelopeId - The envelope's ID
762 | # status - Sent, created, or voided
763 | # statusDateTime - The date/time the envelope was created
764 | # uri - The relative envelope uri
765 | def create_envelope_from_document(options={})
766 | ios = create_file_ios(options[:files])
767 | file_params = create_file_params(ios)
768 | recipients = if options[:certified_deliveries].nil? || options[:certified_deliveries].empty?
769 | { signers: get_signers(options[:signers]) }
770 | else
771 | { certifiedDeliveries: get_signers(options[:certified_deliveries]) }
772 | end
773 |
774 |
775 | post_hash = {
776 | emailBlurb: "#{options[:email][:body] if options[:email]}",
777 | emailSubject: "#{options[:email][:subject] if options[:email]}",
778 | emailSettings: get_email_settings(options[:email_settings]),
779 | documents: get_documents(ios),
780 | recipients: {
781 | signers: get_signers(options[:signers]),
782 | carbonCopies: get_carbon_copies(options[:carbon_copies],options[:signers].size)
783 | },
784 | eventNotification: get_event_notification(options[:event_notification]),
785 | status: "#{options[:status]}",
786 | customFields: options[:custom_fields]
787 | }
788 | post_hash[:enableWetSign] = options[:wet_sign] if options.has_key? :wet_sign
789 | post_body = post_hash.to_json
790 |
791 | uri = build_uri("/accounts/#{acct_id}/envelopes")
792 |
793 | http = initialize_net_http_ssl(uri)
794 |
795 | request = initialize_net_http_multipart_post_request(
796 | uri, post_body, file_params, headers(options[:headers])
797 | )
798 |
799 | response = http.request(request)
800 | generate_log(request, response, uri)
801 | JSON.parse(response.body)
802 | end
803 |
804 | # Public: allows a template to be dynamically created with several options.
805 | #
806 | # files - An array of hashes of file parameters which will be used
807 | # to create actual files suitable for upload in a multipart
808 | # request.
809 | #
810 | # Options: io, path, name. The io is optional and would
811 | # require creating a file_io object to embed as the first
812 | # argument of any given file hash. See the create_file_ios
813 | # method definition above for more details.
814 | #
815 | # email/body - (Optional) sets the text in the email. Note: the envelope
816 | # seems to override this, not sure why it needs to be
817 | # configured here as well. I usually leave it blank.
818 | # email/subject - (Optional) sets the text in the email. Note: the envelope
819 | # seems to override this, not sure why it needs to be
820 | # configured here as well. I usually leave it blank.
821 | # signers - An array of hashes of signers. See the
822 | # get_signers method definition for options.
823 | # description - The template description
824 | # name - The template name
825 | # headers - Optional hash of headers to merge into the existing
826 | # required headers for a multipart request.
827 | #
828 | # Returns a JSON parsed response body containing the template's:
829 | # name - Name given above
830 | # templateId - The auto-generated ID provided by DocuSign
831 | # Uri - the URI where the template is located on the DocuSign servers
832 | def create_template(options={})
833 | ios = create_file_ios(options[:files])
834 | file_params = create_file_params(ios)
835 |
836 | post_body = {
837 | emailBlurb: "#{options[:email][:body] if options[:email]}",
838 | emailSubject: "#{options[:email][:subject] if options[:email]}",
839 | documents: get_documents(ios),
840 | recipients: {
841 | signers: get_signers(options[:signers], template: true)
842 | },
843 | envelopeTemplateDefinition: {
844 | description: options[:description],
845 | name: options[:name],
846 | pageCount: 1,
847 | password: '',
848 | shared: false
849 | }
850 | }.to_json
851 |
852 | uri = build_uri("/accounts/#{acct_id}/templates")
853 | http = initialize_net_http_ssl(uri)
854 |
855 | request = initialize_net_http_multipart_post_request(
856 | uri, post_body, file_params, headers(options[:headers])
857 | )
858 |
859 | response = http.request(request)
860 | generate_log(request, response, uri)
861 | JSON.parse(response.body)
862 | end
863 |
864 |
865 | # TODO (2014-02-03) jonk => document
866 | def get_template(template_id, options = {})
867 | content_type = { 'Content-Type' => 'application/json' }
868 | content_type.merge(options[:headers]) if options[:headers]
869 |
870 | uri = build_uri("/accounts/#{acct_id}/templates/#{template_id}")
871 |
872 | http = initialize_net_http_ssl(uri)
873 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
874 | response = http.request(request)
875 | generate_log(request, response, uri)
876 | JSON.parse(response.body)
877 | end
878 |
879 |
880 | # Public: create an envelope for delivery from a template
881 | #
882 | # headers - Optional hash of headers to merge into the existing
883 | # required headers for a POST request.
884 | # status - Options include: 'sent', 'created', 'voided' and
885 | # determine if the envelope is sent out immediately or
886 | # stored for sending at a later time
887 | # email/body - Sets the text in the email body
888 | # email/subject - Sets the text in the email subject line
889 | # template_id - The id of the template upon which we want to base this
890 | # envelope
891 | # template_roles - See the get_template_roles method definition for a list
892 | # of options to pass. Note: for consistency sake we call
893 | # this 'signers' and not 'templateRoles' when we build up
894 | # the request in client code.
895 | # headers - Optional hash of headers to merge into the existing
896 | # required headers for a multipart request.
897 | #
898 | # Returns a JSON parsed response body containing the envelope's:
899 | # name - Name given above
900 | # templateId - The auto-generated ID provided by DocuSign
901 | # Uri - the URI where the template is located on the DocuSign servers
902 | def create_envelope_from_template(options={})
903 | content_type = { 'Content-Type' => 'application/json' }
904 | content_type.merge(options[:headers]) if options[:headers]
905 |
906 | post_body = {
907 | status: options[:status],
908 | emailBlurb: options[:email][:body],
909 | emailSubject: options[:email][:subject],
910 | templateId: options[:template_id],
911 | enableWetSign: options[:wet_sign],
912 | brandId: options[:brand_id],
913 | eventNotification: get_event_notification(options[:event_notification]),
914 | templateRoles: get_template_roles(options[:signers]),
915 | customFields: options[:custom_fields],
916 | allowReassign: options[:allow_reassign]
917 | }.to_json
918 |
919 | uri = build_uri("/accounts/#{acct_id}/envelopes")
920 |
921 | http = initialize_net_http_ssl(uri)
922 |
923 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
924 | request.body = post_body
925 |
926 | response = http.request(request)
927 | generate_log(request, response, uri)
928 | JSON.parse(response.body)
929 | end
930 |
931 |
932 | # Public: create an envelope for delivery from a composite template
933 | #
934 | # headers - Optional hash of headers to merge into the existing
935 | # required headers for a POST request.
936 | # status - Options include: 'sent', or 'created' and
937 | # determine if the envelope is sent out immediately or
938 | # stored for sending at a later time
939 | # email/body - Sets the text in the email body
940 | # email/subject - Sets the text in the email subject line
941 | # files - Sets documents to be used instead of inline or server templates
942 | # signers - See get_template_roles/get_inline_signers for a list
943 | # of options to pass.
944 | # headers - Optional hash of headers to merge into the existing
945 | # required headers for a multipart request.
946 | # server_template_ids - Array of ids for templates uploaded to DocuSign. Templates
947 | # will be added in the order they appear in the array.
948 | #
949 | # Returns a JSON parsed response body containing the envelope's:
950 | # envelopeId - autogenerated ID provided by Docusign
951 | # uri - the URI where the template is located on the DocuSign servers
952 | # statusDateTime - The date/time the envelope was created
953 | # status - Sent, created, or voided
954 | def create_envelope_from_composite_template(options={})
955 | file_params = {}
956 |
957 | if options[:files]
958 | ios = create_file_ios(options[:files])
959 | file_params = create_file_params(ios)
960 | end
961 |
962 | post_hash = {
963 | emailBlurb: "#{options[:email][:body] if options[:email]}",
964 | emailSubject: "#{options[:email][:subject] if options[:email]}",
965 | status: options[:status],
966 | brandId: options[:brand_id],
967 | eventNotification: get_event_notification(options[:event_notification]),
968 | allowReassign: options[:allow_reassign],
969 | compositeTemplates: get_composite_template(options[:server_template_ids], options[:signers], options[:files])
970 | }
971 |
972 | post_body = post_hash.to_json
973 |
974 | uri = build_uri("/accounts/#{acct_id}/envelopes")
975 |
976 | http = initialize_net_http_ssl(uri)
977 |
978 | request = initialize_net_http_multipart_post_request(
979 | uri, post_body, file_params, headers(options[:headers])
980 | )
981 |
982 | response = http.request(request)
983 | generate_log(request, response, uri)
984 | JSON.parse(response.body)
985 | end
986 |
987 | # Public fetches custom fields for a document
988 | #
989 | # options[:envelope_id] - ID of the envelope which you want to send
990 | # options[:document_id] - ID of the envelope which you want to send
991 | #
992 | # Returns the custom fields Hash.
993 | def get_document_tabs(options)
994 | content_type = { 'Content-Type' => 'application/json' }
995 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/documents/#{options[:document_id]}/tabs")
996 |
997 | http = initialize_net_http_ssl(uri)
998 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
999 | response = http.request(request)
1000 | generate_log(request, response, uri)
1001 | JSON.parse(response.body)
1002 | end
1003 |
1004 | # Public marks an envelope as sent
1005 | #
1006 | # envelope_id - ID of the envelope which you want to send
1007 | #
1008 | # Returns the response (success or failure).
1009 | def send_envelope(envelope_id)
1010 | content_type = { 'Content-Type' => 'application/json' }
1011 |
1012 | post_body = {
1013 | status: 'sent'
1014 | }.to_json
1015 |
1016 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{envelope_id}")
1017 |
1018 | http = initialize_net_http_ssl(uri)
1019 | request = Net::HTTP::Put.new(uri.request_uri, headers(content_type))
1020 | request.body = post_body
1021 | response = http.request(request)
1022 |
1023 | JSON.parse(response.body)
1024 | end
1025 |
1026 |
1027 | # Public returns the names specified for a given email address (existing docusign user)
1028 | #
1029 | # email - the email of the recipient
1030 | # headers - optional hash of headers to merge into the existing
1031 | # required headers for a multipart request.
1032 | #
1033 | # Returns the list of names
1034 | def get_recipient_names(options={})
1035 | content_type = { 'Content-Type' => 'application/json' }
1036 | content_type.merge(options[:headers]) if options[:headers]
1037 |
1038 | uri = build_uri("/accounts/#{acct_id}/recipient_names?email=#{options[:email]}")
1039 |
1040 | http = initialize_net_http_ssl(uri)
1041 |
1042 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1043 |
1044 | response = http.request(request)
1045 | generate_log(request, response, uri)
1046 | JSON.parse(response.body)
1047 | end
1048 |
1049 | # Public adds the certified delivery recipients (Need to View) for a given envelope
1050 | #
1051 | # envelope_id - ID of the envelope for which you want to retrieve the
1052 | # signer info
1053 | # headers - optional hash of headers to merge into the existing
1054 | # required headers for a multipart request.
1055 | # certified_deliveries - A required hash of all the certified delivery recipients
1056 | # that need to be added to the envelope
1057 | #
1058 | # # The response returns the success or failure of each recipient being added
1059 | # to the envelope and the envelope ID
1060 | def add_envelope_certified_deliveries(options={})
1061 | content_type = { 'Content-Type' => 'application/json' }
1062 | content_type.merge(options[:headers]) if options[:headers]
1063 |
1064 | post_body = {
1065 | certifiedDeliveries: get_certified_deliveries(options[:certified_deliveries]),
1066 | }.to_json
1067 |
1068 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/recipients")
1069 |
1070 | http = initialize_net_http_ssl(uri)
1071 |
1072 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1073 | request.body = post_body
1074 |
1075 | response = http.request(request)
1076 | generate(request, response, uri)
1077 | JSON.parse(response.body)
1078 | end
1079 |
1080 | # Public returns the URL for embedded signing
1081 | #
1082 | # envelope_id - the ID of the envelope you wish to use for embedded signing
1083 | # name - the name of the signer
1084 | # email - the email of the recipient
1085 | # return_url - the URL you want the user to be directed to after he or she
1086 | # completes the document signing
1087 | # headers - optional hash of headers to merge into the existing
1088 | # required headers for a multipart request.
1089 | #
1090 | # Returns the URL string for embedded signing (can be put in an iFrame)
1091 | def get_recipient_view(options={})
1092 | content_type = { 'Content-Type' => 'application/json' }
1093 | content_type.merge(options[:headers]) if options[:headers]
1094 |
1095 | post_body = {
1096 | authenticationMethod: 'email',
1097 | clientUserId: options[:client_id] || options[:email],
1098 | email: options[:email],
1099 | returnUrl: options[:return_url],
1100 | userName: options[:name]
1101 | }.to_json
1102 |
1103 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/views/recipient")
1104 |
1105 | http = initialize_net_http_ssl(uri)
1106 |
1107 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1108 | request.body = post_body
1109 |
1110 | response = http.request(request)
1111 | generate_log(request, response, uri)
1112 | JSON.parse(response.body)
1113 | end
1114 |
1115 | # Public returns the URL for embedded sending
1116 | #
1117 | # envelope_id - the ID of the envelope you wish to use
1118 | # return_url - the URL you want the user to be directed to after he or she
1119 | # closes the view
1120 | # headers - optional hash of headers to merge into the existing
1121 | # required headers for a multipart request.
1122 | #
1123 | # Returns the URL string for embedded sending
1124 | def get_sender_view(options = {})
1125 | content_type = { 'Content-Type' => 'application/json' }
1126 | content_type.merge(options[:headers]) if options[:headers]
1127 |
1128 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/views/sender")
1129 |
1130 | http = initialize_net_http_ssl(uri)
1131 |
1132 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1133 | request.body = { returnUrl: options[:return_url] }.to_json
1134 |
1135 | response = http.request(request)
1136 | generate_log(request, response, uri)
1137 | JSON.parse(response.body)
1138 | end
1139 |
1140 | # Public returns the URL for embedded console
1141 | #
1142 | # envelope_id - the ID of the envelope you wish to use for embedded signing
1143 | # headers - optional hash of headers to merge into the existing
1144 | # required headers for a multipart request.
1145 | #
1146 | # Returns the URL string for embedded console (can be put in an iFrame)
1147 | def get_console_view(options={})
1148 | content_type = { 'Content-Type' => 'application/json' }
1149 | content_type.merge(options[:headers]) if options[:headers]
1150 |
1151 | post_body = {
1152 | envelopeId: options[:envelope_id]
1153 | }.to_json
1154 |
1155 | uri = build_uri("/accounts/#{acct_id}/views/console")
1156 |
1157 | http = initialize_net_http_ssl(uri)
1158 |
1159 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1160 | request.body = post_body
1161 |
1162 | response = http.request(request)
1163 | generate_log(request, response, uri)
1164 | parsed_response = JSON.parse(response.body)
1165 | parsed_response['url']
1166 | end
1167 |
1168 |
1169 | # Public returns the envelope recipients for a given envelope
1170 | #
1171 | # include_tabs - boolean, determines if the tabs for each signer will be
1172 | # returned in the response, defaults to false.
1173 | # envelope_id - ID of the envelope for which you want to retrieve the
1174 | # signer info
1175 | # headers - optional hash of headers to merge into the existing
1176 | # required headers for a multipart request.
1177 | #
1178 | # Returns a hash of detailed info about the envelope including the signer
1179 | # hash and status of each signer
1180 | def get_envelope_recipients(options={})
1181 | content_type = { 'Content-Type' => 'application/json' }
1182 | content_type.merge(options[:headers]) if options[:headers]
1183 |
1184 | include_tabs = options[:include_tabs] || false
1185 | include_extended = options[:include_extended] || false
1186 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/recipients?include_tabs=#{include_tabs}&include_extended=#{include_extended}")
1187 |
1188 | http = initialize_net_http_ssl(uri)
1189 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1190 | response = http.request(request)
1191 | generate_log(request, response, uri)
1192 | JSON.parse(response.body)
1193 | end
1194 |
1195 |
1196 | # Public retrieves the envelope status
1197 | #
1198 | # envelope_id - ID of the envelope from which the doc will be retrieved
1199 | def get_envelope_status(options={})
1200 | content_type = { 'Content-Type' => 'application/json' }
1201 | content_type.merge(options[:headers]) if options[:headers]
1202 |
1203 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}")
1204 |
1205 | http = initialize_net_http_ssl(uri)
1206 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1207 | response = http.request(request)
1208 | generate_log(request, response, uri)
1209 | JSON.parse(response.body)
1210 | end
1211 |
1212 |
1213 | # Public retrieves the statuses of envelopes matching the given query
1214 | #
1215 | # from_date - Docusign formatted Date/DateTime. Only return items after this date.
1216 | #
1217 | # to_date - Docusign formatted Date/DateTime. Only return items up to this date.
1218 | # Defaults to the time of the call.
1219 | #
1220 | # from_to_status - The status of the envelope checked for in the from_date - to_date period.
1221 | # Defaults to 'changed'
1222 | #
1223 | # envelope_ids - Comma joined list of envelope_ids which you want to query.
1224 | #
1225 | # status - The current status of the envelope. Defaults to any status.
1226 | #
1227 | # Returns an array of hashes containing envelope statuses, ids, and similar information.
1228 | def get_envelope_statuses(options={})
1229 | content_type = { 'Content-Type' => 'application/json' }
1230 | content_type.merge(options[:headers]) if options[:headers]
1231 |
1232 | query_params = options.slice(:from_date, :to_date, :from_to_status, :envelope_ids, :status)
1233 | # Note that Hash#to_query is an ActiveSupport monkeypatch
1234 | uri = build_uri("/accounts/#{acct_id}/envelopes?#{query_params.to_query}")
1235 |
1236 | http = initialize_net_http_ssl(uri)
1237 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1238 | response = http.request(request)
1239 | generate_log(request, response, uri)
1240 | JSON.parse(response.body)
1241 | end
1242 |
1243 | # Public retrieves a png of a page of a document in an envelope
1244 | #
1245 | # envelope_id - ID of the envelope from which the doc will be retrieved
1246 | # document_id - ID of the document to retrieve
1247 | # page_number - page number to retrieve
1248 | #
1249 | # Returns the png as a bytestream
1250 | def get_page_image(options={})
1251 | envelope_id = options[:envelope_id]
1252 | document_id = options[:document_id]
1253 | page_number = options[:page_number]
1254 |
1255 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{envelope_id}/documents/#{document_id}/pages/#{page_number}/page_image")
1256 |
1257 | http = initialize_net_http_ssl(uri)
1258 | request = Net::HTTP::Get.new(uri.request_uri, headers)
1259 | response = http.request(request)
1260 | generate_log(request, response, uri)
1261 | response.body
1262 | end
1263 |
1264 | # Public retrieves the attached file from a given envelope
1265 | #
1266 | # envelope_id - ID of the envelope from which the doc will be retrieved
1267 | # document_id - ID of the document to retrieve
1268 | # local_save_path - Local absolute path to save the doc to including the
1269 | # filename itself
1270 | # headers - Optional hash of headers to merge into the existing
1271 | # required headers for a multipart request.
1272 | #
1273 | # Example
1274 | #
1275 | # client.get_document_from_envelope(
1276 | # envelope_id: @envelope_response['envelopeId'],
1277 | # document_id: 1,
1278 | # local_save_path: 'docusign_docs/file_name.pdf',
1279 | # return_stream: true/false # will return the bytestream instead of saving doc to file system.
1280 | # )
1281 | #
1282 | # Returns the PDF document as a byte stream.
1283 | def get_document_from_envelope(options={})
1284 | content_type = { 'Content-Type' => 'application/json' }
1285 | content_type.merge(options[:headers]) if options[:headers]
1286 |
1287 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/documents/#{options[:document_id]}")
1288 |
1289 | http = initialize_net_http_ssl(uri)
1290 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1291 | response = http.request(request)
1292 | generate_log(request, response, uri)
1293 | return response.body if options[:return_stream]
1294 |
1295 | split_path = options[:local_save_path].split('/')
1296 | split_path.pop #removes the document name and extension from the array
1297 | path = split_path.join("/") #rejoins the array to form path to the folder that will contain the file
1298 |
1299 | FileUtils.mkdir_p(path)
1300 | File.open(options[:local_save_path], 'wb') do |output|
1301 | output << response.body
1302 | end
1303 | end
1304 |
1305 |
1306 | # Public retrieves the document infos from a given envelope
1307 | #
1308 | # envelope_id - ID of the envelope from which document infos are to be retrieved
1309 | #
1310 | # Returns a hash containing the envelopeId and the envelopeDocuments array
1311 | def get_documents_from_envelope(options={})
1312 | content_type = { 'Content-Type' => 'application/json' }
1313 | content_type.merge(options[:headers]) if options[:headers]
1314 |
1315 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/documents")
1316 |
1317 | http = initialize_net_http_ssl(uri)
1318 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1319 | response = http.request(request)
1320 | generate_log(request, response, uri)
1321 | JSON.parse(response.body)
1322 | end
1323 |
1324 |
1325 | # Public retrieves a PDF containing the combined content of all
1326 | # documents and the certificate for the given envelope.
1327 | #
1328 | # envelope_id - ID of the envelope from which the doc will be retrieved
1329 | # local_save_path - Local absolute path to save the doc to including the
1330 | # filename itself
1331 | # headers - Optional hash of headers to merge into the existing
1332 | # required headers for a multipart request.
1333 | # params - Optional params; for example, certificate: true
1334 | #
1335 | # Example
1336 | #
1337 | # client.get_combined_document_from_envelope(
1338 | # envelope_id: @envelope_response['envelopeId'],
1339 | # local_save_path: 'docusign_docs/file_name.pdf',
1340 | # return_stream: true/false # will return the bytestream instead of saving doc to file system.
1341 | # )
1342 | #
1343 | # Returns the PDF document as a byte stream.
1344 | def get_combined_document_from_envelope(options={})
1345 | content_type = { 'Content-Type' => 'application/json' }
1346 | content_type.merge(options[:headers]) if options[:headers]
1347 |
1348 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/documents/combined")
1349 | uri.query = URI.encode_www_form(options[:params]) if options[:params]
1350 |
1351 | http = initialize_net_http_ssl(uri)
1352 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1353 | response = http.request(request)
1354 | generate_log(request, response, uri)
1355 | return response.body if options[:return_stream]
1356 |
1357 | split_path = options[:local_save_path].split('/')
1358 | split_path.pop #removes the document name and extension from the array
1359 | path = split_path.join("/") #rejoins the array to form path to the folder that will contain the file
1360 |
1361 | FileUtils.mkdir_p(path)
1362 | File.open(options[:local_save_path], 'wb') do |output|
1363 | output << response.body
1364 | end
1365 | end
1366 |
1367 |
1368 | # Public moves the specified envelopes to the given folder
1369 | #
1370 | # envelope_ids - IDs of the envelopes to be moved
1371 | # folder_id - ID of the folder to move the envelopes to
1372 | # headers - Optional hash of headers to merge into the existing
1373 | # required headers for a multipart request.
1374 | #
1375 | # Example
1376 | #
1377 | # client.move_envelope_to_folder(
1378 | # envelope_ids: ["xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"]
1379 | # folder_id: "xxxxx-2222xxxxx",
1380 | # )
1381 | #
1382 | # Returns the response.
1383 | def move_envelope_to_folder(options = {})
1384 | content_type = { 'Content-Type' => 'application/json' }
1385 | content_type.merge(options[:headers]) if options[:headers]
1386 |
1387 | post_body = {
1388 | envelopeIds: options[:envelope_ids]
1389 | }.to_json
1390 |
1391 | uri = build_uri("/accounts/#{acct_id}/folders/#{options[:folder_id]}")
1392 |
1393 | http = initialize_net_http_ssl(uri)
1394 | request = Net::HTTP::Put.new(uri.request_uri, headers(content_type))
1395 | request.body = post_body
1396 | response = http.request(request)
1397 | generate_log(request, response, uri)
1398 | response
1399 | end
1400 |
1401 |
1402 | # Public returns a hash of audit events for a given envelope
1403 | #
1404 | # envelope_id - ID of the envelope to get audit events from
1405 | #
1406 | #
1407 | # Example
1408 | # client.get_envelope_audit_events(
1409 | # envelope_id: "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
1410 | # )
1411 | # Returns a hash of the events that have happened to the envelope.
1412 | def get_envelope_audit_events(options = {})
1413 | content_type = { 'Content-Type' => 'application/json' }
1414 | content_type.merge(options[:headers]) if options[:headers]
1415 |
1416 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/audit_events")
1417 |
1418 | http = initialize_net_http_ssl(uri)
1419 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1420 | response = http.request(request)
1421 | generate_log(request, response, uri)
1422 | JSON.parse(response.body)
1423 | end
1424 |
1425 | # Public retrieves folder information. Helpful to use before client.search_folder_for_envelopes
1426 | def get_folder_list(options={})
1427 | content_type = { 'Content-Type' => 'application/json' }
1428 | content_type.merge(options[:headers]) if options[:headers]
1429 |
1430 | uri = build_uri("/accounts/#{@acct_id}/folders/")
1431 |
1432 | http = initialize_net_http_ssl(uri)
1433 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1434 | response = http.request(request)
1435 | generate_log(request, response, uri)
1436 | JSON.parse(response.body)
1437 | end
1438 |
1439 | # Public retrieves the envelope(s) from a specific folder based on search params.
1440 | #
1441 | # Option Query Terms(none are required):
1442 | # query_params:
1443 | # start_position: Integer The position of the folder items to return. This is used for repeated calls, when the number of envelopes returned is too much for one return (calls return 100 envelopes at a time). The default value is 0.
1444 | # from_date: date/Time Only return items on or after this date. If no value is provided, the default search is the previous 30 days.
1445 | # to_date: date/Time Only return items up to this date. If no value is provided, the default search is to the current date.
1446 | # search_text: String The search text used to search the items of the envelope. The search looks at recipient names and emails, envelope custom fields, sender name, and subject.
1447 | # status: Status The current status of the envelope. If no value is provided, the default search is all/any status.
1448 | # owner_name: username The name of the folder owner.
1449 | # owner_email: email The email of the folder owner.
1450 | #
1451 | # Example
1452 | #
1453 | # client.search_folder_for_envelopes(
1454 | # folder_id: xxxxx-2222xxxxx,
1455 | # query_params: {
1456 | # search_text: "John Appleseed",
1457 | # from_date: '7-1-2011+11:00:00+AM',
1458 | # to_date: '7-1-2011+11:00:00+AM',
1459 | # status: "completed"
1460 | # }
1461 | # )
1462 | #
1463 | def search_folder_for_envelopes(options={})
1464 | content_type = { 'Content-Type' => 'application/json' }
1465 | content_type.merge(options[:headers]) if options[:headers]
1466 |
1467 | q ||= []
1468 | options[:query_params].each do |key, val|
1469 | q << "#{key}=#{val}"
1470 | end
1471 |
1472 | uri = build_uri("/accounts/#{@acct_id}/folders/#{options[:folder_id]}/?#{q.join('&')}")
1473 |
1474 | http = initialize_net_http_ssl(uri)
1475 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1476 | response = http.request(request)
1477 | generate_log(request, response, uri)
1478 | JSON.parse(response.body)
1479 | end
1480 |
1481 |
1482 | # TODO (2014-02-03) jonk => document
1483 | def create_account(options)
1484 | content_type = { 'Content-Type' => 'application/json' }
1485 | content_type.merge(options[:headers]) if options[:headers]
1486 |
1487 | uri = build_uri('/accounts')
1488 |
1489 | post_body = convert_hash_keys(options).to_json
1490 |
1491 | http = initialize_net_http_ssl(uri)
1492 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1493 | request.body = post_body
1494 | response = http.request(request)
1495 | generate_log(request, response, uri)
1496 | JSON.parse(response.body)
1497 | end
1498 |
1499 |
1500 | # TODO (2014-02-03) jonk => document
1501 | def convert_hash_keys(value)
1502 | case value
1503 | when Array
1504 | value.map { |v| convert_hash_keys(v) }
1505 | when Hash
1506 | Hash[value.map { |k, v| [k.to_s.camelize(:lower), convert_hash_keys(v)] }]
1507 | else
1508 | value
1509 | end
1510 | end
1511 |
1512 |
1513 | # TODO (2014-02-03) jonk => document
1514 | def delete_account(account_id, options = {})
1515 | content_type = { 'Content-Type' => 'application/json' }
1516 | content_type.merge(options[:headers]) if options[:headers]
1517 |
1518 | uri = build_uri("/accounts/#{account_id}")
1519 |
1520 | http = initialize_net_http_ssl(uri)
1521 | request = Net::HTTP::Delete.new(uri.request_uri, headers(content_type))
1522 | response = http.request(request)
1523 | generate_log(request, response, uri)
1524 | json = response.body
1525 | json = '{}' if json.nil? || json == ''
1526 | JSON.parse(json)
1527 | end
1528 |
1529 |
1530 | # Public: Retrieves a list of available templates
1531 | #
1532 | # params: Can contain a folder
1533 | #
1534 | # Example
1535 | #
1536 | # client.get_templates()
1537 | #
1538 | # or
1539 | #
1540 | # client.get_templates(params: {folder: "somefolder"})
1541 | #
1542 | # Returns a list of the available templates.
1543 | def get_templates(options={})
1544 | uri = build_uri("/accounts/#{acct_id}/templates")
1545 | uri.query = URI.encode_www_form(options[:params]) if options[:params]
1546 |
1547 | http = initialize_net_http_ssl(uri)
1548 | request = Net::HTTP::Get.new(uri.request_uri, headers({ 'Content-Type' => 'application/json' }))
1549 | response = http.request(request)
1550 | generate_log(request, response, uri)
1551 | JSON.parse(response.body)
1552 | end
1553 |
1554 |
1555 | # Public: Retrieves a list of templates used in an envelope
1556 | #
1557 | # Returns templateId, name and uri for each template found.
1558 | #
1559 | # envelope_id - DS id of envelope with templates.
1560 | def get_templates_in_envelope(envelope_id)
1561 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{envelope_id}/templates")
1562 |
1563 | http = initialize_net_http_ssl(uri)
1564 | request = Net::HTTP::Get.new(uri.request_uri, headers({ 'Content-Type' => 'application/json' }))
1565 | response = http.request(request)
1566 | generate_log(request, response, uri)
1567 | JSON.parse(response.body)
1568 | end
1569 |
1570 |
1571 | # Grabs envelope data.
1572 | # Equivalent to the following call in the API explorer:
1573 | # Get Envelopev2/accounts/:accountId/envelopes/:envelopeId
1574 | #
1575 | # envelope_id- DS id of envelope to be retrieved.
1576 | def get_envelope(envelope_id)
1577 | content_type = { 'Content-Type' => 'application/json' }
1578 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{envelope_id}")
1579 |
1580 | http = initialize_net_http_ssl(uri)
1581 | request = Net::HTTP::Get.new(uri.request_uri, headers(content_type))
1582 | response = http.request(request)
1583 | generate_log(request, response, uri)
1584 | JSON.parse(response.body)
1585 | end
1586 |
1587 |
1588 | # Public deletes a recipient for a given envelope
1589 | #
1590 | # envelope_id - ID of the envelope for which you want to retrieve the
1591 | # signer info
1592 | # recipient_id - ID of the recipient to delete
1593 | #
1594 | # Returns a hash of recipients with an error code for any recipients that
1595 | # were not successfully deleted.
1596 | def delete_envelope_recipient(options={})
1597 | content_type = {'Content-Type' => 'application/json'}
1598 | content_type.merge(options[:headers]) if options[:headers]
1599 |
1600 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/recipients")
1601 | post_body = "{
1602 | \"signers\" : [{\"recipientId\" : \"#{options[:recipient_id]}\"}]
1603 | }"
1604 |
1605 | http = initialize_net_http_ssl(uri)
1606 | request = Net::HTTP::Delete.new(uri.request_uri, headers(content_type))
1607 | request.body = post_body
1608 |
1609 | response = http.request(request)
1610 | generate_log(request, response, uri)
1611 | JSON.parse(response.body)
1612 | end
1613 |
1614 |
1615 | # Public voids an in-process envelope
1616 | #
1617 | # envelope_id - ID of the envelope to be voided
1618 | # voided_reason - Optional reason for the envelope being voided
1619 | #
1620 | # Returns the response (success or failure).
1621 | def void_envelope(options = {})
1622 | content_type = { 'Content-Type' => 'application/json' }
1623 | content_type.merge(options[:headers]) if options[:headers]
1624 |
1625 | post_body = {
1626 | "status" =>"voided",
1627 | "voidedReason" => options[:voided_reason] || "No reason provided."
1628 | }.to_json
1629 |
1630 | uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}")
1631 |
1632 | http = initialize_net_http_ssl(uri)
1633 | request = Net::HTTP::Put.new(uri.request_uri, headers(content_type))
1634 | request.body = post_body
1635 | response = http.request(request)
1636 | generate_log(request, response, uri)
1637 | response
1638 | end
1639 |
1640 | # Public deletes a document for a given envelope
1641 | # See https://docs.docusign.com/esign/restapi/Envelopes/EnvelopeDocuments/delete/
1642 | #
1643 | # envelope_id - ID of the envelope from which the doc will be retrieved
1644 | # document_id - ID of the document to delete
1645 | #
1646 | # Returns the success or failure of each document being added to the envelope and
1647 | # the envelope ID. Failed operations on array elements will add the "errorDetails"
1648 | # structure containing an error code and message. If "errorDetails" is null, then
1649 | # the operation was successful for that item.
1650 | def delete_envelope_document(options={})
1651 | content_type = {'Content-Type' => 'application/json'}
1652 | content_type.merge(options[:headers]) if options[:headers]
1653 |
1654 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/documents")
1655 | post_body = {
1656 | documents: [
1657 | { documentId: options[:document_id] }
1658 | ]
1659 | }.to_json
1660 |
1661 | http = initialize_net_http_ssl(uri)
1662 | request = Net::HTTP::Delete.new(uri.request_uri, headers(content_type))
1663 | request.body = post_body
1664 |
1665 | response = http.request(request)
1666 | generate_log(request, response, uri)
1667 | JSON.parse(response.body)
1668 | end
1669 |
1670 | # Public adds a document to a given envelope
1671 | # See https://docs.docusign.com/esign/restapi/Envelopes/EnvelopeDocuments/update/
1672 | #
1673 | # envelope_id - ID of the envelope from which the doc will be added
1674 | # document_id - ID of the document to add
1675 | # file_path - Local or remote path to file
1676 | # content_type - optional content type for file. Defaults to application/pdf.
1677 | # file_name - optional name for file. Defaults to basename of file_path.
1678 | # file_extension - optional extension for file. Defaults to extname of file_name.
1679 | # file_io - Optional: an opened I/O stream of data (if you don't
1680 | # want to read from a file)
1681 | #
1682 | # The response only returns a success or failure.
1683 | def add_envelope_document(options={})
1684 | options[:content_type] ||= 'application/pdf'
1685 | options[:file_name] ||= File.basename(options[:file_path])
1686 | options[:file_extension] ||= File.extname(options[:file_name])[1..-1]
1687 |
1688 | headers = {
1689 | 'Content-Type' => options[:content_type],
1690 | 'Content-Disposition' => "file; filename=\"#{options[:file_name]}\"; documentid=#{options[:document_id]}; fileExtension=\"#{options[:file_extension]}\""
1691 | }
1692 |
1693 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/documents/#{options[:document_id]}")
1694 | post_body = if options[:file_io].present?
1695 | options[:file_io].read
1696 | else
1697 | open(options[:file_path]).read
1698 | end
1699 |
1700 | http = initialize_net_http_ssl(uri)
1701 | request = Net::HTTP::Put.new(uri.request_uri, headers(headers))
1702 | request.body = post_body
1703 | response = http.request(request)
1704 | generate_log(request, response, uri)
1705 | response
1706 | end
1707 |
1708 | # Public adds signers to a given envelope
1709 | # Seehttps://docs.docusign.com/esign/restapi/Envelopes/EnvelopeRecipients/update/
1710 | #
1711 | # envelope_id - ID of the envelope to which the recipient will be added
1712 | # signers - Array of hashes
1713 | # See https://docs.docusign.com/esign/restapi/Envelopes/EnvelopeRecipients/update/#definitions
1714 | #
1715 | # TODO: This could be made more general as an add_envelope_recipient method
1716 | # to handle recipient types other than Signer
1717 | # See: https://docs.docusign.com/esign/restapi/Envelopes/EnvelopeRecipients/update/#examples
1718 | def add_envelope_signers(options = {})
1719 | content_type = { "Content-Type" => "application/json" }
1720 | content_type.merge(options[:headers]) if options[:headers]
1721 |
1722 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/recipients")
1723 | post_body = { signers: options[:signers] }.to_json
1724 |
1725 | http = initialize_net_http_ssl(uri)
1726 | request = Net::HTTP::Put.new(uri.request_uri, headers(content_type))
1727 | request.body = post_body
1728 |
1729 | response = http.request(request)
1730 | generate_log(request, response, uri)
1731 | JSON.parse(response.body)
1732 | end
1733 |
1734 | # Public adds recipient tabs to a given envelope
1735 | # See https://docs.docusign.com/esign/restapi/Envelopes/EnvelopeRecipients/update/
1736 | #
1737 | # envelope_id - ID of the envelope from which the doc will be added
1738 | # recipient - ID of the recipient to add tabs to
1739 | # tabs - hash of tab (see example below)
1740 | # {
1741 | # signHereTabs: [
1742 | # {
1743 | # anchorString: '/s1/',
1744 | # anchorXOffset: '5',
1745 | # anchorYOffset: '8',
1746 | # anchorIgnoreIfNotPresent: 'true',
1747 | # documentId: '1',
1748 | # pageNumber: '1',
1749 | # recipientId: '1'
1750 | # }
1751 | # ],
1752 | # initialHereTabs: [
1753 | # {
1754 | # anchorString: '/i1/',
1755 | # anchorXOffset: '5',
1756 | # anchorYOffset: '8',
1757 | # anchorIgnoreIfNotPresent: 'true',
1758 | # documentId: '1',
1759 | # pageNumber: '1',
1760 | # recipientId: '1'
1761 | # }
1762 | # ]
1763 | # }
1764 | #
1765 | # The response returns the success or failure of each document being added
1766 | # to the envelope and the envelope ID. Failed operations on array elements
1767 | # will add the "errorDetails" structure containing an error code and message.
1768 | # If "errorDetails" is null, then the operation was successful for that item.
1769 | def add_recipient_tabs(options={})
1770 | content_type = {'Content-Type' => 'application/json'}
1771 | content_type.merge(options[:headers]) if options[:headers]
1772 |
1773 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/recipients/#{options[:recipient_id]}/tabs")
1774 | tabs = options[:tabs]
1775 | index = options[:recipient_id] - 1
1776 |
1777 | post_body = {
1778 | approveTabs: nil,
1779 | checkboxTabs: nil,
1780 | companyTabs: nil,
1781 | dateSignedTabs: get_tabs(tabs[:date_signed_tabs], options, index),
1782 | dateTabs: nil,
1783 | declineTabs: nil,
1784 | emailTabs: nil,
1785 | envelopeIdTabs: nil,
1786 | fullNameTabs: nil,
1787 | listTabs: nil,
1788 | noteTabs: nil,
1789 | numberTabs: nil,
1790 | radioGroupTabs: nil,
1791 | initialHereTabs: get_tabs(tabs[:initial_here_tabs], options.merge!(initial_here_tab: true), index),
1792 | signHereTabs: get_tabs(tabs[:sign_here_tabs], options.merge!(sign_here_tab: true), index),
1793 | signerAttachmentTabs: nil,
1794 | ssnTabs: nil,
1795 | textTabs: get_tabs(tabs[:text_tabs], options, index),
1796 | titleTabs: nil,
1797 | zipTabs: nil
1798 | }.to_json
1799 |
1800 | http = initialize_net_http_ssl(uri)
1801 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1802 | request.body = post_body
1803 |
1804 | response = http.request(request)
1805 | generate_log(request, response, uri)
1806 | JSON.parse(response.body)
1807 | end
1808 |
1809 | # Public method - Creates Signing group
1810 | # group_name: The display name for the signing group. This can be a maximum of 100 characters.
1811 | # users: An array of group members for the signing group. (see example below)
1812 | # It is composed of two elements:
1813 | # name – The name for the group member. This can be a maximum of 100 characters.
1814 | # email – The email address for the group member. This can be a maximum of 100 characters.
1815 | # [
1816 | # {name: 'test1', email: 'test1@ygrene.us'}
1817 | # {name: 'test2', email: 'test2@ygrene.us'}
1818 | # ]
1819 | #
1820 | #
1821 | # The response returns a success or failure with any error messages.
1822 | # For successes DocuSign generates a signingGroupId for each group, which is included in the response.
1823 | # The response also includes information about when the group was created and modified,
1824 | # including the account user that created and modified the group.
1825 | def create_signing_group(options={})
1826 | content_type = { 'Content-Type' => 'application/json' }
1827 | content_type.merge(options[:headers]) if options[:headers]
1828 |
1829 | group_users = []
1830 | if options[:users]
1831 | options[:users].each do |user|
1832 | group_users << {
1833 | userName: user[:name],
1834 | email: user[:email]
1835 | }
1836 | end
1837 | end
1838 |
1839 | post_body = {
1840 | groups: [
1841 | {
1842 | groupName: options[:group_name],
1843 | groupType: 'sharedSigningGroup',
1844 | users: group_users
1845 | }
1846 | ]
1847 | }.to_json
1848 |
1849 | uri = build_uri("/accounts/#{@acct_id}/signing_groups")
1850 |
1851 | http = initialize_net_http_ssl(uri)
1852 |
1853 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1854 | request.body = post_body
1855 |
1856 | response = http.request(request)
1857 |
1858 | JSON.parse(response.body)
1859 | end
1860 |
1861 | # Public method - deletes a signing group
1862 | # See https://docs.docusign.com/esign/restapi/SigningGroups/SigningGroups/delete/
1863 | #
1864 | # signingGroupId - ID of the signing group to delete
1865 | #
1866 | # Returns the success or failure of each group being deleted. Failed operations on array elements will add the "errorDetails"
1867 | # structure containing an error code and message. If "errorDetails" is null, then
1868 | # the operation was successful for that item.
1869 | def delete_signing_groups(options={})
1870 | content_type = {'Content-Type' => 'application/json'}
1871 | content_type.merge!(options[:headers]) if options[:headers]
1872 |
1873 | uri = build_uri("/accounts/#{@acct_id}/signing_groups")
1874 |
1875 | groups = options[:groups]
1876 | groups.each{|h| h[:signingGroupId] = h.delete(:signing_group_id) if h.key?(:signing_group_id)}
1877 | post_body = {
1878 | groups: groups
1879 | }.to_json
1880 |
1881 | http = initialize_net_http_ssl(uri)
1882 | request = Net::HTTP::Delete.new(uri.request_uri, headers(content_type))
1883 | request.body = post_body
1884 |
1885 | response = http.request(request)
1886 | JSON.parse(response.body)
1887 | end
1888 |
1889 | # Public method - updates signing group users
1890 | # See https://docs.docusign.com/esign/restapi/SigningGroups/SigningGroupUsers/update/
1891 | #
1892 | # signingGroupId - ID of the signing group to update
1893 | #
1894 | # Returns the success or failure of each user being updated. Failed operations on array elements will add the "errorDetails"
1895 | # structure containing an error code and message. If "errorDetails" is null, then
1896 | # the operation was successful for that item.
1897 | def update_signing_group_users(options={})
1898 | content_type = {'Content-Type' => 'application/json'}
1899 | content_type.merge!(options[:headers]) if options[:headers]
1900 |
1901 | uri = build_uri("/accounts/#{@acct_id}/signing_groups/#{options[:signing_group_id]}/users")
1902 |
1903 | users = options[:users]
1904 | users.each do |user|
1905 | user[:userName] = user.delete(:user_name) if user.key?(:user_name)
1906 | end
1907 | post_body = {
1908 | users: users
1909 | }.to_json
1910 |
1911 | http = initialize_net_http_ssl(uri)
1912 | request = Net::HTTP::Put.new(uri.request_uri, headers(content_type))
1913 | request.body = post_body
1914 |
1915 | response = http.request(request)
1916 | JSON.parse(response.body)
1917 | end
1918 |
1919 | # Public: Retrieves a list of available signing groups
1920 | def get_signing_groups
1921 | uri = build_uri("/accounts/#{@acct_id}/signing_groups")
1922 |
1923 | http = initialize_net_http_ssl(uri)
1924 | request = Net::HTTP::Get.new(uri.request_uri, headers({ 'Content-Type' => 'application/json' }))
1925 | JSON.parse(http.request(request).body)
1926 | end
1927 |
1928 | # Public: Update envelope recipients
1929 | def update_envelope_recipients(options={})
1930 | content_type = {'Content-Type' => 'application/json'}
1931 | content_type.merge(options[:headers]) if options[:headers]
1932 |
1933 | resend = options[:resend].present?
1934 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/recipients?resend_envelope=#{resend}")
1935 |
1936 | signers = options[:signers]
1937 | signers.each do |signer|
1938 | signer[:recipientId] = signer.delete(:recipient_id) if signer.key?(:recipient_id)
1939 | signer[:clientUserId] = signer.delete(:client_user_id) if signer.key?(:client_user_id)
1940 | end
1941 | post_body = {
1942 | signers: signers
1943 | }.to_json
1944 |
1945 | http = initialize_net_http_ssl(uri)
1946 | request = Net::HTTP::Put.new(uri.request_uri, headers(content_type))
1947 | request.body = post_body
1948 |
1949 | response = http.request(request)
1950 | JSON.parse(response.body)
1951 | end
1952 |
1953 | # Public: Add recipients to envelope
1954 | def add_envelope_recipients(options={})
1955 | content_type = {'Content-Type' => 'application/json'}
1956 | content_type.merge(options[:headers]) if options[:headers]
1957 |
1958 | uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/recipients?resend_envelope=true")
1959 |
1960 | post_body = {
1961 | signers: get_signers(options[:signers])
1962 | }.to_json
1963 |
1964 | http = initialize_net_http_ssl(uri)
1965 | request = Net::HTTP::Post.new(uri.request_uri, headers(content_type))
1966 | request.body = post_body
1967 |
1968 | response = http.request(request)
1969 | JSON.parse(response.body)
1970 | end
1971 |
1972 | # Public method - get list of users
1973 | # See https://developers.docusign.com/esign-rest-api/reference/Users
1974 | #
1975 | # Returns a list of users
1976 | def get_users_list(options={})
1977 | content_type = {'Content-Type' => 'application/json'}
1978 | content_type.merge!(options[:headers]) if options[:headers]
1979 |
1980 | uri = build_uri("/accounts/#{@acct_id}/users?additional_info=true")
1981 |
1982 | request = Net::HTTP::Get.new(uri.request_uri, headers(options[:headers]))
1983 | http = initialize_net_http_ssl(uri)
1984 | response = http.request(request)
1985 | generate_log(request, response, uri)
1986 |
1987 | parsed_response = JSON.parse(response.body)
1988 | (parsed_response || {}).fetch("users", [])
1989 | end
1990 |
1991 | private
1992 |
1993 | # Private: Generates a standardized log of the request and response pair
1994 | # to and from DocuSign for logging and API Certification.
1995 | # and resulting list is set to the publicly accessible: @previous_call_log
1996 | # For example:
1997 | # envelope = connection.create_envelope_from_document(doc)
1998 | # connection.previous_call_log.each {|line| logger.debug line }
1999 | def generate_log(request, response, uri)
2000 | log = ['--DocuSign REQUEST--']
2001 | log << "#{request.method} #{uri.to_s}"
2002 | request.each_capitalized{ |k,v| log << "#{k}: #{v.gsub(/(?<="Password":")(.+?)(?=")/, '[FILTERED]')}" }
2003 | # Trims out the actual binary file to reduce log size
2004 | if request.body
2005 | request_body = begin
2006 | request.body.gsub(/(?<=Content-Transfer-Encoding: binary).+?(?=-------------RubyMultipartPost)/m, "\n[BINARY BLOB]\n")
2007 | rescue ArgumentError => ae
2008 | if ae.message == "invalid byte sequence in UTF-8"
2009 | request.body.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '').gsub(/^%PDF.*%%EOF/m, "\n[PDF BLOB]\n")
2010 | else
2011 | raise
2012 | end
2013 | end
2014 | log << "Body: #{request_body}"
2015 | end
2016 | log << '--DocuSign RESPONSE--'
2017 | log << "HTTP/#{response.http_version} #{response.code} #{response.msg}"
2018 | response.each_capitalized{ |k,v| log << "#{k}: #{v}" }
2019 | log << "Body: #{response.body}"
2020 | @previous_call_log = log
2021 | end
2022 |
2023 | def get_id_check_information_input(input)
2024 | {
2025 | addressInformationInput: get_address_information_input(
2026 | input.dig(:address_information_input, :address_information)),
2027 | ssn4InformationInput: get_ssn4_information_input(input[:ssn4_information_input]),
2028 | dobInformationInput: get_dob_information_input(input[:dob_information_input])
2029 | }
2030 | end
2031 |
2032 | def get_address_information_input(input)
2033 | return {} unless input
2034 | {
2035 | addressInformation:{
2036 | street1: input[:street1],
2037 | city: input[:city],
2038 | state: input[:state],
2039 | zip: input[:zip],
2040 | zipPlus4: input[:zip_plus4],
2041 | },
2042 | displayLevelCode: 'DoNotDisplay',
2043 | receiveInResponse: true,
2044 | }
2045 | end
2046 |
2047 | def get_phone_authentication(input)
2048 | return {} unless input
2049 | {
2050 | recipMayProvideNumber: true,
2051 | validateRecipProvidedNumber: true,
2052 | recordVoicePrint: true,
2053 | senderProvidedNumbers: input[:sender_provided_numbers],
2054 | }
2055 | end
2056 |
2057 | def get_ssn4_information_input(input)
2058 | return {} unless input
2059 | {
2060 | ssn4: input[:ssn4],
2061 | displayLevelCode: 'DoNotDisplay',
2062 | receiveInResponse: true,
2063 | }
2064 | end
2065 |
2066 | def get_dob_information_input(input)
2067 | return {} unless input
2068 | {
2069 | dateOfBirth: input[:date_of_birth],
2070 | displayLevelCode: 'DoNotDisplay',
2071 | receiveInResponse: true,
2072 | }
2073 | end
2074 |
2075 | def get_email_settings(input)
2076 | return {} unless input
2077 | {
2078 | bccEmailAddresses: input[:bcc_email_addresses],
2079 | replyEmailAddressOverride: input[:reply_to_email],
2080 | replyEmailNameOverride: input[:reply_to_name]
2081 | }
2082 | end
2083 | end
2084 | end
2085 |
--------------------------------------------------------------------------------
/lib/docusign_rest/configuration.rb:
--------------------------------------------------------------------------------
1 | module DocusignRest
2 | module Configuration
3 | VALID_CONNECTION_KEYS = [:endpoint, :api_version, :user_agent, :method].freeze
4 | VALID_OPTIONS_KEYS = [:access_token, :username, :password, :integrator_key, :account_id, :format, :ca_file, :open_timeout, :read_timeout].freeze
5 | VALID_CONFIG_KEYS = VALID_CONNECTION_KEYS + VALID_OPTIONS_KEYS
6 |
7 | DEFAULT_ENDPOINT = 'https://demo.docusign.net/restapi'
8 | DEFAULT_API_VERSION = 'v2'
9 | DEFAULT_USER_AGENT = "DocusignRest API Ruby Gem #{DocusignRest::VERSION}".freeze
10 | DEFAULT_METHOD = :get
11 |
12 | DEFAULT_ACCESS_TOKEN = nil
13 |
14 | DEFAULT_USERNAME = nil
15 | DEFAULT_PASSWORD = nil
16 | DEFAULT_INTEGRATOR_KEY = nil
17 | DEFAULT_ACCOUNT_ID = nil
18 | DEFAULT_CA_FILE = nil # often found at: '/etc/ssl/certs/cert.pem'
19 | DEFAULT_FORMAT = :json
20 | DEFAULT_OPEN_TIMEOUT = 5
21 | DEFAULT_READ_TIMEOUT = 10
22 |
23 | # Build accessor methods for every config options so we can do this, for example:
24 | # DocusignRest.format = :xml
25 | attr_accessor *VALID_CONFIG_KEYS
26 |
27 | # Make sure we have the default values set when we get 'extended'
28 | def self.extended(base)
29 | base.reset
30 | end
31 |
32 | def reset
33 | self.endpoint = DEFAULT_ENDPOINT
34 | self.api_version = DEFAULT_API_VERSION
35 | self.user_agent = DEFAULT_USER_AGENT
36 | self.method = DEFAULT_METHOD
37 | self.access_token = DEFAULT_ACCESS_TOKEN
38 | self.username = DEFAULT_USERNAME
39 | self.password = DEFAULT_PASSWORD
40 | self.integrator_key = DEFAULT_INTEGRATOR_KEY
41 | self.account_id = DEFAULT_ACCOUNT_ID
42 | self.format = DEFAULT_FORMAT
43 | self.ca_file = DEFAULT_CA_FILE
44 | self.open_timeout = DEFAULT_OPEN_TIMEOUT
45 | self.read_timeout = DEFAULT_READ_TIMEOUT
46 | end
47 |
48 | # Allow configuration via a block
49 | def configure
50 | yield self
51 | end
52 |
53 | def options
54 | Hash[ * VALID_CONFIG_KEYS.map { |key| [key, send(key)] }.flatten ]
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/docusign_rest/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'docusign_rest'
2 | require 'rails'
3 | module DocusignRest
4 | class Railtie < Rails::Railtie
5 | railtie_name :docusign_rest
6 |
7 | rake_tasks do
8 | load 'tasks/docusign_task.rake'
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/docusign_rest/utility.rb:
--------------------------------------------------------------------------------
1 | module DocusignRest
2 | class Utility
3 | # Public takes a path to redirect to and breaks the redirect out of an iFrame
4 | #
5 | # This can be used after embedded signing is either successful or not and has
6 | # been redirected to a controller action (like docusign_response for instance)
7 | # where the return from the signing process can be evaluated. If successful
8 | # use this to redirect to one place, otherwise redirect to another place. You
9 | # can use params[:event] to evaluate whether or not the signing was successful
10 | #
11 | # path - a relative or absolute path or rails helper can be passed in
12 | #
13 | # Example
14 | #
15 | # class SomeController < ApplicationController
16 | #
17 | # # the view corresponding to this action has the iFrame in it with the
18 | # # @url as it's src. @envelope_response is populated from either:
19 | # # @envelope_response = client.create_envelope_from_document
20 | # # or
21 | # # @envelope_response = client.create_envelope_from_template
22 | # def embedded_signing
23 | # client = DocusignRest::Client.new
24 | # @url = client.get_recipient_view(
25 | # envelope_id: @envelope_response["envelopeId"],
26 | # name: current_user.display_name,
27 | # email: current_user.email,
28 | # return_url: "http://localhost:3000/docusign_response"
29 | # )
30 | # end
31 | #
32 | # def docusign_response
33 | # utility = DocusignRest::Utility.new
34 | #
35 | # if params[:event] == "signing_complete"
36 | # flash[:notice] = "Thanks! Successfully signed"
37 | # render :text => utility.breakout_path(posts_path), content_type: 'text/html'
38 | # else
39 | # flash[:notice] = "You chose not to sign the document."
40 | # render :text => utility.breakout_path(logout_path), content_type: 'text/html'
41 | # end
42 | # end
43 | #
44 | # end
45 | #
46 | # Returns a string of HTML including some JS to redirect to the passed in
47 | # path but in the iFrame's parent.
48 | def breakout_path(path)
49 | "
"
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/docusign_rest/version.rb:
--------------------------------------------------------------------------------
1 | module DocusignRest
2 | VERSION = "0.4.4"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/tasks/docusign_task.rake:
--------------------------------------------------------------------------------
1 | require 'docusign_rest'
2 |
3 | namespace :docusign_rest do
4 | desc "Retrive account_id from the API"
5 | task :generate_config do
6 | def ask(message)
7 | STDOUT.print message
8 | STDIN.gets.chomp
9 | end
10 |
11 | STDOUT.puts %Q{
12 | Please do the following:
13 | ------------------------
14 | 1) Login or register for an account at https://demo.docusign.net
15 | ...or their production url if applicable
16 | 2) From the Avatar menu in the upper right hand corner of the page, click "Go to Admin"
17 | 3) From the left sidebar menu, click "API and Keys"
18 | 4) Request a new 'Integrator Key' via the web interface
19 | * You will use this key in one of the next steps to retrieve your 'accountId'\n\n}
20 |
21 | username = ask('Please enter your DocuSign username: ')
22 | password = ask('Please enter your DocuSign password: ')
23 | integrator_key = ask('Please enter your DocuSign integrator key: ')
24 |
25 | DocusignRest.configure do |config|
26 | config.username = username
27 | config.password = password
28 | config.integrator_key = integrator_key
29 | end
30 |
31 | # initialize a client and request the accountId
32 | client = DocusignRest::Client.new
33 | acct_id = client.get_account_id
34 |
35 | puts ""
36 |
37 | # construct the configure block for the user with his or her credentials and accountId
38 | config = %Q{# This file was generated by the docusign_rest:generate_config rake task.
39 | # You can run 'rake docusign_rest:generate_config' as many times as you need
40 | # to replace the content of this file with a new config.
41 |
42 | require 'docusign_rest'
43 |
44 | DocusignRest.configure do |config|
45 | config.username = '#{username}'
46 | config.password = '#{password}'
47 | config.integrator_key = '#{integrator_key}'
48 | config.account_id = '#{acct_id}'
49 | #config.endpoint = 'https://www.docusign.net/restapi'
50 | #config.api_version = 'v2'
51 | end\n\n}
52 |
53 | # write the initializer for the user
54 | if defined?(Rails)
55 | docusign_initializer_path = Rails.root.join("config/initializers/docusign_rest.rb")
56 | else
57 | docusign_initializer_path = "test/docusign_login_config.rb"
58 | end
59 |
60 | File.open(docusign_initializer_path, 'w') { |f| f.write(config) }
61 | # read the initializer file into a var for comparison to the config block above
62 | docusign_initializer_content = File.open(docusign_initializer_path) { |io| io.read }
63 |
64 | # if they match tell the user we wrote the file, otherwise tell them to do it themselves
65 | if docusign_initializer_content.include?(config)
66 | if defined?(Rails)
67 | puts "The following block of code was added to config/initializers/docusign_rest.rb\n\n"
68 | else
69 | puts "The following block of code was added to test/docusign_login_config.rb\n\n"
70 | end
71 | puts config
72 | else
73 | puts %Q{The config file was not able to be automatically created for you.
74 | Please create it at config/initializers/docusign_rest.rb and add the following content:\n\n}
75 | puts config
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/tasks/docusign_task.rb:
--------------------------------------------------------------------------------
1 | require 'rake'
2 | require 'json'
3 |
4 | Rake.application.rake_require '../lib/tasks/docusign_task'
5 | Rake.application['docusign_rest:generate_config'].invoke
6 |
--------------------------------------------------------------------------------
/test.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jondkinney/docusign_rest/f93eaff7b649336ef54fe5310c4c00d74531e5e1/test.pdf
--------------------------------------------------------------------------------
/test/docusign_rest/client_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../helper'
2 |
3 | describe DocusignRest::Client do
4 |
5 | before do
6 | @keys = DocusignRest::Configuration::VALID_CONFIG_KEYS
7 | end
8 |
9 | describe 'with module configuration' do
10 | before do
11 | DocusignRest.configure do |config|
12 | @keys.each do |key|
13 | config.send("#{key}=", key)
14 | end
15 | end
16 | end
17 |
18 | after do
19 | DocusignRest.reset
20 | end
21 |
22 | it "should inherit module configuration" do
23 | api = DocusignRest::Client.new
24 | @keys.each do |key|
25 | api.send(key).must_equal key
26 | end
27 | end
28 |
29 | describe 'with class configuration' do
30 | before do
31 | @config = {
32 | :username => 'un',
33 | :password => 'pd',
34 | :integrator_key => 'ik',
35 | :account_id => 'ai',
36 | :format => 'fm',
37 | :endpoint => 'ep',
38 | :api_version => 'av',
39 | :user_agent => 'ua',
40 | :method => 'md',
41 | :ca_file => 'ca',
42 | :open_timeout => 6,
43 | :read_timeout => 12,
44 | :access_token => 'at'
45 | }
46 | end
47 |
48 | it 'should override module configuration' do
49 | api = DocusignRest::Client.new(@config)
50 | @keys.each do |key|
51 | api.send(key).must_equal @config[key]
52 | end
53 | end
54 |
55 | it 'should override module configuration after' do
56 | api = DocusignRest::Client.new
57 |
58 | @config.each do |key, value|
59 | api.send("#{key}=", value)
60 | end
61 |
62 | @keys.each do |key|
63 | api.send("#{key}").must_equal @config[key]
64 | end
65 | end
66 | end
67 | end
68 |
69 | describe 'client' do
70 | before do
71 | # Note: to configure the client please run the docusign_task.rb file:
72 | #
73 | # $ ruby lib/tasks/docusign_task.rb
74 | #
75 | # which will populate the test/docusign_login_config.rb file
76 | @client = DocusignRest::Client.new
77 | end
78 |
79 | it "should allow access to the auth headers after initialization" do
80 | @client.must_respond_to :docusign_authentication_headers
81 | end
82 |
83 | it "should allow access to the acct_id after initialization" do
84 | @client.must_respond_to :acct_id
85 | end
86 |
87 | it "should return the value of acct_id" do
88 | @client.get_account_id.must_equal @client.acct_id
89 | end
90 |
91 | it "should allow creating an envelope from a document" do
92 | VCR.use_cassette("create_envelope/from_document") do
93 | response = @client.create_envelope_from_document(
94 | email: {
95 | subject: "test email subject",
96 | body: "this is the email body and it's large!"
97 | },
98 | # If embedded is set to true in the signers array below, emails
99 | # don't go out and you can embed the signature page in an iFrame
100 | # by using the get_recipient_view method. You can choose 'false' or
101 | # simply omit the option as I show in the second signer hash.
102 | signers: [
103 | {
104 | embedded: true,
105 | name: 'Test Guy',
106 | email: 'testguy@example.com',
107 | role_name: 'Issuer',
108 | sign_here_tabs: [
109 | {
110 | anchor_string: 'sign here',
111 | anchor_x_offset: '125',
112 | anchor_y_offset: '-12'
113 | }
114 | ],
115 | list_tabs: [
116 | {
117 | anchor_string: 'another test',
118 | width: '180',
119 | height: '14',
120 | anchor_x_offset: '10',
121 | anchor_y_offset: '-5',
122 | label: 'another test',
123 | selected: true,
124 | list_items: [
125 | {
126 | selected: false,
127 | text: 'Option 1',
128 | value: 'option_1'
129 | },
130 | {
131 | selected: true,
132 | text: 'Option 2',
133 | value: 'option_2'
134 | }
135 | ]
136 | }
137 | ],
138 | },
139 | {
140 | embedded: true,
141 | name: 'Test Girl',
142 | email: 'testgirl@example.com',
143 | role_name: 'Attorney',
144 | sign_here_tabs: [
145 | {
146 | anchor_string: 'sign here',
147 | anchor_x_offset: '140',
148 | anchor_y_offset: '-12'
149 | }
150 | ]
151 | }
152 | ],
153 | files: [
154 | {path: 'test.pdf', name: 'test.pdf'},
155 | {path: 'test2.pdf', name: 'test2.pdf'}
156 | ],
157 | status: 'sent',
158 | email_settings: {
159 | bcc_emails: [
160 | "test@example.com"
161 | ],
162 | reply_to_email: "test@example.com",
163 | reply_to_name: "Tester"
164 | }
165 | )
166 | response["status"].must_equal "sent"
167 | end
168 | end
169 |
170 | describe "#carbon_copies" do
171 | before do
172 | options = [
173 | {name: 'first', email: 'user@example.com', access_code: '12345', email_notification: {email_body: 'This is an email'}},
174 | {name: 'second', email: 'user2@example.com'}
175 | ]
176 | @result = @client.get_carbon_copies(options, 1)
177 | end
178 |
179 | it 'carbon copies returns an array' do
180 | @result.must_be_instance_of Array
181 | end
182 | it 'carbon copies processes multiple records' do
183 | @result.size.must_equal(2)
184 | end
185 | it 'carbon copies converts key to camel case' do
186 | @result[0]['accessCode'].wont_be_nil
187 | end
188 | it 'carbon copies translates nested key to camel case' do
189 | @result[0]['emailNotification']['emailBody'].wont_be_nil
190 | end
191 | it 'carbon copies increments and injects recipientId' do
192 | @result[0]['recipientId'].wont_be_nil
193 | @result[0]['recipientId'].must_equal(2)
194 | end
195 | it 'carbon copies increments and injects routingOrder' do
196 | @result[0]['routingOrder'].wont_be_nil
197 | @result[0]['routingOrder'].must_equal(2)
198 | end
199 | end
200 |
201 | describe "Signing groups" do
202 | def create_signing_group
203 | VCR.use_cassette('create_signing_group') do
204 | @signing_group_response = @client.create_signing_group(
205 | {
206 | users: [{name: 'test1', email: 'test@ygrene.us'}],
207 | group_name: 'sample_group'
208 | }
209 | )
210 | end
211 | end
212 |
213 | describe 'When all options are passed it should create' do
214 | before { create_signing_group }
215 |
216 | it "should create_signing_group" do
217 | signing_group = @signing_group_response["groups"].first
218 | signing_group['signingGroupId'].wont_be_nil
219 | signing_group['groupName'].must_equal "sample_group"
220 | end
221 | end
222 |
223 | describe "when no options are passed it should return errors" do
224 | before do
225 | VCR.use_cassette('signing_group_error') do
226 | @signing_group_response = @client.create_signing_group()
227 | end
228 | end
229 |
230 | it "should return error when improper options are sent" do
231 | error_response = {"groups"=>[{"errorDetails"=>{"errorCode"=>"INVALID_GROUP_NAME", "message"=>"No group name was provided."}}]}
232 |
233 | @signing_group_response.must_equal error_response
234 | end
235 | end
236 |
237 | describe 'When signingGroupId is passed it should delete' do
238 | before do
239 | create_signing_group
240 | VCR.use_cassette('delete_signing_groups') do
241 | signing_group = @signing_group_response["groups"].first
242 | @signing_group_response = @client.delete_signing_groups(
243 | {
244 | groups: [{signing_group_id: signing_group['signingGroupId']}]
245 | }
246 | )
247 | end
248 | end
249 |
250 | it "should delete_signing_groups with success response" do
251 | signing_group = @signing_group_response["groups"].first
252 | signing_group['signingGroupId'].wont_be_nil
253 | signing_group['groupName'].must_equal "sample_group"
254 | signing_group['errorDetails'].must_be_nil
255 | end
256 | end
257 |
258 | describe 'When signingGroupId is not passed it should return error' do
259 | before do
260 | VCR.use_cassette('delete_signing_groups_error') do
261 | @signing_group_response = @client.delete_signing_groups(
262 | {
263 | groups: [{}]
264 | }
265 | )
266 | end
267 | end
268 |
269 | it "should return error with error details" do
270 | error_response = {"groups"=>[{"errorDetails"=>{"errorCode"=>"SIGNING_GROUP_INVALID", "message"=>"Invalid signing group supplied."}}]}
271 | @signing_group_response.must_equal error_response
272 | end
273 | end
274 |
275 | describe 'returns list of signing groups' do
276 | before do
277 | create_signing_group
278 | VCR.use_cassette('list_signing_groups') do
279 | signing_group = @signing_group_response["groups"].first
280 | @signing_group_response = @client.get_signing_groups
281 | end
282 | end
283 |
284 | it "should return list of signing groups" do
285 | signing_group = @signing_group_response["groups"].last
286 | signing_group['signingGroupId'].wont_be_nil
287 | signing_group['groupName'].must_equal "sample_group"
288 | end
289 | end
290 | end
291 |
292 | describe "embedded signing" do
293 | before do
294 | # create the template dynamically
295 | VCR.use_cassette("create_template") do
296 | @template_response = @client.create_template(
297 | description: 'Cool Description',
298 | name: "Cool Template Name",
299 | signers: [
300 | {
301 | embedded: true,
302 | name: 'jon',
303 | email: 'someone@example.com',
304 | role_name: 'Issuer',
305 | sign_here_tabs: [
306 | {
307 | anchor_string: 'sign here',
308 | template_locked: true, #doesn't seem to do anything
309 | template_required: true, #doesn't seem to do anything
310 | email_notification: {supportedLanguage: 'en'} #FIXME if signer is setup as 'embedded' initial email notifications don't go out, but even when I set up a signer as non-embedded this setting didn't seem to make the email notifications actually stop...
311 | }
312 | ]
313 | }
314 | ],
315 | files: [
316 | {path: 'test.pdf', name: 'test.pdf'}
317 | ]
318 | )
319 | if ! @template_response["errorCode"].nil?
320 | puts "[API ERROR] (create_template) errorCode: '#{@template_response["errorCode"]}', message: '#{@template_response["message"]}'"
321 | end
322 | end
323 |
324 |
325 | # use the templateId to get the envelopeId
326 | VCR.use_cassette("create_envelope/from_template") do
327 | @envelope_response = @client.create_envelope_from_template(
328 | status: 'sent',
329 | email: {
330 | subject: "The test email subject envelope",
331 | body: "Envelope body content here"
332 | },
333 | template_id: @template_response["templateId"],
334 | signers: [
335 | {
336 | embedded: true,
337 | name: 'jon',
338 | email: 'someone@example.com',
339 | role_name: 'Issuer'
340 | }
341 | ]
342 | )
343 | if ! @envelope_response["errorCode"].nil?
344 | puts "[API ERROR] (create_envelope/from_template) errorCode: '#{@envelope_response["errorCode"]}', message: '#{@envelope_response["message"]}'"
345 | end
346 | end
347 | end
348 |
349 | it "should get a template" do
350 | VCR.use_cassette("get_template", record: :all) do
351 | response = @client.get_template(@template_response["templateId"])
352 | assert_equal @template_response["templateId"], response['envelopeTemplateDefinition']['templateId']
353 | end
354 | end
355 |
356 | it "should return a URL for embedded signing" do
357 | #ensure template was created
358 | @template_response["templateId"].wont_be_nil
359 | @template_response["name"].must_equal "Cool Template Name"
360 |
361 | #ensure creating an envelope from a dynamic template did not error
362 | @envelope_response["errorCode"].must_be_nil
363 |
364 | #return the URL for embedded signing
365 | VCR.use_cassette("get_recipient_view") do
366 | response = @client.get_recipient_view(
367 | envelope_id: @envelope_response["envelopeId"],
368 | name: 'jon',
369 | email: 'someone@example.com',
370 | return_url: 'http://google.com'
371 | )
372 | response['url'].must_match(/http/)
373 | end
374 | end
375 |
376 | #status return values = "sent", "delivered", "completed"
377 | it "should retrieve the envelope recipients status" do
378 | VCR.use_cassette("get_envelope_recipients") do
379 | response = @client.get_envelope_recipients(
380 | envelope_id: @envelope_response["envelopeId"],
381 | include_tabs: true,
382 | include_extended: true
383 | )
384 | response["signers"].wont_be_nil
385 | end
386 | end
387 |
388 | #status return values = "sent", "delivered", "completed"
389 | it "should retrieve the byte stream of the envelope doc from DocuSign" do
390 | VCR.use_cassette("get_document_from_envelope") do
391 | @client.get_document_from_envelope(
392 | envelope_id: @envelope_response["envelopeId"],
393 | document_id: 1,
394 | local_save_path: 'docusign_docs/file_name.pdf'
395 | )
396 | # NOTE manually check that this file has the content you'd expect
397 | end
398 | end
399 |
400 | it "should add signers to an envelope" do
401 | VCR.use_cassette("add_envelope_signers") do
402 | response = @client.add_envelope_signers(
403 | envelope_id: @envelope_response["envelopeId"],
404 | signers: [{
405 | email: "signer@example.com",
406 | name: "Signer Person",
407 | recipientId: 2,
408 | }],
409 | )
410 |
411 | response["recipientUpdateResults"].first["errorDetails"]["errorCode"]
412 | .must_equal "SUCCESS"
413 | end
414 | end
415 | it "should get envelope's sender view" do
416 | VCR.use_cassette("get_envelope_sender_view") do
417 | response = @client.get_sender_view(
418 | envelope_id: @envelope_response["envelopeId"],
419 | )
420 |
421 | response['errorCode'].must_be_nil
422 | response['url'].must_match(/http/)
423 | end
424 | end
425 | end
426 | end
427 | end
428 |
--------------------------------------------------------------------------------
/test/docusign_rest/configuration_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../helper'
2 |
3 | describe 'configuration' do
4 | after do
5 | DocusignRest.reset
6 | end
7 |
8 | describe '.configure' do
9 | DocusignRest::Configuration::VALID_CONFIG_KEYS.each do |key|
10 | it "should set the #{key}" do
11 | DocusignRest.configure do |config|
12 | config.send("#{key}=", key)
13 | DocusignRest.send(key).must_equal key
14 | end
15 | end
16 |
17 | describe '.key' do
18 | it "should return default value for #{key}" do
19 | DocusignRest.send(key).must_equal DocusignRest::Configuration.const_get("DEFAULT_#{key.upcase}")
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/docusign_rest/docusign_rest_test.rb:
--------------------------------------------------------------------------------
1 | require_relative '../helper'
2 |
3 | describe DocusignRest do
4 | it "should have a version" do
5 | DocusignRest::VERSION.wont_be_nil
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require_relative '../lib/docusign_rest'
2 | require 'minitest/spec'
3 | require 'minitest/autorun'
4 | require 'turn'
5 | require 'json'
6 | require 'vcr'
7 | require_relative 'docusign_login_config'
8 | require 'pry'
9 |
10 | VCR.configure do |c|
11 | c.cassette_library_dir = "test/fixtures/vcr"
12 | c.hook_into :webmock
13 | c.default_cassette_options = { record: :all }
14 |
15 | c.filter_sensitive_data('') do
16 | DocusignRest.password
17 | end
18 |
19 | c.filter_sensitive_data('') do
20 | DocusignRest.integrator_key
21 | end
22 |
23 | c.filter_sensitive_data('') do
24 | DocusignRest.username
25 | end
26 |
27 | c.filter_sensitive_data('') do
28 | DocusignRest.account_id
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jondkinney/docusign_rest/f93eaff7b649336ef54fe5310c4c00d74531e5e1/test2.pdf
--------------------------------------------------------------------------------