├── .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 --------------------------------------------------------------------------------