anvil-ruby 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.
- checksums.yaml +7 -0
- data/AGENTS.md +123 -0
- data/API_COVERAGE.md +294 -0
- data/CHANGELOG.md +34 -0
- data/CLAUDE_README.md +98 -0
- data/GITHUB_ACTIONS_QUICKREF.md +174 -0
- data/Gemfile.lock +112 -0
- data/Gemfile.minimal +9 -0
- data/LICENSE +21 -0
- data/PROJECT_CONTEXT.md +196 -0
- data/README.md +445 -0
- data/anvil-ruby.gemspec +66 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/create_signature_direct.rb +142 -0
- data/create_signature_packet.rb +232 -0
- data/debug_env.rb +28 -0
- data/examples/create_signature.rb +194 -0
- data/examples/fill_pdf.rb +89 -0
- data/examples/generate_pdf.rb +347 -0
- data/examples/verify_webhook.rb +281 -0
- data/lib/anvil/client.rb +216 -0
- data/lib/anvil/configuration.rb +87 -0
- data/lib/anvil/env_loader.rb +30 -0
- data/lib/anvil/errors.rb +95 -0
- data/lib/anvil/rate_limiter.rb +66 -0
- data/lib/anvil/resources/base.rb +100 -0
- data/lib/anvil/resources/pdf.rb +171 -0
- data/lib/anvil/resources/signature.rb +517 -0
- data/lib/anvil/resources/webform.rb +154 -0
- data/lib/anvil/resources/webhook.rb +201 -0
- data/lib/anvil/resources/workflow.rb +169 -0
- data/lib/anvil/response.rb +98 -0
- data/lib/anvil/version.rb +5 -0
- data/lib/anvil.rb +88 -0
- data/quickstart_signature.rb +220 -0
- data/test_api_connection.rb +143 -0
- data/test_etch_signature.rb +230 -0
- data/test_gem.rb +72 -0
- data/test_signature.rb +281 -0
- data/test_signature_with_template.rb +112 -0
- metadata +247 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anvil
|
|
4
|
+
class PDF < Resources::Base
|
|
5
|
+
attr_reader :raw_data
|
|
6
|
+
|
|
7
|
+
def initialize(raw_data, attributes = {}, client: nil)
|
|
8
|
+
super(attributes, client: client)
|
|
9
|
+
@raw_data = raw_data
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Save the PDF to a file
|
|
13
|
+
def save_as(filename, mode = 'wb')
|
|
14
|
+
raise FileError, 'No PDF data to save' unless raw_data
|
|
15
|
+
|
|
16
|
+
File.open(filename, mode) do |file|
|
|
17
|
+
file.write(raw_data)
|
|
18
|
+
end
|
|
19
|
+
filename
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Save and raise on error
|
|
23
|
+
def save_as!(filename, mode = 'wb')
|
|
24
|
+
save_as(filename, mode)
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
raise FileError, "Failed to save PDF: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get the PDF as a base64 encoded string
|
|
30
|
+
def to_base64
|
|
31
|
+
return nil unless raw_data
|
|
32
|
+
|
|
33
|
+
Base64.strict_encode64(raw_data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the size in bytes
|
|
37
|
+
def size
|
|
38
|
+
raw_data&.bytesize || 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def size_human
|
|
42
|
+
size_in_bytes = size
|
|
43
|
+
return '0 B' if size_in_bytes.zero?
|
|
44
|
+
|
|
45
|
+
units = %w[B KB MB GB]
|
|
46
|
+
exp = (Math.log(size_in_bytes) / Math.log(1024)).to_i
|
|
47
|
+
exp = units.size - 1 if exp >= units.size
|
|
48
|
+
|
|
49
|
+
format('%.2f %s', size_in_bytes.to_f / (1024**exp), units[exp])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# Fill a PDF template with data
|
|
54
|
+
#
|
|
55
|
+
# @param template_id [String] The PDF template ID
|
|
56
|
+
# @param data [Hash] The data to fill the PDF with
|
|
57
|
+
# @param options [Hash] Additional options
|
|
58
|
+
# @option options [String] :title Optional document title
|
|
59
|
+
# @option options [String] :font_family Font family (default: "Noto Sans")
|
|
60
|
+
# @option options [Integer] :font_size Font size (default: 10)
|
|
61
|
+
# @option options [String] :text_color Text color (default: "#333333")
|
|
62
|
+
# @option options [Boolean] :use_interactive_fields Use interactive form fields
|
|
63
|
+
# @option options [String] :api_key Optional API key override
|
|
64
|
+
# @return [PDF] The filled PDF
|
|
65
|
+
def fill(template_id:, data:, **options)
|
|
66
|
+
api_key = options.delete(:api_key)
|
|
67
|
+
client = api_key ? Client.new(api_key: api_key) : self.client
|
|
68
|
+
|
|
69
|
+
payload = build_fill_payload(data, options)
|
|
70
|
+
path = "/fill/#{template_id}.pdf"
|
|
71
|
+
|
|
72
|
+
response = client.post(path, payload)
|
|
73
|
+
|
|
74
|
+
raise APIError, "Expected PDF response but got: #{response.content_type}" unless response.binary?
|
|
75
|
+
|
|
76
|
+
new(response.raw_body, { template_id: template_id }, client: client)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Generate a PDF from HTML or Markdown
|
|
80
|
+
#
|
|
81
|
+
# @param type [Symbol, String] :html or :markdown
|
|
82
|
+
# @param data [Hash, Array] Content data
|
|
83
|
+
# @param options [Hash] Additional options
|
|
84
|
+
# @option options [String] :title Document title
|
|
85
|
+
# @option options [Hash] :page Page configuration
|
|
86
|
+
# @option options [String] :api_key Optional API key override
|
|
87
|
+
# @return [PDF] The generated PDF
|
|
88
|
+
def generate(data:, type: :markdown, **options)
|
|
89
|
+
api_key = options.delete(:api_key)
|
|
90
|
+
client = api_key ? Client.new(api_key: api_key) : self.client
|
|
91
|
+
|
|
92
|
+
type = type.to_s.downcase
|
|
93
|
+
unless %w[html markdown].include?(type)
|
|
94
|
+
raise ArgumentError, "Type must be :html or :markdown, got #{type.inspect}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
payload = build_generate_payload(type, data, options)
|
|
98
|
+
path = '/generate-pdf'
|
|
99
|
+
|
|
100
|
+
response = client.post(path, payload)
|
|
101
|
+
|
|
102
|
+
raise APIError, "Expected PDF response but got: #{response.content_type}" unless response.binary?
|
|
103
|
+
|
|
104
|
+
new(response.raw_body, { type: type }, client: client)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Convenience methods for specific generation types
|
|
108
|
+
def generate_from_html(html:, css: nil, **options)
|
|
109
|
+
data = { html: html }
|
|
110
|
+
data[:css] = css if css
|
|
111
|
+
generate(type: :html, data: data, **options)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def generate_from_markdown(content, **options)
|
|
115
|
+
data = if content.is_a?(String)
|
|
116
|
+
[{ content: content }]
|
|
117
|
+
elsif content.is_a?(Array)
|
|
118
|
+
content
|
|
119
|
+
else
|
|
120
|
+
raise ArgumentError, 'Markdown content must be a string or array'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
generate(type: :markdown, data: data, **options)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def build_fill_payload(data, options)
|
|
129
|
+
payload = { data: data }
|
|
130
|
+
|
|
131
|
+
# Add optional parameters if provided
|
|
132
|
+
payload[:title] = options[:title] if options[:title]
|
|
133
|
+
payload[:fontSize] = options[:font_size] if options[:font_size]
|
|
134
|
+
payload[:fontFamily] = options[:font_family] if options[:font_family]
|
|
135
|
+
payload[:textColor] = options[:text_color] if options[:text_color]
|
|
136
|
+
payload[:useInteractiveFields] = options[:use_interactive_fields] if options.key?(:use_interactive_fields)
|
|
137
|
+
|
|
138
|
+
payload
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def build_generate_payload(type, data, options)
|
|
142
|
+
payload = {
|
|
143
|
+
type: type,
|
|
144
|
+
data: data
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Add optional parameters
|
|
148
|
+
payload[:title] = options[:title] if options[:title]
|
|
149
|
+
|
|
150
|
+
# Page configuration
|
|
151
|
+
payload[:page] = build_page_config(options[:page]) if options[:page]
|
|
152
|
+
|
|
153
|
+
payload
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.build_page_config(page)
|
|
157
|
+
return nil unless page
|
|
158
|
+
|
|
159
|
+
config = {}
|
|
160
|
+
config[:width] = page[:width] if page[:width]
|
|
161
|
+
config[:height] = page[:height] if page[:height]
|
|
162
|
+
config[:marginTop] = page[:margin_top] if page[:margin_top]
|
|
163
|
+
config[:marginBottom] = page[:margin_bottom] if page[:margin_bottom]
|
|
164
|
+
config[:marginLeft] = page[:margin_left] if page[:margin_left]
|
|
165
|
+
config[:marginRight] = page[:margin_right] if page[:margin_right]
|
|
166
|
+
config[:pageCount] = page[:page_count] if page.key?(:page_count)
|
|
167
|
+
config
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Anvil
|
|
4
|
+
class Signature < Resources::Base
|
|
5
|
+
# Etch packet statuses
|
|
6
|
+
STATUSES = %w[draft sent partial_complete complete].freeze
|
|
7
|
+
|
|
8
|
+
def id
|
|
9
|
+
attributes[:eid] || attributes[:id]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def eid
|
|
13
|
+
attributes[:eid]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def status
|
|
17
|
+
attributes[:status]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def name
|
|
21
|
+
attributes[:name]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check packet status
|
|
25
|
+
def draft?
|
|
26
|
+
status == 'draft'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sent?
|
|
30
|
+
status == 'sent'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def partially_complete?
|
|
34
|
+
status == 'partial_complete'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def complete?
|
|
38
|
+
status == 'complete'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def completed?
|
|
42
|
+
complete?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Has the packet been sent to signers?
|
|
46
|
+
def in_progress?
|
|
47
|
+
sent? || partially_complete?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get signing URL for a specific signer
|
|
51
|
+
def signing_url(signer_id:, client_user_id: nil)
|
|
52
|
+
self.class.generate_signing_url(
|
|
53
|
+
packet_eid: eid,
|
|
54
|
+
signer_eid: signer_id,
|
|
55
|
+
client_user_id: client_user_id,
|
|
56
|
+
client: client
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get all signers
|
|
61
|
+
def signers
|
|
62
|
+
Array(attributes[:signers]).map do |signer|
|
|
63
|
+
SignatureSigner.new(signer, packet: self)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get documents
|
|
68
|
+
def documents
|
|
69
|
+
Array(attributes[:documents])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reload from API
|
|
73
|
+
def reload!
|
|
74
|
+
refreshed = self.class.find(eid, client: client)
|
|
75
|
+
@attributes = refreshed.attributes
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Update the signature packet
|
|
80
|
+
#
|
|
81
|
+
# @param options [Hash] Fields to update (name, signers, email_subject, email_body)
|
|
82
|
+
# @return [self] The updated signature packet
|
|
83
|
+
def update(**options)
|
|
84
|
+
payload = build_update_payload(options)
|
|
85
|
+
|
|
86
|
+
data = client.graphql(self.class.send(:update_packet_mutation), variables: { eid: eid, input: payload })
|
|
87
|
+
raise APIError, "Failed to update signature packet: #{eid}" unless data && data[:updateEtchPacket]
|
|
88
|
+
|
|
89
|
+
@attributes = symbolize_keys(data[:updateEtchPacket])
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Send a draft packet to signers
|
|
94
|
+
#
|
|
95
|
+
# @return [self] The sent signature packet
|
|
96
|
+
def send!
|
|
97
|
+
data = client.graphql(self.class.send(:send_packet_mutation), variables: { eid: eid })
|
|
98
|
+
raise APIError, "Failed to send signature packet: #{eid}" unless data && data[:sendEtchPacket]
|
|
99
|
+
|
|
100
|
+
@attributes = symbolize_keys(data[:sendEtchPacket])
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Delete the signature packet
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] true if deleted
|
|
107
|
+
def delete!
|
|
108
|
+
data = client.graphql(self.class.send(:remove_packet_mutation), variables: { eid: eid })
|
|
109
|
+
raise APIError, "Failed to delete signature packet: #{eid}" unless data
|
|
110
|
+
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Skip a signer in the signature flow
|
|
115
|
+
#
|
|
116
|
+
# @param signer_eid [String] The EID of the signer to skip
|
|
117
|
+
# @return [self] The updated signature packet
|
|
118
|
+
def skip_signer(signer_eid)
|
|
119
|
+
data = client.graphql(self.class.send(:skip_signer_mutation),
|
|
120
|
+
variables: { signerEid: signer_eid, packetEid: eid })
|
|
121
|
+
raise APIError, "Failed to skip signer: #{signer_eid}" unless data
|
|
122
|
+
|
|
123
|
+
reload!
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Send a reminder notification to a signer
|
|
127
|
+
#
|
|
128
|
+
# @param signer_eid [String] The EID of the signer to notify
|
|
129
|
+
# @return [Boolean] true if notification sent
|
|
130
|
+
def notify_signer(signer_eid)
|
|
131
|
+
data = client.graphql(self.class.send(:notify_signer_mutation),
|
|
132
|
+
variables: { signerEid: signer_eid, packetEid: eid })
|
|
133
|
+
raise APIError, "Failed to notify signer: #{signer_eid}" unless data
|
|
134
|
+
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Void signed documents
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean] true if voided
|
|
141
|
+
def void!
|
|
142
|
+
data = client.graphql(
|
|
143
|
+
self.class.send(:void_document_group_mutation),
|
|
144
|
+
variables: { eid: eid }
|
|
145
|
+
)
|
|
146
|
+
raise APIError, "Failed to void signature packet: #{eid}" unless data
|
|
147
|
+
|
|
148
|
+
reload!
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Expire all active signing sessions
|
|
153
|
+
#
|
|
154
|
+
# @return [Boolean] true if tokens expired
|
|
155
|
+
def expire_tokens!
|
|
156
|
+
data = client.graphql(
|
|
157
|
+
self.class.send(:expire_signer_tokens_mutation),
|
|
158
|
+
variables: { eid: eid }
|
|
159
|
+
)
|
|
160
|
+
raise APIError, "Failed to expire signer tokens: #{eid}" unless data
|
|
161
|
+
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def build_update_payload(options)
|
|
168
|
+
payload = {}
|
|
169
|
+
payload[:name] = options[:name] if options.key?(:name)
|
|
170
|
+
payload[:signers] = self.class.send(:build_signers_payload, options[:signers]) if options[:signers]
|
|
171
|
+
payload[:signatureEmailSubject] = options[:email_subject] if options[:email_subject]
|
|
172
|
+
payload[:signatureEmailBody] = options[:email_body] if options[:email_body]
|
|
173
|
+
payload
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class << self
|
|
177
|
+
# Create a new signature packet
|
|
178
|
+
#
|
|
179
|
+
# @param name [String] Name of the packet
|
|
180
|
+
# @param signers [Array<Hash>] Array of signer information
|
|
181
|
+
# @param files [Array<Hash>] Array of files to sign
|
|
182
|
+
# @param options [Hash] Additional options
|
|
183
|
+
# @return [Signature] The created signature packet
|
|
184
|
+
def create(name:, signers:, files: nil, **options)
|
|
185
|
+
api_key = options.delete(:api_key)
|
|
186
|
+
client = api_key ? Client.new(api_key: api_key) : self.client
|
|
187
|
+
|
|
188
|
+
payload = build_create_payload(name, signers, files, options)
|
|
189
|
+
|
|
190
|
+
# Use full GraphQL endpoint URL
|
|
191
|
+
response = client.post(client.config.graphql_url, {
|
|
192
|
+
query: create_packet_mutation,
|
|
193
|
+
variables: { input: payload }
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
data = response.data
|
|
197
|
+
unless data[:data] && data[:data][:createEtchPacket]
|
|
198
|
+
raise APIError, "Failed to create signature packet: #{data[:errors]}"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
new(data[:data][:createEtchPacket], client: client)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Find a signature packet by ID
|
|
205
|
+
def find(packet_eid, client: nil)
|
|
206
|
+
client ||= self.client
|
|
207
|
+
|
|
208
|
+
response = client.post(client.config.graphql_url, {
|
|
209
|
+
query: find_packet_query,
|
|
210
|
+
variables: { eid: packet_eid }
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
data = response.data
|
|
214
|
+
raise NotFoundError, "Signature packet not found: #{packet_eid}" unless data[:data] && data[:data][:etchPacket]
|
|
215
|
+
|
|
216
|
+
new(data[:data][:etchPacket], client: client)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# List all signature packets
|
|
220
|
+
def list(limit: 10, offset: 0, status: nil, client: nil)
|
|
221
|
+
client ||= self.client
|
|
222
|
+
|
|
223
|
+
variables = { limit: limit, offset: offset }
|
|
224
|
+
variables[:status] = status if status
|
|
225
|
+
|
|
226
|
+
response = client.post(client.config.graphql_url, {
|
|
227
|
+
query: list_packets_query,
|
|
228
|
+
variables: variables
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
data = response.data
|
|
232
|
+
if data[:data] && data[:data][:etchPackets]
|
|
233
|
+
data[:data][:etchPackets].map { |packet| new(packet, client: client) }
|
|
234
|
+
else
|
|
235
|
+
[]
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Generate a signing URL for a signer
|
|
240
|
+
def generate_signing_url(packet_eid:, signer_eid:, client_user_id: nil, client: nil)
|
|
241
|
+
client ||= self.client
|
|
242
|
+
|
|
243
|
+
payload = {
|
|
244
|
+
packetEid: packet_eid,
|
|
245
|
+
signerEid: signer_eid
|
|
246
|
+
}
|
|
247
|
+
payload[:clientUserId] = client_user_id if client_user_id
|
|
248
|
+
|
|
249
|
+
response = client.post(client.config.graphql_url, {
|
|
250
|
+
query: generate_url_mutation,
|
|
251
|
+
variables: { input: payload }
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
data = response.data
|
|
255
|
+
raise APIError, 'Failed to generate signing URL' unless data[:data] && data[:data][:generateEtchSignURL]
|
|
256
|
+
|
|
257
|
+
data[:data][:generateEtchSignURL][:url]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def build_create_payload(name, signers, files, options)
|
|
263
|
+
payload = {
|
|
264
|
+
name: name,
|
|
265
|
+
signers: build_signers_payload(signers)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
payload[:files] = build_files_payload(files) if files
|
|
269
|
+
payload[:isDraft] = options[:is_draft] if options.key?(:is_draft)
|
|
270
|
+
payload[:webhookURL] = options[:webhook_url] if options[:webhook_url]
|
|
271
|
+
payload[:signatureEmailSubject] = options[:email_subject] if options[:email_subject]
|
|
272
|
+
payload[:signatureEmailBody] = options[:email_body] if options[:email_body]
|
|
273
|
+
|
|
274
|
+
payload
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def build_signers_payload(signers)
|
|
278
|
+
signers.map do |signer|
|
|
279
|
+
{
|
|
280
|
+
name: signer[:name],
|
|
281
|
+
email: signer[:email],
|
|
282
|
+
role: signer[:role] || 'signer',
|
|
283
|
+
signerType: signer[:signer_type] || 'email'
|
|
284
|
+
}.compact
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def build_files_payload(files)
|
|
289
|
+
files.map do |file|
|
|
290
|
+
if file[:type] == :pdf && file[:id]
|
|
291
|
+
# Template ID should be castEid
|
|
292
|
+
{
|
|
293
|
+
id: 'file1', # File identifier
|
|
294
|
+
castEid: file[:id] # Template ID
|
|
295
|
+
}
|
|
296
|
+
elsif file[:type] == :upload && file[:data]
|
|
297
|
+
{
|
|
298
|
+
type: 'upload',
|
|
299
|
+
data: Base64.strict_encode64(file[:data]),
|
|
300
|
+
filename: file[:filename] || 'document.pdf'
|
|
301
|
+
}
|
|
302
|
+
else
|
|
303
|
+
file
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# GraphQL queries and mutations (simplified versions)
|
|
309
|
+
def create_packet_mutation
|
|
310
|
+
<<~GRAPHQL
|
|
311
|
+
mutation CreateEtchPacket($input: JSON) {
|
|
312
|
+
createEtchPacket(variables: $input) {
|
|
313
|
+
eid
|
|
314
|
+
name
|
|
315
|
+
status
|
|
316
|
+
createdAt
|
|
317
|
+
signers {
|
|
318
|
+
eid
|
|
319
|
+
name
|
|
320
|
+
email
|
|
321
|
+
status
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
GRAPHQL
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def find_packet_query
|
|
329
|
+
<<~GRAPHQL
|
|
330
|
+
query GetEtchPacket($eid: String!) {
|
|
331
|
+
etchPacket(eid: $eid) {
|
|
332
|
+
eid
|
|
333
|
+
name
|
|
334
|
+
status
|
|
335
|
+
createdAt
|
|
336
|
+
completedAt
|
|
337
|
+
signers {
|
|
338
|
+
eid
|
|
339
|
+
name
|
|
340
|
+
email
|
|
341
|
+
status
|
|
342
|
+
completedAt
|
|
343
|
+
}
|
|
344
|
+
documents {
|
|
345
|
+
eid
|
|
346
|
+
name
|
|
347
|
+
type
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
GRAPHQL
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def list_packets_query
|
|
355
|
+
<<~GRAPHQL
|
|
356
|
+
query ListEtchPackets($limit: Int, $offset: Int, $status: String) {
|
|
357
|
+
etchPackets(limit: $limit, offset: $offset, status: $status) {
|
|
358
|
+
eid
|
|
359
|
+
name
|
|
360
|
+
status
|
|
361
|
+
createdAt
|
|
362
|
+
completedAt
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
GRAPHQL
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def generate_url_mutation
|
|
369
|
+
<<~GRAPHQL
|
|
370
|
+
mutation GenerateEtchSignURL($input: GenerateEtchSignURLInput!) {
|
|
371
|
+
generateEtchSignURL(input: $input) {
|
|
372
|
+
url
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
GRAPHQL
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def update_packet_mutation
|
|
379
|
+
<<~GRAPHQL
|
|
380
|
+
mutation UpdateEtchPacket($eid: String!, $input: JSON) {
|
|
381
|
+
updateEtchPacket(eid: $eid, variables: $input) {
|
|
382
|
+
eid
|
|
383
|
+
name
|
|
384
|
+
status
|
|
385
|
+
createdAt
|
|
386
|
+
signers {
|
|
387
|
+
eid
|
|
388
|
+
name
|
|
389
|
+
email
|
|
390
|
+
status
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
GRAPHQL
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def send_packet_mutation
|
|
398
|
+
<<~GRAPHQL
|
|
399
|
+
mutation SendEtchPacket($eid: String!) {
|
|
400
|
+
sendEtchPacket(eid: $eid) {
|
|
401
|
+
eid
|
|
402
|
+
name
|
|
403
|
+
status
|
|
404
|
+
createdAt
|
|
405
|
+
signers {
|
|
406
|
+
eid
|
|
407
|
+
name
|
|
408
|
+
email
|
|
409
|
+
status
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
GRAPHQL
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def remove_packet_mutation
|
|
417
|
+
<<~GRAPHQL
|
|
418
|
+
mutation RemoveEtchPacket($eid: String!) {
|
|
419
|
+
removeEtchPacket(eid: $eid)
|
|
420
|
+
}
|
|
421
|
+
GRAPHQL
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def skip_signer_mutation
|
|
425
|
+
<<~GRAPHQL
|
|
426
|
+
mutation SkipSigner($signerEid: String!, $packetEid: String!) {
|
|
427
|
+
skipSigner(signerEid: $signerEid, packetEid: $packetEid)
|
|
428
|
+
}
|
|
429
|
+
GRAPHQL
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def notify_signer_mutation
|
|
433
|
+
<<~GRAPHQL
|
|
434
|
+
mutation NotifySigner($signerEid: String!, $packetEid: String!) {
|
|
435
|
+
notifySigner(signerEid: $signerEid, packetEid: $packetEid)
|
|
436
|
+
}
|
|
437
|
+
GRAPHQL
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def void_document_group_mutation
|
|
441
|
+
<<~GRAPHQL
|
|
442
|
+
mutation VoidDocumentGroup($eid: String!) {
|
|
443
|
+
voidDocumentGroup(eid: $eid)
|
|
444
|
+
}
|
|
445
|
+
GRAPHQL
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def expire_signer_tokens_mutation
|
|
449
|
+
<<~GRAPHQL
|
|
450
|
+
mutation ExpireSignerTokens($eid: String!) {
|
|
451
|
+
expireSignerTokens(eid: $eid)
|
|
452
|
+
}
|
|
453
|
+
GRAPHQL
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Helper class for signature signers
|
|
459
|
+
class SignatureSigner < Resources::Base
|
|
460
|
+
attr_reader :packet
|
|
461
|
+
|
|
462
|
+
def initialize(attributes, packet: nil)
|
|
463
|
+
super(attributes)
|
|
464
|
+
@packet = packet
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def eid
|
|
468
|
+
attributes[:eid]
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def name
|
|
472
|
+
attributes[:name]
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def email
|
|
476
|
+
attributes[:email]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def status
|
|
480
|
+
attributes[:status]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def complete?
|
|
484
|
+
status == 'complete'
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def completed_at
|
|
488
|
+
return unless attributes[:completed_at]
|
|
489
|
+
|
|
490
|
+
Time.parse(attributes[:completed_at])
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Get signing URL for this signer
|
|
494
|
+
def signing_url(client_user_id: nil)
|
|
495
|
+
return nil unless packet
|
|
496
|
+
|
|
497
|
+
packet.signing_url(
|
|
498
|
+
signer_id: eid,
|
|
499
|
+
client_user_id: client_user_id
|
|
500
|
+
)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Skip this signer
|
|
504
|
+
def skip!
|
|
505
|
+
raise Error, 'No packet associated with this signer' unless packet
|
|
506
|
+
|
|
507
|
+
packet.skip_signer(eid)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Send a reminder to this signer
|
|
511
|
+
def send_reminder!
|
|
512
|
+
raise Error, 'No packet associated with this signer' unless packet
|
|
513
|
+
|
|
514
|
+
packet.notify_signer(eid)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|