autentique 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Models
5
+ class DocumentInput
6
+ attr_accessor :name, :message, :reminder, :sortable, :footer, :refusable,
7
+ :qualified, :scrolling_required, :stop_on_rejected,
8
+ :new_signature_style, :show_audit_page, :ignore_cpf,
9
+ :ignore_birthdate, :email_template_id, :deadline_at,
10
+ :cc, :expiration, :configs, :locale
11
+
12
+ def initialize(attributes = {})
13
+ attributes.each do |key, value|
14
+ send("#{key}=", value) if respond_to?("#{key}=")
15
+ end
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ name: name,
21
+ message: message,
22
+ reminder: reminder,
23
+ sortable: sortable,
24
+ footer: footer,
25
+ refusable: refusable,
26
+ qualified: qualified,
27
+ scrolling_required: scrolling_required,
28
+ stop_on_rejected: stop_on_rejected,
29
+ new_signature_style: new_signature_style,
30
+ show_audit_page: show_audit_page,
31
+ ignore_cpf: ignore_cpf,
32
+ ignore_birthdate: ignore_birthdate,
33
+ email_template_id: email_template_id,
34
+ deadline_at: deadline_at,
35
+ cc: cc,
36
+ expiration: expiration,
37
+ configs: configs,
38
+ locale: locale
39
+ }.compact
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Models
5
+ class Signature
6
+ attr_reader :public_id, :name, :email, :created_at, :action, :link,
7
+ :user, :viewed, :signed, :rejected, :email_events, :delivery_method
8
+
9
+ def initialize(attributes = {})
10
+ @public_id = attributes['public_id']
11
+ @name = attributes['name']
12
+ @email = attributes['email']
13
+ @created_at = attributes['created_at']
14
+ @action = attributes['action']
15
+ @link = attributes['link']
16
+ @user = attributes['user']
17
+ @viewed = attributes['viewed']
18
+ @signed = attributes['signed']
19
+ @rejected = attributes['rejected']
20
+ @email_events = attributes['email_events']
21
+ @delivery_method = attributes['delivery_method']
22
+ end
23
+
24
+ def signed?
25
+ !@signed.nil?
26
+ end
27
+
28
+ def rejected?
29
+ !@rejected.nil?
30
+ end
31
+
32
+ def pending?
33
+ @signed.nil? && @rejected.nil?
34
+ end
35
+
36
+ def short_link
37
+ @link&.dig('short_link')
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Models
5
+ class SignerInput
6
+ attr_accessor :email, :phone, :name, :action, :delivery_method,
7
+ :configs, :security_verifications, :positions
8
+
9
+ def initialize(attributes = {})
10
+ attributes.each do |key, value|
11
+ send("#{key}=", value) if respond_to?("#{key}=")
12
+ end
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ email: email,
18
+ phone: phone,
19
+ name: name,
20
+ action: action,
21
+ delivery_method: delivery_method,
22
+ configs: configs,
23
+ security_verifications: security_verifications,
24
+ positions: positions
25
+ }.compact
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ class Documents
6
+ module Create
7
+ def create(file:, document:, signers:, organization_id: nil, folder_id: nil, sandbox: nil) # rubocop:disable Metrics/ParameterLists
8
+ doc_input = document.is_a?(Models::DocumentInput) ? document : Models::DocumentInput.new(document)
9
+ signer_inputs = signers.map { |s| s.is_a?(Models::SignerInput) ? s : Models::SignerInput.new(s) }
10
+
11
+ use_sandbox = sandbox.nil? ? client.sandbox : sandbox
12
+ if use_sandbox
13
+ doc_hash = doc_input.to_h.merge(sandbox: true)
14
+ doc_input = Models::DocumentInput.new(doc_hash)
15
+ end
16
+
17
+ response = upload_document(
18
+ file: file,
19
+ document: doc_input,
20
+ signers: signer_inputs,
21
+ organization_id: organization_id,
22
+ folder_id: folder_id
23
+ )
24
+
25
+ document_data = response.dig('data', 'createDocument')
26
+ raise QueryError.new('Failed to create document', response['errors']) if document_data.nil?
27
+
28
+ Models::Document.new(document_data)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ class Documents
6
+ module Delete
7
+ # Delete a document
8
+ #
9
+ # @param id [String] The document ID (UUID)
10
+ # @return [Boolean]
11
+ def delete(id)
12
+ query = client.graphql_client.parse <<-GRAPHQL
13
+ mutation($id: UUID!) {
14
+ deleteDocument(id: $id)
15
+ }
16
+ GRAPHQL
17
+
18
+ result = client.query(query, variables: { id: id })
19
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
20
+
21
+ result.data.delete_document
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ class Documents
6
+ module Find
7
+ def find(id) # rubocop:disable Metrics/MethodLength
8
+ query = client.graphql_client.parse <<-GRAPHQL
9
+ query($id: UUID!) {
10
+ document(id: $id) {
11
+ id
12
+ name
13
+ refusable
14
+ sortable
15
+ created_at
16
+ files { original signed }
17
+ signatures {
18
+ public_id
19
+ name
20
+ email
21
+ created_at
22
+ action { name }
23
+ link { short_link }
24
+ user { id name email }
25
+ viewed { created_at }
26
+ signed { created_at }
27
+ rejected { created_at reason }
28
+ email_events { sent opened delivered refused reason }
29
+ }
30
+ }
31
+ }
32
+ GRAPHQL
33
+
34
+ result = client.query(query, variables: { id: id })
35
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
36
+
37
+ document_data = result.data.document
38
+ raise NotFoundError, "Document with ID #{id} not found" if document_data.nil?
39
+
40
+ Models::Document.new(document_data.to_h)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ class Documents
6
+ module List
7
+ def list(status: nil, limit: 60, page: 1) # rubocop:disable Metrics/MethodLength
8
+ query_string = if status
9
+ <<-GRAPHQL
10
+ query($status: DocumentStatusEnum, $limit: Int!, $page: Int!) {
11
+ documents(status: $status, limit: $limit, page: $page) {
12
+ total
13
+ data { id name created_at signatures { public_id name email } }
14
+ }
15
+ }
16
+ GRAPHQL
17
+ else
18
+ <<-GRAPHQL
19
+ query($limit: Int, $page: Int) {
20
+ documents(limit: $limit, page: $page) {
21
+ total
22
+ data { id name created_at signatures { public_id name email } }
23
+ }
24
+ }
25
+ GRAPHQL
26
+ end
27
+
28
+ query = client.graphql_client.parse(query_string)
29
+ variables = { limit: limit, page: page }
30
+ variables[:status] = status if status
31
+
32
+ result = client.query(query, variables: variables)
33
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
34
+
35
+ docs = result.data.documents&.data || []
36
+ docs.map { |doc| Models::Document.new(doc.to_h) }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ class Documents
6
+ module Pending
7
+ def self.pending_query(client)
8
+ client.graphql_client.parse <<~GRAPHQL
9
+ query($limit: Int!, $page: Int!) {
10
+ documents(status: PENDING, limit: $limit, page: $page) {
11
+ total
12
+ data {
13
+ id
14
+ name
15
+ created_at
16
+ signatures {
17
+ public_id
18
+ name
19
+ email
20
+ user { id name email phone }
21
+ delivery_method
22
+ email_events { sent opened delivered refused reason }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ GRAPHQL
28
+ end
29
+
30
+ def pending(limit: 60, page: 1)
31
+ query = Pending.pending_query(client)
32
+ result = client.query(query, variables: { limit: limit, page: page })
33
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
34
+
35
+ result.data.documents&.data&.map { |doc| Models::Document.new(doc.to_h) } || []
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'mime/types'
6
+
7
+ require_relative 'documents/create'
8
+ require_relative 'documents/find'
9
+ require_relative 'documents/pending'
10
+ require_relative 'documents/list'
11
+ require_relative 'documents/delete'
12
+
13
+ module Autentique
14
+ module Resources
15
+ class Documents
16
+ include Documents::Create
17
+ include Documents::Find
18
+ include Documents::Pending
19
+ include Documents::List
20
+ include Documents::Delete
21
+
22
+ attr_reader :client
23
+
24
+ def initialize(client)
25
+ @client = client
26
+ end
27
+
28
+ private
29
+
30
+ # Upload document using multipart/form-data
31
+ def upload_document(file:, document:, signers:, organization_id: nil, folder_id: nil)
32
+ uri = URI(Client::API_ENDPOINT)
33
+
34
+ # Build GraphQL mutation
35
+ mutation = build_create_mutation(organization_id, folder_id)
36
+
37
+ # Build variables
38
+ variables = {
39
+ document: document.to_h,
40
+ signers: signers.map(&:to_h),
41
+ file: nil
42
+ }
43
+
44
+ # Prepare multipart request
45
+ boundary = "----RubyAutentiqueGem#{Time.now.to_i}"
46
+ body = build_multipart_body(
47
+ mutation: mutation,
48
+ variables: variables,
49
+ file: file,
50
+ boundary: boundary
51
+ )
52
+
53
+ # Make request
54
+ http = build_http_client(uri)
55
+ request = Net::HTTP::Post.new(uri.path)
56
+ request['Authorization'] = "Bearer #{client.api_key}"
57
+ request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
58
+ request.body = body
59
+
60
+ response = http.request(request)
61
+
62
+ raise UploadError, "Upload failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
63
+
64
+ JSON.parse(response.body)
65
+ end
66
+
67
+ # Build the GraphQL mutation string for document creation
68
+ def build_create_mutation(organization_id, folder_id) # rubocop:disable Metrics/MethodLength
69
+ mutation = <<-GRAPHQL
70
+ mutation CreateDocumentMutation(
71
+ $document: DocumentInput!,
72
+ $signers: [SignerInput!]!,
73
+ $file: Upload!
74
+ ) {
75
+ createDocument(
76
+ document: $document,
77
+ signers: $signers,
78
+ file: $file
79
+ GRAPHQL
80
+
81
+ mutation += ",\n organization_id: #{organization_id}" if organization_id
82
+ mutation += ",\n folder_id: \"#{folder_id}\"" if folder_id
83
+
84
+ mutation += <<-GRAPHQL
85
+ ) {
86
+ id
87
+ name
88
+ refusable
89
+ sortable
90
+ created_at
91
+ signatures {
92
+ public_id
93
+ name
94
+ email
95
+ created_at
96
+ action { name }
97
+ link { short_link }
98
+ user { id name email }
99
+ }
100
+ }
101
+ }
102
+ GRAPHQL
103
+
104
+ mutation
105
+ end
106
+
107
+ # Build multipart/form-data body for file uploads
108
+ def build_multipart_body(mutation:, variables:, file:, boundary:)
109
+ parts = []
110
+ parts.concat(build_operations_part(mutation, variables, boundary))
111
+ parts.concat(build_map_part(boundary))
112
+ parts.concat(build_file_part(file, boundary))
113
+ parts << "--#{boundary}--\r\n"
114
+ parts.join
115
+ end
116
+
117
+ def build_operations_part(mutation, variables, boundary)
118
+ operations = { query: mutation, variables: variables }
119
+ [
120
+ "--#{boundary}\r\n",
121
+ "Content-Disposition: form-data; name=\"operations\"\r\n\r\n",
122
+ "#{operations.to_json}\r\n"
123
+ ]
124
+ end
125
+
126
+ def build_map_part(boundary)
127
+ map = { file: ['variables.file'] }
128
+ [
129
+ "--#{boundary}\r\n",
130
+ "Content-Disposition: form-data; name=\"map\"\r\n\r\n",
131
+ "#{map.to_json}\r\n"
132
+ ]
133
+ end
134
+
135
+ def build_file_part(file, boundary)
136
+ content, name, type = extract_file_info(file)
137
+ [
138
+ "--#{boundary}\r\n",
139
+ "Content-Disposition: form-data; name=\"file\"; filename=\"#{name}\"\r\n",
140
+ "Content-Type: #{type}\r\n\r\n",
141
+ content,
142
+ "\r\n"
143
+ ]
144
+ end
145
+
146
+ def extract_file_info(file)
147
+ if file.is_a?(String)
148
+ [File.binread(file), File.basename(file),
149
+ MIME::Types.type_for(file).first&.content_type || 'application/pdf']
150
+ elsif file.is_a?(Hash)
151
+ [file[:io].read, file[:name] || 'document.pdf', file[:mime_type] || 'application/pdf']
152
+ else
153
+ [file.read, 'document.pdf', 'application/pdf']
154
+ end
155
+ end
156
+
157
+ def build_http_client(uri)
158
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
159
+ http.use_ssl = true
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ class Folders
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ # List all folders
13
+ #
14
+ # @return [Array<Hash>]
15
+ def list(limit: 30, page: 1)
16
+ query = client.graphql_client.parse <<-GRAPHQL
17
+ query($limit: Int!, $page: Int!) {
18
+ folders(limit: $limit, page: $page) {
19
+ total
20
+ data {
21
+ id
22
+ name
23
+ created_at
24
+ }
25
+ }
26
+ }
27
+ GRAPHQL
28
+
29
+ result = client.query(query, variables: { limit: limit, page: page })
30
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
31
+
32
+ result.data.folders.data.map(&:to_h)
33
+ end
34
+
35
+ # Create a new folder
36
+ #
37
+ # @param name [String] The folder name
38
+ # @param parent_id [String, nil] The parent folder ID
39
+ # @return [Hash]
40
+ def create(name:, parent_id: nil)
41
+ query = client.graphql_client.parse <<-GRAPHQL
42
+ mutation($folder: FolderInput!, $parent_id: UUID) {
43
+ createFolder(folder: $folder, parent_id: $parent_id) {
44
+ id
45
+ name
46
+ created_at
47
+ }
48
+ }
49
+ GRAPHQL
50
+
51
+ variables = { folder: { name: name } }
52
+ variables[:parent_id] = parent_id if parent_id
53
+
54
+ result = client.query(query, variables: variables)
55
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
56
+
57
+ result.data.create_folder.to_h
58
+ end
59
+
60
+ # Delete a folder
61
+ #
62
+ # @param id [String] The folder ID
63
+ # @return [Boolean]
64
+ def delete(id:)
65
+ query = client.graphql_client.parse <<-GRAPHQL
66
+ mutation($id: UUID!) {
67
+ deleteFolder(id: $id)
68
+ }
69
+ GRAPHQL
70
+
71
+ result = client.query(query, variables: { id: id })
72
+ raise QueryError.new('Query failed', result.errors.messages) if result.errors.any?
73
+
74
+ result.data.delete_folder
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ module Resources
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ VERSION = '0.1.0'
5
+ end
data/lib/autentique.rb ADDED
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'autentique/version'
4
+ require_relative 'autentique/errors'
5
+ require_relative 'autentique/client'
6
+ require_relative 'autentique/models/document'
7
+ require_relative 'autentique/resources'
8
+ require_relative 'autentique/resources/documents'
9
+ require_relative 'autentique/resources/folders'
10
+
11
+ # Main module for the Autentique gem
12
+ #
13
+ # @example Basic usage
14
+ # client = Autentique::Client.new(api_key: 'your_api_key')
15
+ #
16
+ # # Create a document
17
+ # document = client.documents.create(
18
+ # file: '/path/to/contract.pdf',
19
+ # document: { name: 'Contract' },
20
+ # signers: [
21
+ # { email: 'signer@example.com', action: 'SIGN' }
22
+ # ]
23
+ # )
24
+ #
25
+ # # Retrieve a document
26
+ # doc = client.documents.find('document-uuid')
27
+ #
28
+ # # List pending documents
29
+ # pending_docs = client.documents.pending
30
+ #
31
+ module Autentique
32
+ class << self
33
+ # Configure the Autentique client with default settings
34
+ #
35
+ # @yield [Configuration] configuration object
36
+ # @return [Configuration]
37
+ def configure
38
+ yield configuration
39
+ end
40
+
41
+ # Get the current configuration
42
+ #
43
+ # @return [Configuration]
44
+ def configuration
45
+ @configuration ||= Configuration.new
46
+ end
47
+
48
+ # Create a new client with default configuration
49
+ #
50
+ # @return [Client]
51
+ def client
52
+ @client ||= Client.new(
53
+ api_key: configuration.api_key,
54
+ sandbox: configuration.sandbox
55
+ )
56
+ end
57
+
58
+ # Reset the configuration
59
+ def reset
60
+ @configuration = Configuration.new
61
+ @client = nil
62
+ end
63
+ end
64
+
65
+ # Configuration class for global settings
66
+ class Configuration
67
+ attr_accessor :api_key, :sandbox
68
+
69
+ def initialize
70
+ @api_key = ENV.fetch('AUTENTIQUE_API_KEY', nil)
71
+ @sandbox = false
72
+ end
73
+ end
74
+ end