assinafy 1.11.0 → 2.0.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 +4 -4
- data/CHANGELOG.md +39 -0
- data/README.en.md +51 -3
- data/README.md +59 -1
- data/docs/API_REFERENCE.md +96 -12
- data/lib/assinafy/errors.rb +26 -0
- data/lib/assinafy/resources/assignment_resource.rb +115 -12
- data/lib/assinafy/resources/base_resource.rb +20 -6
- data/lib/assinafy/resources/document_resource.rb +28 -54
- data/lib/assinafy/resources/field_resource.rb +31 -16
- data/lib/assinafy/resources/signer_document_resource.rb +75 -21
- data/lib/assinafy/resources/signer_resource.rb +55 -1
- data/lib/assinafy/resources/webhook_resource.rb +83 -55
- data/lib/assinafy/support/webhook_verifier.rb +1 -1
- data/lib/assinafy/version.rb +1 -1
- data/sig/assinafy.rbs +25 -7
- metadata +2 -2
|
@@ -29,7 +29,10 @@ module Assinafy
|
|
|
29
29
|
# certificate (an e-CPF, or an e-CNPJ naming them as legal
|
|
30
30
|
# representative); a CNPJ requires an e-CNPJ for that company, from any
|
|
31
31
|
# of its representatives.
|
|
32
|
-
|
|
32
|
+
# - `IdDocumentFaceMatchBasic` — government ID photos, face match and liveness.
|
|
33
|
+
# Requires its account feature and a valid CPF in `government_id`; priced per signer.
|
|
34
|
+
VERIFICATION_METHODS = %w[Email Whatsapp DigitalCertificate IdDocumentFaceMatchBasic].freeze
|
|
35
|
+
IDENTITY_REVIEW_IMAGES = %w[document_front document_back document_pdf face].freeze
|
|
33
36
|
|
|
34
37
|
# Channels used to notify a signer of the request. Exactly one per signer;
|
|
35
38
|
# WhatsApp incurs an additional cost and is available only on paid
|
|
@@ -52,12 +55,14 @@ module Assinafy
|
|
|
52
55
|
# - `signers: [{ id:, verification_method:, notification_methods:, step: }]`
|
|
53
56
|
# - Legacy `signer_ids:`/`signerIds:` arrays of IDs
|
|
54
57
|
#
|
|
55
|
-
# @note
|
|
56
|
-
# requires positioned `entries
|
|
58
|
+
# @note Creation requires top-level `signers` for both methods, and `collect` additionally
|
|
59
|
+
# requires positioned `entries`; both are optional in estimate-cost payloads.
|
|
57
60
|
# @param payload [Hash]
|
|
58
61
|
# @param options [Hash]
|
|
59
62
|
# @option options [Boolean] :allow_signers_without_id allow estimate-cost
|
|
60
63
|
# payloads where method-only signer descriptors carry no id
|
|
64
|
+
# @option options [Boolean] :require_signers (true) false for estimate-cost payloads, where
|
|
65
|
+
# `signers` and `entries` are optional; an empty `signers` list is then omitted
|
|
61
66
|
# @return [Hash] string-keyed body suitable for {#create} / {#estimate_cost}
|
|
62
67
|
# @raise [Assinafy::ValidationError] on missing required fields
|
|
63
68
|
# @example Bare signer IDs are normalised into { id: } hashes (virtual method)
|
|
@@ -114,12 +119,13 @@ module Assinafy
|
|
|
114
119
|
entries = p[:entries]
|
|
115
120
|
method = (p[:method] || 'virtual').to_s
|
|
116
121
|
|
|
117
|
-
validate_method!(method, signers, entries, p)
|
|
122
|
+
validate_method!(method, signers, entries, p, options.fetch(:require_signers, true))
|
|
118
123
|
validate_optional_fields!(p)
|
|
119
124
|
|
|
120
125
|
result = { method: method }
|
|
121
|
-
|
|
122
|
-
validate_signing_steps!(
|
|
126
|
+
normalised = signers.map { |ref| normalise_signer_ref(ref, options) }
|
|
127
|
+
validate_signing_steps!(normalised)
|
|
128
|
+
result[:signers] = normalised unless normalised.empty?
|
|
123
129
|
OPTIONAL_FIELDS.each { |key| result[key] = p[key] if p[key] }
|
|
124
130
|
result[:entries] = entries if entries
|
|
125
131
|
Utils.body_params(result)
|
|
@@ -127,13 +133,13 @@ module Assinafy
|
|
|
127
133
|
|
|
128
134
|
private
|
|
129
135
|
|
|
130
|
-
def validate_method!(method, signers, entries, payload)
|
|
136
|
+
def validate_method!(method, signers, entries, payload, require_signers)
|
|
131
137
|
unless METHODS.include?(method)
|
|
132
138
|
raise ValidationError.new("Assignment method must be one of: #{METHODS.join(', ')}")
|
|
133
139
|
end
|
|
140
|
+
return unless require_signers
|
|
134
141
|
|
|
135
|
-
# `signers`
|
|
136
|
-
# estimate is priced per signer. Positioned fields do not replace this list.
|
|
142
|
+
# Creation needs `signers` for both methods; positioned fields do not replace this list.
|
|
137
143
|
if signers.empty?
|
|
138
144
|
raise ValidationError.new(
|
|
139
145
|
'At least one signer is required',
|
|
@@ -241,7 +247,8 @@ module Assinafy
|
|
|
241
247
|
def validate_method_pair!(signer)
|
|
242
248
|
verification = signer[:verification_method]
|
|
243
249
|
notification = signer[:notification_methods]&.first
|
|
244
|
-
return if verification.nil? || notification.nil? ||
|
|
250
|
+
return if verification.nil? || notification.nil? ||
|
|
251
|
+
%w[DigitalCertificate IdDocumentFaceMatchBasic].include?(verification)
|
|
245
252
|
return if verification == notification
|
|
246
253
|
|
|
247
254
|
raise ValidationError.new('notification_methods must match the verification_method')
|
|
@@ -356,7 +363,7 @@ module Assinafy
|
|
|
356
363
|
# `id`. An empty descriptor (`{}`) defaults both methods to `Email`.
|
|
357
364
|
#
|
|
358
365
|
# @param document_id [String]
|
|
359
|
-
# @param payload [Hash]
|
|
366
|
+
# @param payload [Hash] `method`, and optional `signers` (method-only descriptors) and `entries`
|
|
360
367
|
# @return [Hash] cost breakdown (documents, credits, needs_extra_document, extra_document_cost,
|
|
361
368
|
# total_credits, breakdown[], document_balance, credit_balance, has_sufficient_resources,
|
|
362
369
|
# blocking_reason, message)
|
|
@@ -379,7 +386,7 @@ module Assinafy
|
|
|
379
386
|
# # }
|
|
380
387
|
def estimate_cost(document_id, payload)
|
|
381
388
|
doc_id = require_id(document_id, 'Document ID')
|
|
382
|
-
body = self.class.build_payload(payload, allow_signers_without_id: true)
|
|
389
|
+
body = self.class.build_payload(payload, allow_signers_without_id: true, require_signers: false)
|
|
383
390
|
|
|
384
391
|
call('Failed to estimate assignment cost') do
|
|
385
392
|
http_post("documents/#{doc_id}/assignments/estimate-cost", body)
|
|
@@ -485,6 +492,95 @@ module Assinafy
|
|
|
485
492
|
end
|
|
486
493
|
end
|
|
487
494
|
|
|
495
|
+
# Read the account review of a signer's biometric identity verification.
|
|
496
|
+
# Evidence contains summaries; images are available only while InReview.
|
|
497
|
+
# The signer can sign during review, but certification awaits approval.
|
|
498
|
+
#
|
|
499
|
+
# @param document_id [String]
|
|
500
|
+
# @param assignment_id [String]
|
|
501
|
+
# @param signer_id [String]
|
|
502
|
+
# @return [Hash] SignerIdentityReview; see docs/API_REFERENCE.md#signeridentityreview for all fields
|
|
503
|
+
# @raise [Assinafy::ApiError] on failure, including 404 when no verification exists
|
|
504
|
+
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
505
|
+
# @raise [Assinafy::ValidationError] on invalid IDs
|
|
506
|
+
# @see GET /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification
|
|
507
|
+
# @example Read an open review (no request body; workspace credentials)
|
|
508
|
+
# client.assignments.identity_review('document-id', 'assignment-id', 'signer-id')
|
|
509
|
+
# # => { 'id' => 'verification-id', 'status' => 'InReview', 'provider' => 'AssinafyVerify',
|
|
510
|
+
# # 'verification_method' => 'IdDocumentFaceMatchBasic',
|
|
511
|
+
# # 'reason_codes' => [{ 'code' => 'NAME_MISMATCH', 'description' => 'Name differs' }],
|
|
512
|
+
# # 'evidence_available' => true, 'expected' => { 'name' => 'Example Signer', 'cpf' => nil },
|
|
513
|
+
# # 'document' => { 'type' => 'CNH', 'name' => 'EXAMPLE SIGNER', 'cpf' => nil },
|
|
514
|
+
# # 'matches' => { 'cpf' => true, 'name' => 0.52, 'birth_date' => nil },
|
|
515
|
+
# # 'checks' => [{ 'name' => 'name', 'result' => 'review', 'score' => 0.52,
|
|
516
|
+
# # 'reasons' => ['NAME_MISMATCH'] }], 'images' => ['document_front', 'face'],
|
|
517
|
+
# # 'ruleset_version' => '2026-10-12.2', 'evidence_sha256' => 'sha256-placeholder',
|
|
518
|
+
# # 'reviewed_by' => nil, 'reviewed_at' => nil, 'review_note' => nil, 'completed_at' => nil }
|
|
519
|
+
def identity_review(document_id, assignment_id, signer_id)
|
|
520
|
+
path = identity_review_path(document_id, assignment_id, signer_id)
|
|
521
|
+
|
|
522
|
+
call('Failed to fetch signer identity review') { http_get(path) }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Download one evidence image or document PDF while a review is InReview.
|
|
526
|
+
# Fetched from the provider on each call; keep the sensitive bytes out of logs and caches.
|
|
527
|
+
#
|
|
528
|
+
# @param document_id [String]
|
|
529
|
+
# @param assignment_id [String]
|
|
530
|
+
# @param signer_id [String]
|
|
531
|
+
# @param name [String] document_front, document_back, document_pdf or face, as listed in the review
|
|
532
|
+
# @return [String] binary JPEG, PNG or PDF bytes
|
|
533
|
+
# @raise [Assinafy::ApiError] on failure; 404 after a decision or when evidence is unavailable
|
|
534
|
+
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
535
|
+
# @raise [Assinafy::ValidationError] on invalid IDs or image name
|
|
536
|
+
# @see GET /documents/{document}/assignments/{assignment}/signers/{signer}/identity-verification/images/{name}
|
|
537
|
+
# @example Read the face image (no request body; workspace credentials)
|
|
538
|
+
# client.assignments.identity_review_image('document-id', 'assignment-id', 'signer-id', 'face')
|
|
539
|
+
# # => "\x89PNG\r\n\x1A\n..." # binary String
|
|
540
|
+
def identity_review_image(document_id, assignment_id, signer_id, name)
|
|
541
|
+
path = identity_review_path(document_id, assignment_id, signer_id)
|
|
542
|
+
unless IDENTITY_REVIEW_IMAGES.include?(name)
|
|
543
|
+
raise ValidationError.new("Image name must be one of: #{IDENTITY_REVIEW_IMAGES.join(', ')}")
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
call_binary('Failed to download signer identity review image') { http_get("#{path}/images/#{name}") }
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Approve or reject an InReview identity verification. Rejection cancels the
|
|
550
|
+
# document and notifies every signer and copy receiver. The note is recorded
|
|
551
|
+
# in activities and sent to the provider; it is never shown to the signer.
|
|
552
|
+
#
|
|
553
|
+
# @param document_id [String]
|
|
554
|
+
# @param assignment_id [String]
|
|
555
|
+
# @param signer_id [String]
|
|
556
|
+
# @param decision [String] Approved or Rejected
|
|
557
|
+
# @param note [String, nil] optional review note, at most 2000 characters
|
|
558
|
+
# @return [Hash] SignerIdentityReview, the same fields as {#identity_review}, with the decision recorded
|
|
559
|
+
# @raise [Assinafy::ApiError] on failure; 400 unless InReview and awaiting signatures or identity review
|
|
560
|
+
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
561
|
+
# @raise [Assinafy::ValidationError] on invalid IDs, decision or note
|
|
562
|
+
# @see POST /documents/{documentId}/assignments/{assignmentId}/signers/{signerId}/identity-verification/review
|
|
563
|
+
# @example Approve a review with workspace credentials
|
|
564
|
+
# client.assignments.decide_identity_review('document-id', 'assignment-id', 'signer-id',
|
|
565
|
+
# decision: 'Approved', note: 'Identity checked')
|
|
566
|
+
# # Body: { 'decision' => 'Approved', 'note' => 'Identity checked' }
|
|
567
|
+
# # => the identity_review object with 'status' => 'Approved', 'images' => [],
|
|
568
|
+
# # 'reviewed_by' => { 'id' => 'user-id', 'name' => 'Example Reviewer' },
|
|
569
|
+
# # 'reviewed_at' => '2026-10-16T12:00:00Z', 'review_note' => 'Identity checked'
|
|
570
|
+
def decide_identity_review(document_id, assignment_id, signer_id, decision:, note: nil)
|
|
571
|
+
path = identity_review_path(document_id, assignment_id, signer_id)
|
|
572
|
+
unless %w[Approved Rejected].include?(decision)
|
|
573
|
+
raise ValidationError.new('Decision must be Approved or Rejected')
|
|
574
|
+
end
|
|
575
|
+
unless note.nil? || (note.is_a?(String) && note.length <= 2000)
|
|
576
|
+
raise ValidationError.new('Review note must be a String of at most 2000 characters')
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
call('Failed to decide signer identity review') do
|
|
580
|
+
http_post("#{path}/review", body_params(decision: decision, note: note))
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
488
584
|
# Fetch the document a signer is being asked to sign (signer-access-code auth).
|
|
489
585
|
#
|
|
490
586
|
# @param signer_access_code [String]
|
|
@@ -623,6 +719,13 @@ module Assinafy
|
|
|
623
719
|
|
|
624
720
|
private
|
|
625
721
|
|
|
722
|
+
def identity_review_path(document_id, assignment_id, signer_id)
|
|
723
|
+
doc_id = require_id(document_id, 'Document ID')
|
|
724
|
+
asg_id = require_id(assignment_id, 'Assignment ID')
|
|
725
|
+
sid = require_id(signer_id, 'Signer ID')
|
|
726
|
+
"documents/#{doc_id}/assignments/#{asg_id}/signers/#{sid}/identity-verification"
|
|
727
|
+
end
|
|
728
|
+
|
|
626
729
|
def normalise_sign_item(item)
|
|
627
730
|
normalised = require_payload(item, 'Assignment item').each_with_object({}) do |(key, value), result|
|
|
628
731
|
raw = key.to_s
|
|
@@ -17,6 +17,11 @@ module Assinafy
|
|
|
17
17
|
total: 'x-pagination-total-count',
|
|
18
18
|
last_page: 'x-pagination-page-count'
|
|
19
19
|
}.freeze
|
|
20
|
+
RATE_LIMIT_HEADERS = {
|
|
21
|
+
limit: 'x-rate-limit-limit',
|
|
22
|
+
remaining: 'x-rate-limit-remaining',
|
|
23
|
+
reset: 'x-rate-limit-reset'
|
|
24
|
+
}.freeze
|
|
20
25
|
|
|
21
26
|
# @param connection [Faraday::Connection]
|
|
22
27
|
# @param default_account_id [String, nil] used by `account_id` when no override is passed
|
|
@@ -297,23 +302,32 @@ module Assinafy
|
|
|
297
302
|
|
|
298
303
|
# On a `403` the `WWW-Authenticate` challenge names the scope an OAuth
|
|
299
304
|
# token is missing, which tells "reconnect with more scope" apart from
|
|
300
|
-
# "wrong workspace or role", so it travels with the error
|
|
305
|
+
# "wrong workspace or role", so it travels with the error, as do the
|
|
306
|
+
# rate-limit headers a `429` needs for backing off.
|
|
301
307
|
def check_status!(response, _label)
|
|
302
308
|
return if (200..299).cover?(response.status)
|
|
303
309
|
|
|
304
|
-
raise
|
|
310
|
+
raise with_response_headers(error_class.from_response(response.status, response.body), response)
|
|
305
311
|
end
|
|
306
312
|
|
|
307
|
-
# A 2xx envelope can still carry an error status, with the same
|
|
313
|
+
# A 2xx envelope can still carry an error status, with the same headers.
|
|
308
314
|
def unwrap(response)
|
|
309
315
|
Utils.handle_assinafy_response(response.body)
|
|
310
316
|
rescue ApiError => e
|
|
311
|
-
raise
|
|
317
|
+
raise with_response_headers(e, response)
|
|
312
318
|
end
|
|
313
319
|
|
|
314
|
-
def
|
|
315
|
-
|
|
320
|
+
def with_response_headers(error, response)
|
|
321
|
+
headers = response.headers || {}
|
|
322
|
+
challenge = headers['www-authenticate']
|
|
316
323
|
error.context[:www_authenticate] = challenge if challenge
|
|
324
|
+
rate_limit = RATE_LIMIT_HEADERS.filter_map do |key, header|
|
|
325
|
+
value = to_int(headers[header])
|
|
326
|
+
[key, value] if value
|
|
327
|
+
end.to_h
|
|
328
|
+
error.context[:rate_limit] = rate_limit unless rate_limit.empty?
|
|
329
|
+
retry_after = to_int(headers['retry-after'])
|
|
330
|
+
error.context[:retry_after] = retry_after if retry_after
|
|
317
331
|
error
|
|
318
332
|
end
|
|
319
333
|
|
|
@@ -131,7 +131,7 @@ module Assinafy
|
|
|
131
131
|
# Lightweight search over an account's documents (id/name/status/artifacts),
|
|
132
132
|
# without the heavier per-document detail returned by {#list}.
|
|
133
133
|
#
|
|
134
|
-
# @param query [String] free-text search term
|
|
134
|
+
# @param query [String, nil] optional free-text search term; nil omits `search`
|
|
135
135
|
# @param params [Hash] extra query parameters (`status`, `page`, `per_page`, ...)
|
|
136
136
|
# @param account_id_override [String, nil]
|
|
137
137
|
# @return [Hash{Symbol=>Array,Hash}] `{ data: [...], meta: {..} | nil }`
|
|
@@ -162,10 +162,10 @@ module Assinafy
|
|
|
162
162
|
# # ... (one Hash per matching document)
|
|
163
163
|
# ]
|
|
164
164
|
# }
|
|
165
|
-
def search(query, params = {}, account_id_override = nil)
|
|
165
|
+
def search(query = nil, params = {}, account_id_override = nil)
|
|
166
166
|
acc_id = account_id(account_id_override)
|
|
167
167
|
filters = require_payload(params, 'Document search parameters')
|
|
168
|
-
require_string(query, 'Search query')
|
|
168
|
+
require_string(query, 'Search query') unless query.nil?
|
|
169
169
|
|
|
170
170
|
call_list('Failed to search documents') do
|
|
171
171
|
http_get("accounts/#{acc_id}/documents/search", filters.merge(search: query))
|
|
@@ -253,7 +253,7 @@ module Assinafy
|
|
|
253
253
|
# Rename a document.
|
|
254
254
|
#
|
|
255
255
|
# @param document_id [String]
|
|
256
|
-
# @param name [String] the new display name
|
|
256
|
+
# @param name [String] the new display name (at most 255 characters)
|
|
257
257
|
# @return [Hash] the updated document object (envelope `data` unwrapped)
|
|
258
258
|
# @raise [Assinafy::ApiError] on an unsuccessful API response
|
|
259
259
|
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
@@ -281,7 +281,7 @@ module Assinafy
|
|
|
281
281
|
# }
|
|
282
282
|
def rename(document_id, name)
|
|
283
283
|
doc_id = require_id(document_id, 'Document ID')
|
|
284
|
-
new_name = require_string(name, 'Name')
|
|
284
|
+
new_name = require_string(name, 'Name', max_length: 255)
|
|
285
285
|
|
|
286
286
|
call('Failed to rename document') do
|
|
287
287
|
http_patch("documents/#{doc_id}", body_params(name: new_name))
|
|
@@ -664,67 +664,41 @@ module Assinafy
|
|
|
664
664
|
end
|
|
665
665
|
end
|
|
666
666
|
|
|
667
|
-
#
|
|
668
|
-
#
|
|
669
|
-
# deployed API requires `{ recipient:, channel: }` when a recipient is supplied.
|
|
667
|
+
# Email a 6-digit access token for the document to its signer (public endpoint;
|
|
668
|
+
# no workspace credentials are sent).
|
|
670
669
|
#
|
|
671
670
|
# @param document_id [String]
|
|
672
|
-
# @param recipient [String
|
|
673
|
-
# @param channel [String
|
|
674
|
-
# @param email [String, nil]
|
|
675
|
-
# @return [
|
|
676
|
-
# API returns `{ 'document' => {..}, 'channel' => String, 'recipient' => String }`
|
|
671
|
+
# @param recipient [String] the signer's email address (required)
|
|
672
|
+
# @param channel [String] delivery channel; only `"email"` is accepted
|
|
673
|
+
# @param email [String, nil] earlier name for `recipient:`; sent as `recipient`
|
|
674
|
+
# @return [Hash] `{ 'document' => Hash, 'channel' => String, 'recipient' => String }`
|
|
677
675
|
# @raise [Assinafy::ApiError] on an unsuccessful API response
|
|
678
676
|
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
679
|
-
# @raise [Assinafy::ValidationError]
|
|
677
|
+
# @raise [Assinafy::ValidationError] when the recipient is missing or not an email address,
|
|
678
|
+
# when the channel is not `email`, or when both `recipient:` and `email:` are given
|
|
680
679
|
# @see PUT /public/documents/{document_id}/send-token
|
|
681
|
-
# @example
|
|
682
|
-
# client.documents.send_token('document-id')
|
|
680
|
+
# @example Email the access token to the signer (no auth required)
|
|
681
|
+
# client.documents.send_token('document-id', recipient: 'signer@example.com')
|
|
683
682
|
#
|
|
684
|
-
# @example Email a signer using the current OpenAPI request (no auth required)
|
|
685
683
|
# # Request: PUT /public/documents/{document_id}/send-token
|
|
686
|
-
# # Body: { "email": "signer@example.com" }
|
|
687
|
-
# client.documents.send_token('document-id', email: 'signer@example.com')
|
|
688
|
-
#
|
|
689
|
-
# @example Use the deployed-API request shape
|
|
690
|
-
# # Body: { "recipient": "signer@example.com", "channel": "email" }
|
|
691
|
-
# client.documents.send_token('document-id', recipient: 'signer@example.com', channel: 'email')
|
|
684
|
+
# # Body: { "channel": "email", "recipient": "signer@example.com" }
|
|
692
685
|
#
|
|
693
|
-
# #
|
|
694
|
-
# {
|
|
695
|
-
#
|
|
696
|
-
#
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
|
|
700
|
-
# 'created_by' => 'John Smith'
|
|
701
|
-
# },
|
|
702
|
-
# 'channel' => 'email',
|
|
703
|
-
# 'recipient' => 'signer@example.com'
|
|
704
|
-
# }
|
|
705
|
-
# # => nil when the API returns the documented no-data envelope
|
|
706
|
-
def send_token(document_id, recipient: nil, channel: nil, email: nil)
|
|
686
|
+
# # => {
|
|
687
|
+
# # 'document' => { 'resource' => 'document', 'id' => 'doc1', 'name' => '1.pdf',
|
|
688
|
+
# # 'page_count' => 1, 'created_by' => 'John Smith' },
|
|
689
|
+
# # 'channel' => 'email',
|
|
690
|
+
# # 'recipient' => 'signer@example.com'
|
|
691
|
+
# # }
|
|
692
|
+
def send_token(document_id, recipient: nil, channel: 'email', email: nil)
|
|
707
693
|
doc_id = require_id(document_id, 'Document ID')
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
elsif !email.nil?
|
|
711
|
-
if recipient || channel
|
|
712
|
-
raise ValidationError.new('Use either email or recipient/channel, not both')
|
|
713
|
-
end
|
|
694
|
+
raise ValidationError.new('Use either recipient or email, not both') if recipient && email
|
|
695
|
+
raise ValidationError.new('Channel must be email') unless channel == 'email'
|
|
714
696
|
|
|
715
|
-
|
|
716
|
-
else
|
|
717
|
-
require_string(recipient, 'Recipient')
|
|
718
|
-
delivery_channel = require_present(channel, 'Channel').to_s
|
|
719
|
-
unless %w[email whatsapp].include?(delivery_channel)
|
|
720
|
-
raise ValidationError.new('Channel must be email or whatsapp')
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
payload = { recipient: recipient, channel: delivery_channel }
|
|
724
|
-
end
|
|
697
|
+
address = Utils.require_email(recipient || email)
|
|
725
698
|
|
|
726
699
|
call('Failed to send signer token') do
|
|
727
|
-
http_put("public/documents/#{doc_id}/send-token",
|
|
700
|
+
http_put("public/documents/#{doc_id}/send-token", { 'channel' => 'email', 'recipient' => address },
|
|
701
|
+
workspace_auth: false)
|
|
728
702
|
end
|
|
729
703
|
end
|
|
730
704
|
|
|
@@ -7,12 +7,17 @@ module Assinafy
|
|
|
7
7
|
# See https://api.assinafy.com.br/v1/docs#field-definition for the
|
|
8
8
|
# documentation of these endpoints.
|
|
9
9
|
class FieldResource < BaseResource
|
|
10
|
+
# Default for `signer_access_code:`, telling an omitted keyword (workspace credentials)
|
|
11
|
+
# apart from an explicit nil (rejected).
|
|
12
|
+
WORKSPACE = Object.new.freeze
|
|
13
|
+
private_constant :WORKSPACE
|
|
14
|
+
|
|
10
15
|
# Create a field definition.
|
|
11
16
|
#
|
|
12
17
|
# @param payload [Hash]
|
|
13
18
|
# @option payload [String] :type required — e.g. `text`, `cpf`, `email` — see {#types}
|
|
14
19
|
# @option payload [String] :name required — display label
|
|
15
|
-
# @option payload [String]
|
|
20
|
+
# @option payload [String, nil] :regex optional validation regex (text fields); nil is sent as null
|
|
16
21
|
# @option payload [Boolean] :is_required default `true`
|
|
17
22
|
# @param account_id_override [String, nil]
|
|
18
23
|
# @return [Hash] the created field definition (envelope `data` unwrapped)
|
|
@@ -41,7 +46,8 @@ module Assinafy
|
|
|
41
46
|
# # }
|
|
42
47
|
def create(payload, account_id_override = nil)
|
|
43
48
|
acc_id = account_id(account_id_override)
|
|
44
|
-
|
|
49
|
+
raw = require_payload(payload)
|
|
50
|
+
body = with_explicit_regex(raw, body_params(raw))
|
|
45
51
|
require_string(body['name'], 'Field name')
|
|
46
52
|
require_string(body['type'], 'Field type')
|
|
47
53
|
require_boolean(body['is_required'], 'is_required') if body.key?('is_required')
|
|
@@ -163,9 +169,7 @@ module Assinafy
|
|
|
163
169
|
acc_id = account_id(account_id_override)
|
|
164
170
|
fid = require_id(field_id, 'Field ID')
|
|
165
171
|
raw = require_payload(payload)
|
|
166
|
-
body = body_params(raw)
|
|
167
|
-
body['regex'] = nil if (raw.key?(:regex) && raw[:regex].nil?) ||
|
|
168
|
-
(raw.key?('regex') && raw['regex'].nil?)
|
|
172
|
+
body = with_explicit_regex(raw, body_params(raw))
|
|
169
173
|
require_string(body['name'], 'Field name') if body.key?('name')
|
|
170
174
|
require_boolean(body['is_active'], 'is_active') if body.key?('is_active')
|
|
171
175
|
|
|
@@ -197,14 +201,15 @@ module Assinafy
|
|
|
197
201
|
|
|
198
202
|
# Validate a single value against a field definition.
|
|
199
203
|
#
|
|
200
|
-
# The OpenAPI declares workspace Authorization
|
|
201
|
-
#
|
|
202
|
-
#
|
|
204
|
+
# The OpenAPI declares workspace Authorization, which is used when
|
|
205
|
+
# `signer_access_code:` is omitted. The deployed API also accepts the signer
|
|
206
|
+
# access code: when the keyword is given it must be a non-blank code, and
|
|
207
|
+
# only the code is sent (never the workspace credentials).
|
|
203
208
|
#
|
|
204
209
|
# @param field_id [String]
|
|
205
210
|
# @param value [Object]
|
|
206
211
|
# @param account_id_override [String, nil]
|
|
207
|
-
# @param signer_access_code [String
|
|
212
|
+
# @param signer_access_code [String] optional keyword; nil or blank raises
|
|
208
213
|
# @return [Hash{String=>Object}] `{ "type" =>, "success" =>, "error_message" => }`
|
|
209
214
|
# @raise [Assinafy::ApiError] on an unsuccessful API response
|
|
210
215
|
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
@@ -221,12 +226,12 @@ module Assinafy
|
|
|
221
226
|
# @example Validate as a signer (signer-access-code auth, sent as a query param)
|
|
222
227
|
# client.fields.validate('field-id', 'Some text', signer_access_code: 'signer-access-code')
|
|
223
228
|
# # => { "type" => "text", "success" => true, "error_message" => "" }
|
|
224
|
-
def validate(field_id, value, account_id_override = nil, signer_access_code:
|
|
229
|
+
def validate(field_id, value, account_id_override = nil, signer_access_code: WORKSPACE)
|
|
225
230
|
acc_id = account_id(account_id_override)
|
|
226
231
|
fid = require_id(field_id, 'Field ID')
|
|
227
232
|
raise ValidationError.new('Field value is required') if value.nil?
|
|
228
233
|
|
|
229
|
-
access_code =
|
|
234
|
+
access_code = field_access_code(signer_access_code)
|
|
230
235
|
|
|
231
236
|
call('Failed to validate field value') do
|
|
232
237
|
http_post("accounts/#{acc_id}/fields/#{fid}/validate", body_params(value: value),
|
|
@@ -236,12 +241,12 @@ module Assinafy
|
|
|
236
241
|
|
|
237
242
|
# Validate many `{ field_id:, value: }` pairs in a single call.
|
|
238
243
|
#
|
|
239
|
-
# As with {#validate},
|
|
240
|
-
#
|
|
244
|
+
# As with {#validate}, omitting `signer_access_code:` uses the workspace
|
|
245
|
+
# credentials; giving it requires a non-blank code and sends only the code.
|
|
241
246
|
#
|
|
242
247
|
# @param values [Array<Hash>]
|
|
243
248
|
# @param account_id_override [String, nil]
|
|
244
|
-
# @param signer_access_code [String
|
|
249
|
+
# @param signer_access_code [String] optional keyword; nil or blank raises
|
|
245
250
|
# @return [Array<Hash>] one validation Hash per input, each carrying its `field_id`
|
|
246
251
|
# @raise [Assinafy::ApiError] on an unsuccessful API response
|
|
247
252
|
# @raise [Assinafy::NetworkError] on transport or TLS failure
|
|
@@ -265,10 +270,10 @@ module Assinafy
|
|
|
265
270
|
# # { "field_id" => "field-id-2", "type" => "email",
|
|
266
271
|
# # "success" => true, "error_message" => "" }
|
|
267
272
|
# # ]
|
|
268
|
-
def validate_multiple(values, account_id_override = nil, signer_access_code:
|
|
273
|
+
def validate_multiple(values, account_id_override = nil, signer_access_code: WORKSPACE)
|
|
269
274
|
acc_id = account_id(account_id_override)
|
|
270
275
|
list = require_array(values, 'Field values')
|
|
271
|
-
access_code =
|
|
276
|
+
access_code = field_access_code(signer_access_code)
|
|
272
277
|
|
|
273
278
|
call_array('Failed to validate field values') do
|
|
274
279
|
http_post("accounts/#{acc_id}/fields/validate-multiple",
|
|
@@ -308,6 +313,16 @@ module Assinafy
|
|
|
308
313
|
|
|
309
314
|
private
|
|
310
315
|
|
|
316
|
+
def field_access_code(signer_access_code)
|
|
317
|
+
signer_access_code.equal?(WORKSPACE) ? nil : require_signer_access_code(signer_access_code)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# `regex` is nullable: an explicit nil is sent as JSON null to clear it.
|
|
321
|
+
def with_explicit_regex(raw, body)
|
|
322
|
+
body['regex'] = nil if [:regex, 'regex'].any? { |key| raw.key?(key) && raw[key].nil? }
|
|
323
|
+
body
|
|
324
|
+
end
|
|
325
|
+
|
|
311
326
|
def field_value_payload(item)
|
|
312
327
|
payload = require_payload(item, 'Field value')
|
|
313
328
|
field_id = payload.key?(:field_id) ? payload[:field_id] : payload['field_id']
|