assinafy 1.5.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/CHANGELOG.md +168 -0
- data/LICENSE +19 -0
- data/README.md +463 -0
- data/lib/assinafy/client.rb +245 -0
- data/lib/assinafy/configuration.rb +157 -0
- data/lib/assinafy/errors.rb +83 -0
- data/lib/assinafy/null_logger.rb +9 -0
- data/lib/assinafy/resources/account_resource.rb +267 -0
- data/lib/assinafy/resources/assignment_resource.rb +476 -0
- data/lib/assinafy/resources/auth_resource.rb +251 -0
- data/lib/assinafy/resources/base_resource.rb +250 -0
- data/lib/assinafy/resources/document_resource.rb +912 -0
- data/lib/assinafy/resources/field_resource.rb +274 -0
- data/lib/assinafy/resources/signer_document_resource.rb +222 -0
- data/lib/assinafy/resources/signer_resource.rb +422 -0
- data/lib/assinafy/resources/tag_resource.rb +183 -0
- data/lib/assinafy/resources/template_resource.rb +215 -0
- data/lib/assinafy/resources/user_resource.rb +182 -0
- data/lib/assinafy/resources/webhook_resource.rb +222 -0
- data/lib/assinafy/support/webhook_verifier.rb +129 -0
- data/lib/assinafy/utils.rb +122 -0
- data/lib/assinafy/version.rb +5 -0
- data/lib/assinafy.rb +25 -0
- metadata +195 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
module Resources
|
|
5
|
+
# Assignments — invitations to sign a specific document. Covers virtual
|
|
6
|
+
# (no positioned fields) and collect (positioned fields) methods, cost
|
|
7
|
+
# estimation, signer notification resends, declines, and signing.
|
|
8
|
+
#
|
|
9
|
+
# See https://api.assinafy.com.br/v1/docs#assignment for the full
|
|
10
|
+
# documentation of these endpoints.
|
|
11
|
+
class AssignmentResource < BaseResource
|
|
12
|
+
OPTIONAL_FIELDS = %i[message expires_at copy_receivers].freeze
|
|
13
|
+
METHODS = %w[virtual collect].freeze
|
|
14
|
+
SIGN_ITEM_KEY_MAP = {
|
|
15
|
+
'item_id' => 'itemId',
|
|
16
|
+
'field_id' => 'fieldId',
|
|
17
|
+
'page_id' => 'pageId',
|
|
18
|
+
'value' => 'value'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# Normalise a flexible Ruby-side assignment payload into the body
|
|
23
|
+
# shape the API expects. Accepts:
|
|
24
|
+
#
|
|
25
|
+
# - `signers: ['id1', 'id2']` — bare IDs
|
|
26
|
+
# - `signers: [{ id:, verification_method:, notification_methods:, step: }]`
|
|
27
|
+
# - Legacy `signer_ids:`/`signerIds:` arrays of IDs
|
|
28
|
+
#
|
|
29
|
+
# @note The OpenAPI marks top-level `signers` as required, but the sandbox
|
|
30
|
+
# accepts `collect` payloads that reference signer IDs only in positioned
|
|
31
|
+
# fields. This builder preserves that live-compatible form.
|
|
32
|
+
# @param payload [Hash]
|
|
33
|
+
# @param options [Hash]
|
|
34
|
+
# @option options [Boolean] :allow_signers_without_id allow estimate-cost
|
|
35
|
+
# payloads where method-only signer descriptors carry no id
|
|
36
|
+
# @return [Hash] string-keyed body suitable for {#create} / {#estimate_cost}
|
|
37
|
+
# @raise [Assinafy::ValidationError] on missing required fields
|
|
38
|
+
# @example Bare signer IDs are normalised into { id: } hashes (virtual method)
|
|
39
|
+
# Assinafy::Resources::AssignmentResource.build_payload(signers: %w[s1 s2])
|
|
40
|
+
# # => { "method" => "virtual", "signers" => [{ "id" => "s1" }, { "id" => "s2" }] }
|
|
41
|
+
# @example Rich signer descriptors with sequential signing steps and optional fields
|
|
42
|
+
# Assinafy::Resources::AssignmentResource.build_payload(
|
|
43
|
+
# signers: [{ id: "s1", verification_method: "Email", notification_methods: ["Email"], step: 1 }],
|
|
44
|
+
# message: "Please sign",
|
|
45
|
+
# expires_at: "2026-12-31T23:59:00Z",
|
|
46
|
+
# copy_receivers: ["copy-signer-id"]
|
|
47
|
+
# )
|
|
48
|
+
# # => {
|
|
49
|
+
# # "method" => "virtual",
|
|
50
|
+
# # "signers" => [{ "id" => "s1", "verification_method" => "Email",
|
|
51
|
+
# # "notification_methods" => ["Email"], "step" => 1 }],
|
|
52
|
+
# # "message" => "Please sign", "expires_at" => "2026-12-31T23:59:00Z",
|
|
53
|
+
# # "copy_receivers" => ["copy-signer-id"]
|
|
54
|
+
# # }
|
|
55
|
+
# @example Estimate-cost payload — method-only descriptor with no id (allow flag set)
|
|
56
|
+
# Assinafy::Resources::AssignmentResource.build_payload(
|
|
57
|
+
# { signers: [{ verification_method: "Whatsapp" }] }, { allow_signers_without_id: true }
|
|
58
|
+
# )
|
|
59
|
+
# # => { "method" => "virtual", "signers" => [{ "verification_method" => "Whatsapp" }] }
|
|
60
|
+
# @example Collect method — positioned fields include all required display settings
|
|
61
|
+
# Assinafy::Resources::AssignmentResource.build_payload(
|
|
62
|
+
# method: "collect",
|
|
63
|
+
# entries: [{
|
|
64
|
+
# page_id: "page-id",
|
|
65
|
+
# fields: [{
|
|
66
|
+
# signer_id: "signer-id",
|
|
67
|
+
# field_id: "field-id",
|
|
68
|
+
# display_settings: { left: 100, top: 100, width: 240, height: 48, fontSize: 16 }
|
|
69
|
+
# }]
|
|
70
|
+
# }]
|
|
71
|
+
# )
|
|
72
|
+
# # => { "method" => "collect", "entries" => [{ ... }] }
|
|
73
|
+
def build_payload(payload, options = {})
|
|
74
|
+
p = Utils.clean_params(payload).transform_keys(&:to_sym) if payload.is_a?(Hash)
|
|
75
|
+
raise ValidationError.new('Assignment payload must be a Hash') unless p
|
|
76
|
+
|
|
77
|
+
signers = extract_signer_refs(p)
|
|
78
|
+
entries = p[:entries]
|
|
79
|
+
method = (p[:method] || 'virtual').to_s
|
|
80
|
+
|
|
81
|
+
validate_method!(method, signers, entries, p)
|
|
82
|
+
|
|
83
|
+
result = { method: method }
|
|
84
|
+
result[:signers] = signers.map { |ref| normalise_signer_ref(ref, options) } unless signers.empty?
|
|
85
|
+
OPTIONAL_FIELDS.each { |key| result[key] = p[key] if p[key] }
|
|
86
|
+
result[:entries] = entries if entries
|
|
87
|
+
Utils.body_params(result)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def validate_method!(method, signers, entries, payload)
|
|
93
|
+
unless METHODS.include?(method)
|
|
94
|
+
raise ValidationError.new("Assignment method must be one of: #{METHODS.join(', ')}")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if method == 'virtual' && signers.empty?
|
|
98
|
+
raise ValidationError.new(
|
|
99
|
+
'At least one signer is required',
|
|
100
|
+
{ signers: payload[:signers] || payload[:signer_ids] || payload[:signerIds] }
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return unless method == 'collect' && (!entries.is_a?(Array) || entries.empty?)
|
|
105
|
+
|
|
106
|
+
raise ValidationError.new('entries are required for collect assignments')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract_signer_refs(payload)
|
|
110
|
+
return payload[:signers] if payload[:signers].is_a?(Array) && !payload[:signers].empty?
|
|
111
|
+
|
|
112
|
+
legacy = payload[:signer_ids] || payload[:signerIds]
|
|
113
|
+
legacy.is_a?(Array) ? legacy : []
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def normalise_signer_ref(ref, options)
|
|
117
|
+
return string_signer_ref(ref) if ref.is_a?(String)
|
|
118
|
+
return hash_signer_ref(ref, options) if ref.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
raise ValidationError.new('Invalid signer reference', { ref: ref })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def string_signer_ref(ref)
|
|
124
|
+
raise ValidationError.new('Signer ID cannot be empty') if ref.empty?
|
|
125
|
+
|
|
126
|
+
{ id: ref }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def hash_signer_ref(ref, options)
|
|
130
|
+
r = ref.transform_keys(&:to_sym)
|
|
131
|
+
id = r[:id] || r[:signer_id]
|
|
132
|
+
|
|
133
|
+
normalised = {}
|
|
134
|
+
normalised[:id] = id if id
|
|
135
|
+
normalised[:verification_method] = r[:verification_method] if r[:verification_method]
|
|
136
|
+
normalised[:notification_methods] = r[:notification_methods] if r[:notification_methods]
|
|
137
|
+
normalised[:step] = r[:step] unless r[:step].nil?
|
|
138
|
+
|
|
139
|
+
return normalised if id.is_a?(String) && !id.empty?
|
|
140
|
+
return normalised.tap { |h| h.delete(:id) } if options[:allow_signers_without_id]
|
|
141
|
+
|
|
142
|
+
raise ValidationError.new('Invalid signer reference', { ref: ref })
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# List assignments for an account. The API requires an account context,
|
|
147
|
+
# supplied as the `accountId` query parameter — note the camelCase, which
|
|
148
|
+
# is unusual for this otherwise snake_case API (verified live).
|
|
149
|
+
#
|
|
150
|
+
# @param params [Hash] documented `page` and `per_page` query parameters
|
|
151
|
+
# @param account_id_override [String, nil]
|
|
152
|
+
# @return [Hash{Symbol=>Array,Hash}] `{ data: [assignment, ...], meta: {..} | nil }`
|
|
153
|
+
# @see GET /assignments
|
|
154
|
+
# @example List assignments for the account
|
|
155
|
+
# # Request: GET /assignments?accountId={account_id}
|
|
156
|
+
# client.assignments.list
|
|
157
|
+
#
|
|
158
|
+
# # Response (unwrapped data payload):
|
|
159
|
+
# {
|
|
160
|
+
# data: [
|
|
161
|
+
# {
|
|
162
|
+
# 'id' => 'assignment-id',
|
|
163
|
+
# 'sender_email' => 'sender@example.com',
|
|
164
|
+
# 'method' => 'virtual',
|
|
165
|
+
# 'expires_at' => nil,
|
|
166
|
+
# 'message' => 'Please sign this contract',
|
|
167
|
+
# 'signers' => [
|
|
168
|
+
# { 'id' => 'signer-id', 'full_name' => 'Example Signer',
|
|
169
|
+
# 'email' => 'signer@example.com', 'completed' => false, 'step' => 1 }
|
|
170
|
+
# ]
|
|
171
|
+
# # ... (see docs for the full assignment shape)
|
|
172
|
+
# }
|
|
173
|
+
# ],
|
|
174
|
+
# meta: nil
|
|
175
|
+
# }
|
|
176
|
+
def list(params = {}, account_id_override = nil)
|
|
177
|
+
acc_id = account_id(account_id_override)
|
|
178
|
+
|
|
179
|
+
call_list('Failed to list assignments') do
|
|
180
|
+
http_get('assignments', params.merge(accountId: acc_id))
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Create an assignment for a document. See {.build_payload} for the
|
|
185
|
+
# accepted shapes, including the sandbox-compatible `collect` form without
|
|
186
|
+
# a top-level `signers` array.
|
|
187
|
+
#
|
|
188
|
+
# @param document_id [String]
|
|
189
|
+
# @param payload [Hash]
|
|
190
|
+
# @return [Hash] the assignment object (resource, id, method, expires_at, message, signers[],
|
|
191
|
+
# items[], summary{signer_count, completed_count, signers[]}, signing_urls[], copy_receivers[])
|
|
192
|
+
# @see POST /documents/{documentId}/assignments
|
|
193
|
+
# @example Create a virtual assignment for one signer
|
|
194
|
+
# resource.create("document-id", signers: %w[signer-id], message: "Please sign")
|
|
195
|
+
# # Request body the SDK sends:
|
|
196
|
+
# # { "method" => "virtual", "signers" => [{ "id" => "signer-id" }],
|
|
197
|
+
# # "message" => "Please sign" }
|
|
198
|
+
# # => {
|
|
199
|
+
# # "resource" => "assignment", "id" => "assignment-id",
|
|
200
|
+
# # "sender_email" => "sender@example.com", "method" => "virtual",
|
|
201
|
+
# # "expires_at" => nil, "message" => "Please sign",
|
|
202
|
+
# # "signers" => [{ "id" => "signer-id", "full_name" => "Example Signer",
|
|
203
|
+
# # "email" => "signer@example.com", "whatsapp_phone_number" => nil,
|
|
204
|
+
# # "has_accepted_terms" => false, "completed" => false, "notification_history" => [],
|
|
205
|
+
# # "verification_method" => "Email", "notification_methods" => ["Email"],
|
|
206
|
+
# # "step" => 1, "notified" => true }],
|
|
207
|
+
# # "copy_receivers" => [],
|
|
208
|
+
# # "items" => [{ "id" => "assignment-item-id", "page" => nil,
|
|
209
|
+
# # "signer" => { "id" => "signer-id", ... },
|
|
210
|
+
# # "field" => { "id" => "field-id", "name" => "Virtual",
|
|
211
|
+
# # "type" => "virtual", "is_pre_defined" => true, ... },
|
|
212
|
+
# # "display_settings" => [], "value" => nil, "completed" => false }],
|
|
213
|
+
# # "summary" => { "signer_count" => 1, "completed_count" => 0, "signers" => [{ ... }] },
|
|
214
|
+
# # "signing_urls" => [{ "signer_id" => "signer-id",
|
|
215
|
+
# # "url" => "https://app-sandbox.assinafy.com.br/sign/document-id?email=signer%40example.com" }]
|
|
216
|
+
# # } # ... (see docs for full shape)
|
|
217
|
+
def create(document_id, payload)
|
|
218
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
219
|
+
body = self.class.build_payload(payload)
|
|
220
|
+
|
|
221
|
+
@logger.info("Creating assignment for document #{doc_id}")
|
|
222
|
+
|
|
223
|
+
call('Failed to create assignment') do
|
|
224
|
+
http_post("documents/#{doc_id}/assignments", body)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Estimate the credit cost of a potential assignment, without creating it.
|
|
229
|
+
# Accepts the same payload as {#create}, but signer descriptors may omit
|
|
230
|
+
# `id`. An empty descriptor (`{}`) defaults both methods to `Email`.
|
|
231
|
+
#
|
|
232
|
+
# @param document_id [String]
|
|
233
|
+
# @param payload [Hash]
|
|
234
|
+
# @return [Hash] cost breakdown (documents, credits, needs_extra_document, extra_document_cost,
|
|
235
|
+
# total_credits, breakdown[], document_balance, credit_balance, has_sufficient_resources,
|
|
236
|
+
# blocking_reason, message)
|
|
237
|
+
# @see POST /documents/{documentId}/assignments/estimate-cost
|
|
238
|
+
# @example Estimate cost of inviting a WhatsApp signer (no id needed)
|
|
239
|
+
# resource.estimate_cost("document-id",
|
|
240
|
+
# signers: [{ verification_method: "Whatsapp" }])
|
|
241
|
+
# # Request body the SDK sends:
|
|
242
|
+
# # { "method" => "virtual", "signers" => [{ "verification_method" => "Whatsapp" }] }
|
|
243
|
+
# # => {
|
|
244
|
+
# # "documents" => 1, "credits" => 0, "needs_extra_document" => false,
|
|
245
|
+
# # "extra_document_cost" => 0, "total_credits" => 0, "breakdown" => [],
|
|
246
|
+
# # "document_balance" => 62, "credit_balance" => 0,
|
|
247
|
+
# # "has_sufficient_resources" => true, "blocking_reason" => nil, "message" => nil
|
|
248
|
+
# # }
|
|
249
|
+
def estimate_cost(document_id, payload)
|
|
250
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
251
|
+
body = self.class.build_payload(payload, allow_signers_without_id: true)
|
|
252
|
+
|
|
253
|
+
call('Failed to estimate assignment cost') do
|
|
254
|
+
http_post("documents/#{doc_id}/assignments/estimate-cost", body)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Update the expiration timestamp of an existing assignment. The
|
|
259
|
+
# `expires_at` body field is required by the API and accepts an explicit
|
|
260
|
+
# `nil` (serialized as JSON `null`) to mean "no expiration". The value is
|
|
261
|
+
# therefore sent verbatim rather than through {Utils.body_params}, which
|
|
262
|
+
# would drop the nil.
|
|
263
|
+
#
|
|
264
|
+
# @param document_id [String]
|
|
265
|
+
# @param assignment_id [String]
|
|
266
|
+
# @param expires_at [String, nil] ISO 8601 timestamp, or nil for no expiry
|
|
267
|
+
# @return [Hash] the updated assignment object (same shape as {#create}; expires_at reflects
|
|
268
|
+
# the new value — nil when cleared)
|
|
269
|
+
# @see PUT /documents/{documentId}/assignments/{assignmentId}/reset-expiration
|
|
270
|
+
# @example Set a new expiration timestamp
|
|
271
|
+
# resource.reset_expiration("document-id", "assignment-id",
|
|
272
|
+
# "2026-12-31T23:59:00Z")
|
|
273
|
+
# # Request body the SDK sends: { "expires_at" => "2026-12-31T23:59:00Z" }
|
|
274
|
+
# # => { "resource" => "assignment", "id" => "assignment-id",
|
|
275
|
+
# # "method" => "virtual", "expires_at" => "2026-12-31T23:59:00Z",
|
|
276
|
+
# # "signers" => [{ ... }], "items" => [{ ... }], "summary" => { ... },
|
|
277
|
+
# # "signing_urls" => [{ ... }], "copy_receivers" => [] } # ... (see docs for full shape)
|
|
278
|
+
# @example Clear the expiration (nil is sent verbatim as JSON null)
|
|
279
|
+
# resource.reset_expiration("document-id", "assignment-id", nil)
|
|
280
|
+
# # Request body the SDK sends: { "expires_at" => nil }
|
|
281
|
+
# # => { "resource" => "assignment", "id" => "assignment-id",
|
|
282
|
+
# # "method" => "virtual", "expires_at" => nil, ... } # ... (see docs for full shape)
|
|
283
|
+
def reset_expiration(document_id, assignment_id, expires_at)
|
|
284
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
285
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
286
|
+
|
|
287
|
+
call('Failed to update assignment expiration') do
|
|
288
|
+
http_put("documents/#{doc_id}/assignments/#{asg_id}/reset-expiration",
|
|
289
|
+
{ 'expires_at' => expires_at })
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Resend the assignment notification (email/WhatsApp) to a signer.
|
|
294
|
+
# May charge credits — use {#estimate_resend_cost} to preview.
|
|
295
|
+
#
|
|
296
|
+
# @param document_id [String]
|
|
297
|
+
# @param assignment_id [String]
|
|
298
|
+
# @param signer_id [String]
|
|
299
|
+
# @return [Hash] delivery confirmation (is_sent, document_id, signer_id)
|
|
300
|
+
# @see PUT /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/resend
|
|
301
|
+
# @example Resend the signing notification to a signer
|
|
302
|
+
# resource.resend_notification("document-id", "assignment-id", "signer-id")
|
|
303
|
+
# # (no request body)
|
|
304
|
+
# # => { "is_sent" => true, "document_id" => "document-id", "signer_id" => "signer-id" }
|
|
305
|
+
def resend_notification(document_id, assignment_id, signer_id)
|
|
306
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
307
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
308
|
+
sid = require_id(signer_id, 'Signer ID')
|
|
309
|
+
|
|
310
|
+
call('Failed to resend signer notification') do
|
|
311
|
+
http_put("documents/#{doc_id}/assignments/#{asg_id}/signers/#{sid}/resend")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Estimate the credit cost of resending the notification to a signer.
|
|
316
|
+
#
|
|
317
|
+
# @param document_id [String]
|
|
318
|
+
# @param assignment_id [String]
|
|
319
|
+
# @param signer_id [String]
|
|
320
|
+
# @return [Hash] cost breakdown (documents, credits, needs_extra_document, extra_document_cost,
|
|
321
|
+
# total_credits, breakdown[], document_balance, credit_balance, has_sufficient_resources,
|
|
322
|
+
# blocking_reason, message)
|
|
323
|
+
# @see POST /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/estimate-resend-cost
|
|
324
|
+
# @example Preview the cost of resending to a WhatsApp signer
|
|
325
|
+
# resource.estimate_resend_cost("document-id", "assignment-id", "signer-id")
|
|
326
|
+
# # (no request body)
|
|
327
|
+
# # => {
|
|
328
|
+
# # "documents" => 1, "credits" => 0.45, "needs_extra_document" => false,
|
|
329
|
+
# # "extra_document_cost" => 0, "total_credits" => 0.45,
|
|
330
|
+
# # "breakdown" => [{ "code" => "NotificationWhatsapp",
|
|
331
|
+
# # "name" => "Whatsapp Notification", "cost" => 0.45,
|
|
332
|
+
# # "quantity" => 1, "unit_cost" => 0.45 }],
|
|
333
|
+
# # "document_balance" => 10, "credit_balance" => 100,
|
|
334
|
+
# # "has_sufficient_resources" => true, "blocking_reason" => nil, "message" => nil
|
|
335
|
+
# # }
|
|
336
|
+
def estimate_resend_cost(document_id, assignment_id, signer_id)
|
|
337
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
338
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
339
|
+
sid = require_id(signer_id, 'Signer ID')
|
|
340
|
+
|
|
341
|
+
call('Failed to estimate resend cost') do
|
|
342
|
+
http_post("documents/#{doc_id}/assignments/#{asg_id}/signers/#{sid}/estimate-resend-cost")
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Fetch the document a signer is being asked to sign (signer-access-code auth).
|
|
347
|
+
#
|
|
348
|
+
# @param signer_access_code [String]
|
|
349
|
+
# @param has_accepted_terms [Boolean, nil]
|
|
350
|
+
# @return [Hash] the document (id, account_id, name, status, artifacts, signing_url, ...) with an
|
|
351
|
+
# embedded current_signer and assignment (items filtered to the current signer); no pages array
|
|
352
|
+
# @see GET /sign
|
|
353
|
+
# @example Resolve the document a signer was invited to sign
|
|
354
|
+
# resource.signer_document(signer_access_code: "signer-access-code")
|
|
355
|
+
# # (no request body — signer-access-code is a query param)
|
|
356
|
+
# # => {
|
|
357
|
+
# # "id" => "document-id", "account_id" => "account-id", "name" => "my_document.pdf",
|
|
358
|
+
# # "status" => "metadata_ready",
|
|
359
|
+
# # "artifacts" => { "original" => "https://.../download/original",
|
|
360
|
+
# # "thumbnail" => "https://.../thumbnail" },
|
|
361
|
+
# # "is_closed" => false, "signing_url" => "%ui_base_url%/sign/doc1",
|
|
362
|
+
# # "decline_reason" => nil, "declined_by" => nil,
|
|
363
|
+
# # "current_signer" => { "id" => "signer-id", "full_name" => "Signer Name",
|
|
364
|
+
# # "email" => "signer@example.com", "has_accepted_terms" => false,
|
|
365
|
+
# # "verification_method" => "Email", "notification_methods" => ["Email"] },
|
|
366
|
+
# # "assignment" => { "id" => "1", "method" => "virtual", "expires_at" => nil,
|
|
367
|
+
# # "items" => [{ "id" => "assignment-item-id", "field" => { "type" => "virtual" }, ... }] }
|
|
368
|
+
# # } # ... (see docs for full shape)
|
|
369
|
+
def signer_document(signer_access_code:, has_accepted_terms: nil)
|
|
370
|
+
call('Failed to fetch signer assignment document') do
|
|
371
|
+
http_get('sign', signer_access_code: signer_access_code,
|
|
372
|
+
has_accepted_terms: has_accepted_terms)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Submit signatures for an assignment as a signer.
|
|
377
|
+
#
|
|
378
|
+
# The API uses camelCase for this body. Callers may pass snake_case
|
|
379
|
+
# (`item_id`, `field_id`, `page_id`, `value`) — this method maps them
|
|
380
|
+
# to the API's `itemId`, `fieldId`, `pageId`, `value`.
|
|
381
|
+
#
|
|
382
|
+
# @param document_id [String]
|
|
383
|
+
# @param assignment_id [String]
|
|
384
|
+
# @param items [Array<Hash>]
|
|
385
|
+
# @param signer_access_code [String]
|
|
386
|
+
# @return [Hash] empty Hash `{}` on success per the API reference. Signing
|
|
387
|
+
# requires an emailed OTP, so this exact shape is not independently
|
|
388
|
+
# verifiable with a workspace API key.
|
|
389
|
+
# @see POST /documents/{documentId}/assignments/{assignmentId}
|
|
390
|
+
# @example Sign with snake_case keys — mapped to camelCase itemId/fieldId/pageId
|
|
391
|
+
# resource.sign("document-id", "assignment-id",
|
|
392
|
+
# [{ item_id: "assignment-item-id", field_id: "field-id",
|
|
393
|
+
# page_id: "page-id", value: "Signed by Example Signer" }],
|
|
394
|
+
# signer_access_code: "signer-access-code")
|
|
395
|
+
# # Request body the SDK sends (snake_case keys mapped to camelCase):
|
|
396
|
+
# # [{ "itemId" => "assignment-item-id", "fieldId" => "field-id",
|
|
397
|
+
# # "pageId" => "page-id", "value" => "Signed by Example Signer" }]
|
|
398
|
+
# # => {} # per the API reference (unverified via workspace key)
|
|
399
|
+
def sign(document_id, assignment_id, items, signer_access_code:)
|
|
400
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
401
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
402
|
+
body = require_array(items, 'Assignment items').map { |item| normalise_sign_item(item) }
|
|
403
|
+
|
|
404
|
+
call('Failed to sign assignment') do
|
|
405
|
+
http_post("documents/#{doc_id}/assignments/#{asg_id}", body,
|
|
406
|
+
signer_access_code: signer_access_code)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Decline an assignment as a signer.
|
|
411
|
+
#
|
|
412
|
+
# @param document_id [String]
|
|
413
|
+
# @param assignment_id [String]
|
|
414
|
+
# @param decline_reason [String]
|
|
415
|
+
# @param signer_access_code [String]
|
|
416
|
+
# @return [Array] empty array on success (the API returns no payload)
|
|
417
|
+
# @see PUT /documents/{documentId}/assignments/{assignmentId}/reject
|
|
418
|
+
# @example Decline an assignment as the signer
|
|
419
|
+
# resource.decline("document-id", "assignment-id",
|
|
420
|
+
# decline_reason: "I do not agree with clause 2.",
|
|
421
|
+
# signer_access_code: "signer-access-code")
|
|
422
|
+
# # Request body the SDK sends: { "decline_reason" => "I do not agree with clause 2." }
|
|
423
|
+
# # => []
|
|
424
|
+
def decline(document_id, assignment_id, decline_reason:, signer_access_code:)
|
|
425
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
426
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
427
|
+
reason = require_present(decline_reason, 'Decline reason')
|
|
428
|
+
|
|
429
|
+
call('Failed to decline assignment') do
|
|
430
|
+
http_put("documents/#{doc_id}/assignments/#{asg_id}/reject",
|
|
431
|
+
body_params(decline_reason: reason),
|
|
432
|
+
signer_access_code: signer_access_code)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# List the WhatsApp notifications that were sent for an assignment,
|
|
437
|
+
# including the rendered template text.
|
|
438
|
+
#
|
|
439
|
+
# @param document_id [String]
|
|
440
|
+
# @param assignment_id [String]
|
|
441
|
+
# @return [Array<Hash>] notification objects (sent_at, header, body, buttons[]{text}, phone_number,
|
|
442
|
+
# signer_id); empty array when no WhatsApp notifications were sent
|
|
443
|
+
# @see GET /documents/{documentId}/assignments/{assignmentId}/whatsapp-notifications
|
|
444
|
+
# @example List WhatsApp notifications sent for an assignment
|
|
445
|
+
# resource.whatsapp_notifications("document-id", "assignment-id")
|
|
446
|
+
# # (no request body)
|
|
447
|
+
# # => [
|
|
448
|
+
# # { "sent_at" => 1710000000,
|
|
449
|
+
# # "header" => "Documento para assinatura: Contrato de Servico",
|
|
450
|
+
# # "body" => "Oi, Maria.\n\nJoao Silva enviou um documento...",
|
|
451
|
+
# # "buttons" => [{ "text" => "Abrir documento" }],
|
|
452
|
+
# # "phone_number" => "+15555550100", "signer_id" => "signer-id" }
|
|
453
|
+
# # ]
|
|
454
|
+
# # => [] # when no WhatsApp notifications were sent (e.g. email-only assignment)
|
|
455
|
+
def whatsapp_notifications(document_id, assignment_id)
|
|
456
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
457
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
458
|
+
|
|
459
|
+
call('Failed to list WhatsApp notifications') do
|
|
460
|
+
http_get("documents/#{doc_id}/assignments/#{asg_id}/whatsapp-notifications")
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
private
|
|
465
|
+
|
|
466
|
+
def normalise_sign_item(item)
|
|
467
|
+
return item unless item.is_a?(Hash)
|
|
468
|
+
|
|
469
|
+
item.each_with_object({}) do |(key, value), result|
|
|
470
|
+
raw = key.to_s
|
|
471
|
+
result[SIGN_ITEM_KEY_MAP.fetch(raw, raw)] = value
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|