lib/docusign_rest/client.rb in docusign_rest-0.0.9 vs lib/docusign_rest/client.rb in docusign_rest-0.1.1

- old
+ new

@@ -1,5 +1,7 @@ +require 'openssl' + module DocusignRest class Client # Define the same set of accessors as the DocusignRest module attr_accessor *Configuration::VALID_CONFIG_KEYS @@ -15,46 +17,51 @@ send("#{key}=", merged_options[key]) end # Set up the DocuSign Authentication headers with the values passed from # our config block - @docusign_authentication_headers = { - "X-DocuSign-Authentication" => "" \ - "<DocuSignCredentials>" \ - "<Username>#{DocusignRest.username}</Username>" \ - "<Password>#{DocusignRest.password}</Password>" \ - "<IntegratorKey>#{DocusignRest.integrator_key}</IntegratorKey>" \ - "</DocuSignCredentials>" - } + if access_token.nil? + @docusign_authentication_headers = { + 'X-DocuSign-Authentication' => { + 'Username' => username, + 'Password' => password, + 'IntegratorKey' => integrator_key + }.to_json + } + else + @docusign_authentication_headers = { + 'Authorization' => "Bearer #{access_token}" + } + end # Set the account_id from the configure block if present, but can't call # the instance var @account_id because that'll override the attr_accessor # that is automatically configured for the configure block - @acct_id = DocusignRest.account_id + @acct_id = account_id end # Internal: sets the default request headers allowing for user overrides # via options[:headers] from within other requests. Additionally injects # the X-DocuSign-Authentication header to authorize the request. # # Client can pass in header options to any given request: - # headers: {"Some-Key" => "some/value", "Another-Key" => "another/value"} + # headers: {'Some-Key' => 'some/value', 'Another-Key' => 'another/value'} # # Then we pass them on to this method to merge them with the other # required headers # # Example: # # headers(options[:headers]) # # Returns a merged hash of headers overriding the default Accept header if - # the user passes in a new "Accept" header key and adds any other + # the user passes in a new 'Accept' header key and adds any other # user-defined headers along with the X-DocuSign-Authentication headers def headers(user_defined_headers={}) default = { - "Accept" => "application/json" #this seems to get added automatically, so I can probably remove this + 'Accept' => 'json' #this seems to get added automatically, so I can probably remove this } default.merge!(user_defined_headers) if user_defined_headers @docusign_authentication_headers.merge(default) @@ -66,34 +73,81 @@ # # url - a relative url requiring a leading forward slash # # Example: # - # build_uri("/login_information") + # build_uri('/login_information') # # Returns a parsed URI object def build_uri(url) - URI.parse("#{DocusignRest.endpoint}/#{DocusignRest.api_version}#{url}") + URI.parse("#{endpoint}/#{api_version}#{url}") end # Internal: configures Net:HTTP with some default values that are required # for every request to the DocuSign API # # Returns a configured Net::HTTP object into which a request can be passed def initialize_net_http_ssl(uri) http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - # Explicitly verifies that the certificate matches the domain. Requires - # that we use www when calling the production DocuSign API - http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.use_ssl = uri.scheme == 'https' + if defined?(Rails) && Rails.env.test? + in_rails_test_env = true + else + in_rails_test_env = false + end + + if http.use_ssl? && !in_rails_test_env + if ca_file + if File.exists?(ca_file) + http.ca_file = ca_file + else + raise 'Certificate path not found.' + end + end + + # Explicitly verifies that the certificate matches the domain. + # Requires that we use www when calling the production DocuSign API + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.verify_depth = 5 + else + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + http end + # Public: creates an OAuth2 authorization server token endpoint. + # + # email - email of user authenticating + # password - password of user authenticating + # + # Examples: + # + # client = DocusignRest::Client.new + # response = client.get_token('someone@example.com', 'p@ssw0rd01') + # + # Returns: + # access_token - Access token information + # scope - This should always be "api" + # token_type - This should always be "bearer" + def get_token(account_id, email, password) + content_type = { 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json' } + uri = build_uri('/oauth2/token') + + request = Net::HTTP::Post.new(uri.request_uri, content_type) + request.body = "grant_type=password&client_id=#{integrator_key}&username=#{email}&password=#{password}&scope=api" + + http = initialize_net_http_ssl(uri) + response = http.request(request) + JSON.parse(response.body) + end + + # Public: gets info necessary to make additional requests to the DocuSign API # # options - hash of headers if the client wants to override something # # Examples: @@ -109,11 +163,11 @@ # isDefault - # TODO identify what this is # name - The account name provided when signing up for DocuSign # userId - # TODO determine what this is used for, if anything # userName - Full name provided when signing up for DocuSign def get_login_information(options={}) - uri = build_uri("/login_information") + uri = build_uri('/login_information') request = Net::HTTP::Get.new(uri.request_uri, headers(options[:headers])) http = initialize_net_http_ssl(uri) http.request(request) end @@ -128,55 +182,94 @@ # which includes the account_id in it. That way we don't require hitting # the /login_information URI in normal requests # # Returns the accountId string def get_account_id - unless @acct_id + unless acct_id response = get_login_information.body hashed_response = JSON.parse(response) login_accounts = hashed_response['loginAccounts'] - @acct_id ||= login_accounts.first['accountId'] + acct_id ||= login_accounts.first['accountId'] end - @acct_id + acct_id end - def check_embedded_signer(embedded, email) - if embedded && embedded == true - "\"clientUserId\" : \"#{email}\"," - end - end - - # Internal: takes in an array of hashes of signers and concatenates all the # hashes with commas # # embedded - Tells DocuSign if this is an embedded signer which determines # weather or not to deliver emails. Also lets us authenticate # them when they go to do embedded signing. Behind the scenes # this is setting the clientUserId value to the signer's email. # name - The name of the signer # email - The email of the signer # role_name - The role name of the signer ('Attorney', 'Client', etc.). + # tabs - Array of tab pairs grouped by type (Example type: 'textTabs') + # { textTabs: [ { tabLabel: "label", name: "name", value: "value" } ] } # + # NOTE: The 'tabs' option is NOT supported in 'v1' of the REST API + # # Returns a hash of users that need to be embedded in the template to # create an envelope def get_template_roles(signers) template_roles = [] signers.each_with_index do |signer, index| - template_roles << "{ - #{check_embedded_signer(signer[:embedded], signer[:email])} - \"name\" : \"#{signer[:name]}\", - \"email\" : \"#{signer[:email]}\", - \"roleName\" : \"#{signer[:role_name]}\" - }" + template_role = { + name: signer[:name], + email: signer[:email], + roleName: signer[:role_name], + tabs: { + textTabs: get_signer_tabs(signer[:text_tabs]), + checkboxTabs: get_signer_tabs(signer[:checkbox_tabs]) + } + } + + if signer[:email_notification] + template_role[:emailNotification] = signer[:email_notification] + end + + template_role['clientUserId'] = (signer[:client_id] || signer[:email]).to_s if signer[:embedded] == true + template_roles << template_role end - template_roles.join(",") + template_roles end + # TODO (2014-02-03) jonk => document + def get_signer_tabs(tabs) + Array(tabs).map do |tab| + { + 'tabLabel' => tab[:label], + 'name' => tab[:name], + 'value' => tab[:value], + 'documentId' => tab[:document_id], + 'selected' => tab[:selected] + } + end + end + + + # TODO (2014-02-03) jonk => document + def get_event_notification(event_notification) + return {} unless event_notification + { + useSoapInterface: event_notification[:use_soap_interface] || false, + includeCertificatWithSoap: event_notification[:include_certificate_with_soap] || false, + url: event_notification[:url], + loggingEnabled: event_notification[:logging], + 'EnvelopeEvents' => Array(event_notification[:envelope_events]).map do |envelope_event| + { + includeDocuments: envelope_event[:include_documents] || false, + envelopeEventStatusCode: envelope_event[:envelope_event_status_code] + } + end + } + end + + # Internal: takes an array of hashes of signers required to complete a # document and allows for setting several options. Not all options are # currently dynamic but that's easy to change/add which I (and I'm # sure others) will be doing in the future. # @@ -205,106 +298,128 @@ # currently work. # sign_here_tab_text - Instead of 'sign here'. Note: doesn't work # tab_label - TODO: figure out what this is def get_signers(signers, options={}) doc_signers = [] + signers.each_with_index do |signer, index| - # Build up a string with concatenation so that we can append the full - # string to the doc_signers array as the last step in this block - doc_signer = "" - doc_signer << "{ - \"email\":\"#{signer[:email]}\", - \"name\":\"#{signer[:name]}\", - \"accessCode\":\"\", - \"addAccessCodeToEmail\":false, - #{check_embedded_signer(signer[:embedded], signer[:email])} - \"customFields\":null, - \"emailNotification\":#{signer[:email_notification] || 'null'}, - \"iDCheckConfigurationName\":null, - \"iDCheckInformationInput\":null, - \"inheritEmailNotificationConfiguration\":false, - \"note\":\"\", - \"phoneAuthentication\":null, - \"recipientAttachment\":null, - \"recipientId\":\"#{index+1}\", - \"requireIdLookup\":false, - \"roleName\":\"#{signer[:role_name]}\", - \"routingOrder\":#{index+1}, - \"socialAuthentications\":null, - " + doc_signer = { + email: signer[:email], + name: signer[:name], + accessCode: '', + addAccessCodeToEmail: false, + customFields: nil, + iDCheckConfigurationName: nil, + iDCheckInformationInput: nil, + inheritEmailNotificationConfiguration: false, + note: '', + phoneAuthentication: nil, + recipientAttachment: nil, + recipientId: "#{index + 1}", + requireIdLookup: false, + roleName: signer[:role_name], + routingOrder: index + 1, + socialAuthentications: nil + } + if signer[:email_notification] + doc_signer[:emailNotification] = signer[:email_notification] + end + + if signer[:embedded] + doc_signer[:clientUserId] = signer[:client_id] || signer[:email] + end + if options[:template] == true - doc_signer << " - \"templateAccessCodeRequired\":false, - \"templateLocked\":#{signer[:template_locked] || true}, - \"templateRequired\":#{signer[:template_required] || true}, - " + doc_signer[:templateAccessCodeRequired] = false + doc_signer[:templateLocked] = signer[:template_locked].nil? ? true : signer[:template_locked] + doc_signer[:templateRequired] = signer[:template_required].nil? ? true : signer[:template_required] end - doc_signer << " - \"autoNavigation\":false, - \"defaultRecipient\":false, - \"signatureInfo\":null, - \"tabs\":{ - \"approveTabs\":null, - \"checkboxTabs\":null, - \"companyTabs\":null, - \"dateSignedTabs\":null, - \"dateTabs\":null, - \"declineTabs\":null, - \"emailTabs\":null, - \"envelopeIdTabs\":null, - \"fullNameTabs\":null, - \"initialHereTabs\":null, - \"listTabs\":null, - \"noteTabs\":null, - \"numberTabs\":null, - \"radioGroupTabs\":null, - \"signHereTabs\":[ - " - signer[:sign_here_tabs].each do |sign_here_tab| - doc_signer << "{ - \"anchorString\":\"#{sign_here_tab[:anchor_string]}\", - \"anchorXOffset\": \"#{sign_here_tab[:anchor_x_offset] || '0'}\", - \"anchorYOffset\": \"#{sign_here_tab[:anchor_y_offset] || '0'}\", - \"anchorIgnoreIfNotPresent\": #{sign_here_tab[:ignore_anchor_if_not_present] || false}, - \"anchorUnits\": \"pixels\", - \"conditionalParentLabel\": null, - \"conditionalParentValue\": null, - \"documentId\":\"#{sign_here_tab[:document_id] || '1'}\", - \"pageNumber\":\"#{sign_here_tab[:page_number] || '1'}\", - \"recipientId\":\"#{index+1}\", - " - if options[:template] == true - doc_signer << " - \"templateLocked\":#{sign_here_tab[:template_locked] || true}, - \"templateRequired\":#{sign_here_tab[:template_required] || true}, - " - end - doc_signer << " - \"xPosition\":\"#{sign_here_tab[:x_position] || '0'}\", - \"yPosition\":\"#{sign_here_tab[:y_position] || '0'}\", - \"name\":\"#{sign_here_tab[:sign_here_tab_text] || 'Sign Here'}\", - \"optional\":false, - \"scaleValue\":1, - \"tabLabel\":\"#{sign_here_tab[:tab_label] || 'Signature 1'}\" - }," - end - doc_signer << "], - \"signerAttachmentTabs\":null, - \"ssnTabs\":null, - \"textTabs\":null, - \"titleTabs\":null, - \"zipTabs\":null - } - }" + doc_signer[:autoNavigation] = false + doc_signer[:defaultRecipient] = false + doc_signer[:signatureInfo] = nil + doc_signer[:tabs] = { + approveTabs: nil, + checkboxTabs: get_tabs(signer[:checkbox_tabs], options, index), + companyTabs: nil, + dateSignedTabs: get_tabs(signer[:date_signed_tabs], options, index), + dateTabs: nil, + declineTabs: nil, + emailTabs: get_tabs(signer[:email_tabs], options, index), + envelopeIdTabs: nil, + fullNameTabs: get_tabs(signer[:full_name_tabs], options, index), + listTabs: get_tabs(signer[:list_tabs], options, index), + noteTabs: nil, + numberTabs: nil, + radioGroupTabs: nil, + initialHereTabs: get_tabs(signer[:initial_here_tabs], options.merge!(initial_here_tab: true), index), + signHereTabs: get_tabs(signer[:sign_here_tabs], options.merge!(sign_here_tab: true), index), + signerAttachmentTabs: nil, + ssnTabs: nil, + textTabs: get_tabs(signer[:text_tabs], options, index), + titleTabs: get_tabs(signer[:title_tabs], options, index), + zipTabs: nil + } + # append the fully build string to the array doc_signers << doc_signer end - doc_signers.join(",") + doc_signers end + + # TODO (2014-02-03) jonk => document + def get_tabs(tabs, options, index) + tab_array = [] + + Array(tabs).map do |tab| + tab_hash = {} + + if tab[:anchor_string] + tab_hash[:anchorString] = tab[:anchor_string] + tab_hash[:anchorXOffset] = tab[:anchor_x_offset] || '0' + tab_hash[:anchorYOffset] = tab[:anchor_y_offset] || '0' + tab_hash[:anchorIgnoreIfNotPresent] = tab[:ignore_anchor_if_not_present] || false + tab_hash[:anchorUnits] = 'pixels' + end + + tab_hash[:conditionalParentLabel] = nil + tab_hash[:conditionalParentValue] = nil + tab_hash[:documentId] = tab[:document_id] || '1' + tab_hash[:pageNumber] = tab[:page_number] || '1' + tab_hash[:recipientId] = index + 1 + tab_hash[:required] = tab[:required] || false + + if options[:template] == true + tab_hash[:templateLocked] = tab[:template_locked].nil? ? true : tab[:template_locked] + tab_hash[:templateRequired] = tab[:template_required].nil? ? true : tab[:template_required] + end + + if options[:sign_here_tab] == true || options[:initial_here_tab] == true + tab_hash[:scaleValue] = tab_hash[:scaleValue] || 1 + end + + tab_hash[:xPosition] = tab[:x_position] || '0' + tab_hash[:yPosition] = tab[:y_position] || '0' + tab_hash[:name] = tab[:name] if tab[:name] + tab_hash[:optional] = false + tab_hash[:tabLabel] = tab[:label] || 'Signature 1' + tab_hash[:width] = tab[:width] if tab[:width] + tab_hash[:height] = tab[:height] if tab[:width] + tab_hash[:value] = tab[:value] if tab[:value] + + tab_hash[:locked] = tab[:locked] || false + + tab_hash[:list_items] = tab[:list_items] if tab[:list_items] + + tab_array << tab_hash + end + tab_array + end + + # Internal: sets up the file ios array # # files - a hash of file params # # Returns the properly formatted ios used to build the file_params hash @@ -324,25 +439,25 @@ # uploading directly from a form in a framework, which often save the file to # an arbitrarily named RackMultipart file in /tmp). # # Usage: # - # UploadIO.new("file.txt", "text/plain") - # UploadIO.new(file_io, "text/plain", "file.txt") + # UploadIO.new('file.txt', 'text/plain') + # UploadIO.new(file_io, 'text/plain', 'file.txt') # ******************************************************************** # # There is also a 4th undocumented argument, opts={}, which allows us # to send in not only the Content-Disposition of 'file' as required by # DocuSign, but also the documentId parameter which is required as well # ios = [] files.each_with_index do |file, index| ios << UploadIO.new( file[:io] || file[:path], - file[:content_type] || "application/pdf", + file[:content_type] || 'application/pdf', file[:name], - "Content-Disposition" => "file; documentid=#{index+1}" + 'Content-Disposition' => "file; documentid=#{index + 1}" ) end ios end @@ -355,29 +470,27 @@ # post request def create_file_params(ios) # multi-doc uploading capabilities, each doc needs to be it's own param file_params = {} ios.each_with_index do |io,index| - file_params.merge!("file#{index+1}" => io) + file_params.merge!("file#{index + 1}" => io) end file_params end # Internal: takes in an array of hashes of documents and calculates the - # documentId then concatenates all the hashes with commas + # documentId # # Returns a hash of documents that are to be uploaded def get_documents(ios) - documents = [] - ios.each_with_index do |io, index| - documents << "{ - \"documentId\" : \"#{index+1}\", - \"name\" : \"#{io.original_filename}\" - }" + ios.each_with_index.map do |io, index| + { + documentId: "#{index + 1}", + name: io.original_filename + } end - documents.join(",") end # Internal sets up the Net::HTTP request # @@ -396,16 +509,16 @@ # headers={} - The fully merged, final request headers # boundary - Optional: you can give the request a custom boundary # request = Net::HTTP::Post::Multipart.new( uri.request_uri, - {post_body: post_body}.merge(file_params), + { post_body: post_body }.merge(file_params), headers ) # DocuSign requires that we embed the document data in the body of the - # JSON request directly so we need to call ".read" on the multipart-post + # JSON request directly so we need to call '.read' on the multipart-post # provided body_stream in order to serialize all the files into a # compatible JSON string. request.body = request.body_stream.read request end @@ -440,32 +553,30 @@ # uri - The relative envelope uri def create_envelope_from_document(options={}) ios = create_file_ios(options[:files]) file_params = create_file_params(ios) - post_body = "{ - \"emailBlurb\" : \"#{options[:email][:body] if options[:email]}\", - \"emailSubject\" : \"#{options[:email][:subject] if options[:email]}\", - \"documents\" : [#{get_documents(ios)}], - \"recipients\" : { - \"signers\" : [#{get_signers(options[:signers])}] + post_body = { + emailBlurb: "#{options[:email][:body] if options[:email]}", + emailSubject: "#{options[:email][:subject] if options[:email]}", + documents: get_documents(ios), + recipients: { + signers: get_signers(options[:signers]) }, - \"status\" : \"#{options[:status]}\" - } - " + status: "#{options[:status]}" + }.to_json - uri = build_uri("/accounts/#{@acct_id}/envelopes") + uri = build_uri("/accounts/#{acct_id}/envelopes") http = initialize_net_http_ssl(uri) request = initialize_net_http_multipart_post_request( uri, post_body, file_params, headers(options[:headers]) ) - # Finally do the Net::HTTP request! response = http.request(request) - parsed_response = JSON.parse(response.body) + JSON.parse(response.body) end # Public: allows a template to be dynamically created with several options. # @@ -497,41 +608,52 @@ # Uri - the URI where the template is located on the DocuSign servers def create_template(options={}) ios = create_file_ios(options[:files]) file_params = create_file_params(ios) - post_body = "{ - \"emailBlurb\" : \"#{options[:email][:body] if options[:email]}\", - \"emailSubject\" : \"#{options[:email][:subject] if options[:email]}\", - \"documents\" : [#{get_documents(ios)}], - \"recipients\" : { - \"signers\" : [#{get_signers(options[:signers], template: true)}] + post_body = { + emailBlurb: "#{options[:email][:body] if options[:email]}", + emailSubject: "#{options[:email][:subject] if options[:email]}", + documents: get_documents(ios), + recipients: { + signers: get_signers(options[:signers], template: true) }, - \"envelopeTemplateDefinition\" : { - \"description\" : \"#{options[:description]}\", - \"name\" : \"#{options[:name]}\", - \"pageCount\" : 1, - \"password\" : \"\", - \"shared\" : false + envelopeTemplateDefinition: { + description: options[:description], + name: options[:name], + pageCount: 1, + password: '', + shared: false } - } - " + }.to_json - uri = build_uri("/accounts/#{@acct_id}/templates") - + uri = build_uri("/accounts/#{acct_id}/templates") http = initialize_net_http_ssl(uri) request = initialize_net_http_multipart_post_request( uri, post_body, file_params, headers(options[:headers]) ) - # Finally do the Net::HTTP request! response = http.request(request) - parsed_response = JSON.parse(response.body) + JSON.parse(response.body) end + # TODO (2014-02-03) jonk => document + def get_template(template_id, options = {}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + uri = build_uri("/accounts/#{acct_id}/templates/#{template_id}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) + response = http.request(request) + JSON.parse(response.body) + end + + # Public: create an envelope for delivery from a template # # headers - Optional hash of headers to merge into the existing # required headers for a POST request. # status - Options include: 'sent', 'created', 'voided' and @@ -551,33 +673,56 @@ # Returns a JSON parsed response body containing the envelope's: # name - Name given above # templateId - The auto-generated ID provided by DocuSign # Uri - the URI where the template is located on the DocuSign servers def create_envelope_from_template(options={}) - content_type = {'Content-Type' => 'application/json'} + content_type = { 'Content-Type' => 'application/json' } content_type.merge(options[:headers]) if options[:headers] - post_body = "{ - \"status\" : \"#{options[:status]}\", - \"emailBlurb\" : \"#{options[:email][:body]}\", - \"emailSubject\" : \"#{options[:email][:subject]}\", - \"templateId\" : \"#{options[:template_id]}\", - \"templateRoles\" : [#{get_template_roles(options[:signers])}], - }" + post_body = { + status: options[:status], + emailBlurb: options[:email][:body], + emailSubject: options[:email][:subject], + templateId: options[:template_id], + eventNotification: get_event_notification(options[:event_notification]), + templateRoles: get_template_roles(options[:signers]) + }.to_json - uri = build_uri("/accounts/#{@acct_id}/envelopes") + uri = build_uri("/accounts/#{acct_id}/envelopes") http = initialize_net_http_ssl(uri) request = Net::HTTP::Post.new(uri.request_uri, headers(content_type)) request.body = post_body response = http.request(request) - parsed_response = JSON.parse(response.body) + JSON.parse(response.body) end + # Public returns the names specified for a given email address (existing docusign user) + # + # email - the email of the recipient + # headers - optional hash of headers to merge into the existing + # required headers for a multipart request. + # + # Returns the list of names + def get_recipient_names(options={}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + uri = build_uri("/accounts/#{acct_id}/recipient_names?email=#{options[:email]}") + + http = initialize_net_http_ssl(uri) + + request = Net::HTTP::Post.new(uri.request_uri, headers(content_type)) + + response = http.request(request) + JSON.parse(response.body) + end + + # Public returns the URL for embedded signing # # envelope_id - the ID of the envelope you wish to use for embedded signing # name - the name of the signer # email - the email of the recipient @@ -586,33 +731,62 @@ # headers - optional hash of headers to merge into the existing # required headers for a multipart request. # # Returns the URL string for embedded signing (can be put in an iFrame) def get_recipient_view(options={}) - content_type = {'Content-Type' => 'application/json'} + content_type = { 'Content-Type' => 'application/json' } content_type.merge(options[:headers]) if options[:headers] - post_body = "{ - \"authenticationMethod\" : \"email\", - \"clientUserId\" : \"#{options[:email]}\", - \"email\" : \"#{options[:email]}\", - \"returnUrl\" : \"#{options[:return_url]}\", - \"userName\" : \"#{options[:name]}\", - }" + post_body = { + authenticationMethod: 'email', + clientUserId: options[:client_id] || options[:email], + email: options[:email], + returnUrl: options[:return_url], + userName: options[:name] + }.to_json - uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/views/recipient") + uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/views/recipient") http = initialize_net_http_ssl(uri) request = Net::HTTP::Post.new(uri.request_uri, headers(content_type)) request.body = post_body response = http.request(request) + JSON.parse(response.body) + end + + + # Public returns the URL for embedded console + # + # envelope_id - the ID of the envelope you wish to use for embedded signing + # headers - optional hash of headers to merge into the existing + # required headers for a multipart request. + # + # Returns the URL string for embedded console (can be put in an iFrame) + def get_console_view(options={}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + post_body = { + envelopeId: options[:envelope_id] + }.to_json + + uri = build_uri("/accounts/#{acct_id}/views/console") + + http = initialize_net_http_ssl(uri) + + request = Net::HTTP::Post.new(uri.request_uri, headers(content_type)) + request.body = post_body + + response = http.request(request) + parsed_response = JSON.parse(response.body) - parsed_response["url"] + parsed_response['url'] end + # Public returns the envelope recipients for a given envelope # # include_tabs - boolean, determines if the tabs for each signer will be # returned in the response, defaults to false. # envelope_id - ID of the envelope for which you want to retrive the @@ -621,23 +795,68 @@ # required headers for a multipart request. # # Returns a hash of detailed info about the envelope including the signer # hash and status of each signer def get_envelope_recipients(options={}) - content_type = {'Content-Type' => 'application/json'} + content_type = { 'Content-Type' => 'application/json' } content_type.merge(options[:headers]) if options[:headers] include_tabs = options[:include_tabs] || false include_extended = options[:include_extended] || false - uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/recipients?include_tabs=#{include_tabs}&include_extended=#{include_extended}") + uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/recipients?include_tabs=#{include_tabs}&include_extended=#{include_extended}") http = initialize_net_http_ssl(uri) request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) response = http.request(request) - parsed_response = JSON.parse(response.body) + JSON.parse(response.body) end + + # Public retrieves the envelope status + # + # envelope_id - ID of the envelope from which the doc will be retrieved + def get_envelope_status(options={}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) + response = http.request(request) + JSON.parse(response.body) + end + + + # Public retrieves the statuses of envelopes matching the given query + # + # from_date - Docusign formatted Date/DateTime. Only return items after this date. + # + # to_date - Docusign formatted Date/DateTime. Only return items up to this date. + # Defaults to the time of the call. + # + # from_to_status - The status of the envelope checked for in the from_date - to_date period. + # Defaults to 'changed' + # + # status - The current status of the envelope. Defaults to any status. + # + # Returns an array of hashes containing envelope statuses, ids, and similar information. + def get_envelope_statuses(options={}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + query_params = options.slice(:from_date, :to_date, :from_to_status, :status) + uri = build_uri("/accounts/#{acct_id}/envelopes?#{query_params.to_query}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) + response = http.request(request) + + JSON.parse(response.body) + end + + # Public retrieves the attached file from a given envelope # # envelope_id - ID of the envelope from which the doc will be retrieved # document_id - ID of the document to retrieve # local_save_path - Local absolute path to save the doc to including the @@ -646,33 +865,207 @@ # required headers for a multipart request. # # Example # # client.get_document_from_envelope( - # envelope_id: @envelope_response["envelopeId"], + # envelope_id: @envelope_response['envelopeId'], # document_id: 1, - # local_save_path: 'docusign_docs/file_name.pdf' + # local_save_path: 'docusign_docs/file_name.pdf', + # return_stream: true/false # will return the bytestream instead of saving doc to file system. # ) # # Returns the PDF document as a byte stream. def get_document_from_envelope(options={}) - content_type = {'Content-Type' => 'application/json'} + content_type = { 'Content-Type' => 'application/json' } content_type.merge(options[:headers]) if options[:headers] - uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/documents/#{options[:document_id]}") + uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/documents/#{options[:document_id]}") http = initialize_net_http_ssl(uri) request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) response = http.request(request) + return response.body if options[:return_stream] split_path = options[:local_save_path].split('/') split_path.pop #removes the document name and extension from the array path = split_path.join("/") #rejoins the array to form path to the folder that will contain the file FileUtils.mkdir_p(path) File.open(options[:local_save_path], 'wb') do |output| output << response.body end end - end + + # Public retrieves the document infos from a given envelope + # + # envelope_id - ID of the envelope from which document infos are to be retrieved + # + # Returns a hash containing the envelopeId and the envelopeDocuments array + def get_documents_from_envelope(options={}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + uri = build_uri("/accounts/#{acct_id}/envelopes/#{options[:envelope_id]}/documents") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) + response = http.request(request) + + JSON.parse(response.body) + end + + + # Public moves the specified envelopes to the given folder + # + # envelope_ids - IDs of the envelopes to be moved + # folder_id - ID of the folder to move the envelopes to + # headers - Optional hash of headers to merge into the existing + # required headers for a multipart request. + # + # Example + # + # client.move_envelope_to_folder( + # envelope_ids: ["xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"] + # folder_id: "xxxxx-2222xxxxx", + # ) + # + # Returns the response. + def move_envelope_to_folder(options = {}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + post_body = { + envelopeIds: options[:envelope_ids] + }.to_json + + uri = build_uri("/accounts/#{acct_id}/folders/#{options[:folder_id]}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Put.new(uri.request_uri, headers(content_type)) + request.body = post_body + response = http.request(request) + + response + end + + + # Public retrieves the envelope(s) from a specific folder based on search params. + # + # Option Query Terms(none are required): + # query_params: + # 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. + # 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. + # to_date: date/Time Only return items up to this date. If no value is provided, the default search is to the current date. + # 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. + # status: Status The current status of the envelope. If no value is provided, the default search is all/any status. + # owner_name: username The name of the folder owner. + # owner_email: email The email of the folder owner. + # + # Example + # + # client.search_folder_for_envelopes( + # folder_id: xxxxx-2222xxxxx, + # query_params: { + # search_text: "John Appleseed", + # from_date: '7-1-2011+11:00:00+AM', + # to_date: '7-1-2011+11:00:00+AM', + # status: "completed" + # } + # ) + # + def search_folder_for_envelopes(options={}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + q ||= [] + options[:query_params].each do |key, val| + q << "#{key}=#{val}" + end + + uri = build_uri("/accounts/#{@acct_id}/folders/#{options[:folder_id]}/?#{q.join('&')}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) + response = http.request(request) + JSON.parse(response.body) + end + + + # TODO (2014-02-03) jonk => document + def create_account(options) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + uri = build_uri('/accounts') + + post_body = convert_hash_keys(options).to_json + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Post.new(uri.request_uri, headers(content_type)) + request.body = post_body + response = http.request(request) + JSON.parse(response.body) + end + + + # TODO (2014-02-03) jonk => document + def convert_hash_keys(value) + case value + when Array + value.map { |v| convert_hash_keys(v) } + when Hash + Hash[value.map { |k, v| [k.to_s.camelize(:lower), convert_hash_keys(v)] }] + else + value + end + end + + + # TODO (2014-02-03) jonk => document + def delete_account(account_id, options = {}) + content_type = { 'Content-Type' => 'application/json' } + content_type.merge(options[:headers]) if options[:headers] + + uri = build_uri("/accounts/#{account_id}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Delete.new(uri.request_uri, headers(content_type)) + response = http.request(request) + json = response.body + json = '{}' if json.nil? || json == '' + JSON.parse(json) + end + + + # Public: Retrieves a list of available templates + # + # Example + # + # client.get_templates() + # + # Returns a list of the available templates. + def get_templates + uri = build_uri("/accounts/#{acct_id}/templates") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers({ 'Content-Type' => 'application/json' })) + JSON.parse(http.request(request).body) + end + + + # Grabs envelope data. + # Equivalent to the following call in the API explorer: + # Get Envelopev2/accounts/:accountId/envelopes/:envelopeId + # + # envelope_id- DS id of envelope to be retrieved. + def get_envelope(envelope_id) + content_type = { 'Content-Type' => 'application/json' } + uri = build_uri("/accounts/#{acct_id}/envelopes/#{envelope_id}") + + http = initialize_net_http_ssl(uri) + request = Net::HTTP::Get.new(uri.request_uri, headers(content_type)) + response = http.request(request) + JSON.parse(response.body) + end + end end