clicksign-ruby-sdk 0.1.1
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/README.md +535 -0
- data/lib/clicksign/client.rb +143 -0
- data/lib/clicksign/configuration.rb +29 -0
- data/lib/clicksign/error_handler.rb +56 -0
- data/lib/clicksign/errors.rb +53 -0
- data/lib/clicksign/instrumentation.rb +32 -0
- data/lib/clicksign/json_api/atomic_results_parser.rb +61 -0
- data/lib/clicksign/json_api/bulk_operations_client.rb +95 -0
- data/lib/clicksign/json_api/operations/bulk_requirement.rb +89 -0
- data/lib/clicksign/json_api/operations.rb +38 -0
- data/lib/clicksign/json_api/parser.rb +31 -0
- data/lib/clicksign/json_api/query_builder.rb +45 -0
- data/lib/clicksign/json_api/serializer.rb +14 -0
- data/lib/clicksign/resource.rb +263 -0
- data/lib/clicksign/resources/acceptance_term/whatsapp.rb +12 -0
- data/lib/clicksign/resources/access_control_list.rb +35 -0
- data/lib/clicksign/resources/auto_signature/term.rb +12 -0
- data/lib/clicksign/resources/envelope_bulk_creation.rb +9 -0
- data/lib/clicksign/resources/folder.rb +22 -0
- data/lib/clicksign/resources/group.rb +21 -0
- data/lib/clicksign/resources/membership.rb +21 -0
- data/lib/clicksign/resources/notarial/bulk_requirement.rb +67 -0
- data/lib/clicksign/resources/notarial/document.rb +40 -0
- data/lib/clicksign/resources/notarial/envelope.rb +80 -0
- data/lib/clicksign/resources/notarial/event.rb +20 -0
- data/lib/clicksign/resources/notarial/requirement.rb +63 -0
- data/lib/clicksign/resources/notarial/signature_watcher.rb +39 -0
- data/lib/clicksign/resources/notarial/signer.rb +56 -0
- data/lib/clicksign/resources/template.rb +13 -0
- data/lib/clicksign/resources/template_field.rb +9 -0
- data/lib/clicksign/resources/user.rb +15 -0
- data/lib/clicksign/resources/webhook.rb +9 -0
- data/lib/clicksign/services.rb +35 -0
- data/lib/clicksign/version.rb +5 -0
- data/lib/clicksign/webhook.rb +41 -0
- data/lib/clicksign.rb +73 -0
- metadata +81 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7c3da29608ca505f7705b718fa579adb1d346bf8000e50fc87382616833e645f
|
|
4
|
+
data.tar.gz: e92995a59c7939aac989d9a165956b9309523a505178a2f79f168e0b6b6578d5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f1ccd82b00a37e3f88cd0e8fc65b0279500750ad8c008c8e98fb50451d22770440809e7d0308d01753e6a66c78dab72f151e949fcd3cfa9249dc9f10e9a5c1dd
|
|
7
|
+
data.tar.gz: c80bdcec1a9c6f7eb47b649cb17756c6cfde384d4fddfca8239b4efa2e26ccc415aa407aa7d83de852706b11571750ef94e1c8d2efa5e3265cb43efda3f9247d
|
data/README.md
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# Clicksign Ruby SDK
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/clicksign-ruby-sdk)
|
|
4
|
+
[](https://github.com/djosino/clicksign-ruby-sdk/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](clicksign-ruby-sdk.gemspec)
|
|
7
|
+
|
|
8
|
+
Cliente Ruby oficial para a [API v3 da Clicksign](https://developers.clicksign.com/) (JSON:API). Permite criar envelopes, adicionar documentos e signatários, configurar requisitos de assinatura, webhooks e demais recursos da plataforma com uma API idiomática em Ruby.
|
|
9
|
+
|
|
10
|
+
**Requisitos:** Ruby >= 3.0 · dependências de runtime: apenas biblioteca padrão (`net/http`, `json`).
|
|
11
|
+
|
|
12
|
+
**Documentação da API:** [Sandbox](https://sandbox.clicksign.com/api/v3) · [Produção](https://app.clicksign.com/api/v3) · Referência interna: [`docs/SPEC.md`](docs/SPEC.md)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Índice
|
|
17
|
+
|
|
18
|
+
- [Instalação](#instalação)
|
|
19
|
+
- [Configuração](#configuração)
|
|
20
|
+
- [Início rápido](#início-rápido)
|
|
21
|
+
- [Fluxo de assinatura (notarial)](#fluxo-de-assinatura-notarial)
|
|
22
|
+
- [Filtros, ordenação e paginação](#filtros-ordenação-e-paginação)
|
|
23
|
+
- [Outros recursos](#outros-recursos)
|
|
24
|
+
- [Tratamento de erros](#tratamento-de-erros)
|
|
25
|
+
- [Ambientes](#ambientes)
|
|
26
|
+
- [Desenvolvimento](#desenvolvimento)
|
|
27
|
+
- [Licença](#licença)
|
|
28
|
+
|
|
29
|
+
> **Exemplo passo a passo:** [`docs/WORKFLOW.md`](docs/WORKFLOW.md) — fluxo completo de envelope → documento → signatário → requisitos → ativação → notificação.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Instalação
|
|
34
|
+
|
|
35
|
+
Adicione ao `Gemfile`:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
source 'https://rubygems.org'
|
|
39
|
+
|
|
40
|
+
gem 'clicksign-ruby-sdk'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Depois:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bundle install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Ou instale diretamente:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gem install clicksign-ruby-sdk
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Configuração
|
|
58
|
+
|
|
59
|
+
Configure a chave de API e a URL base **uma vez** no boot da aplicação (initializer, `config/initializers/clicksign.rb`, script, etc.):
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
require 'clicksign'
|
|
63
|
+
|
|
64
|
+
Clicksign.configure do |c|
|
|
65
|
+
c.api_key = ENV.fetch('CLICKSIGN_API_KEY')
|
|
66
|
+
c.base_url = ENV.fetch('CLICKSIGN_API_BASE_URL', 'https://sandbox.clicksign.com/api/v3')
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A API usa o header `Authorization: <seu-token>` **sem** o prefixo `Bearer`.
|
|
71
|
+
|
|
72
|
+
> **Segurança:** não commite tokens no código. Use variáveis de ambiente ou cofre de secrets (Rails credentials, etc.).
|
|
73
|
+
|
|
74
|
+
Para testar interativamente no console da gem:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
CLICKSIGN_API_KEY=seu-token bundle exec ruby bin/console
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Início rápido
|
|
83
|
+
|
|
84
|
+
Listar envelopes em rascunho e criar um novo:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require 'clicksign'
|
|
88
|
+
|
|
89
|
+
Clicksign.configure do |c|
|
|
90
|
+
c.api_key = ENV['CLICKSIGN_API_KEY']
|
|
91
|
+
c.base_url = 'https://sandbox.clicksign.com/api/v3'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
Envelope = Clicksign::Resources::Notarial::Envelope
|
|
95
|
+
|
|
96
|
+
# Listar com filtro
|
|
97
|
+
drafts = Envelope.filter(status: 'draft').to_a
|
|
98
|
+
puts drafts.map { |e| [e.id, e.name, e.status] }
|
|
99
|
+
|
|
100
|
+
# Criar envelope (status inicial: draft)
|
|
101
|
+
envelope = Envelope.create(
|
|
102
|
+
name: 'Contrato de prestação de serviços',
|
|
103
|
+
locale: 'pt-BR',
|
|
104
|
+
auto_close: true
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
puts envelope.id # UUID do envelope
|
|
108
|
+
puts envelope.status # => "draft"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Buscar, atualizar e excluir:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
found = Envelope.retrieve(envelope.id)
|
|
115
|
+
found.update(name: 'Contrato — revisão 2')
|
|
116
|
+
|
|
117
|
+
found.delete
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Fluxo de assinatura (notarial)
|
|
123
|
+
|
|
124
|
+
Namespace principal: `Clicksign::Resources::Notarial`.
|
|
125
|
+
|
|
126
|
+
### 1. Envelope
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
Envelope = Clicksign::Resources::Notarial::Envelope
|
|
130
|
+
Folder = Clicksign::Resources::Folder
|
|
131
|
+
|
|
132
|
+
# Opcional: associar a uma pasta
|
|
133
|
+
folder = Folder.filter(in_root: true).first
|
|
134
|
+
envelope = Envelope.create(
|
|
135
|
+
name: 'Proposta comercial #1042',
|
|
136
|
+
folder_id: folder&.id,
|
|
137
|
+
deadline_at: '2026-12-31T23:59:59.000-03:00',
|
|
138
|
+
default_subject: 'Documentos para assinatura',
|
|
139
|
+
default_message: 'Por favor, assine os documentos em anexo.'
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 2. Documento
|
|
144
|
+
|
|
145
|
+
Envie o PDF em Base64, via URL ou a partir de um template:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
Document = Clicksign::Resources::Notarial::Document
|
|
149
|
+
|
|
150
|
+
document = Document.create(
|
|
151
|
+
envelope_id: envelope.id,
|
|
152
|
+
filename: 'contrato.pdf',
|
|
153
|
+
content_base64: 'data:application/pdf;base64,JVBERi0xLjQK...'
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Alternativas (mutuamente exclusivas na API):
|
|
157
|
+
# content_url: 'https://exemplo.com/arquivo.pdf'
|
|
158
|
+
# template: { key: template_id, data: {} } # filename deve ser .doc ou .docx
|
|
159
|
+
|
|
160
|
+
# Listar documentos do envelope
|
|
161
|
+
Envelope.list_documents(envelope.id).each do |doc|
|
|
162
|
+
puts "#{doc.id} — #{doc.filename} (#{doc.status})"
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 3. Signatário
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
Signer = Clicksign::Resources::Notarial::Signer
|
|
170
|
+
|
|
171
|
+
signer = Signer.create(
|
|
172
|
+
envelope_id: envelope.id,
|
|
173
|
+
name: 'Maria Silva',
|
|
174
|
+
email: 'maria.silva@example.com',
|
|
175
|
+
phone_number: '11999998888',
|
|
176
|
+
has_documentation: true,
|
|
177
|
+
documentation: '12345678909',
|
|
178
|
+
refusable: true,
|
|
179
|
+
communicate_events: {
|
|
180
|
+
signature_request: 'email', # canal de convite para assinar
|
|
181
|
+
signature_reminder: 'email', # canal de lembrete automático
|
|
182
|
+
document_signed: 'email', # canal de confirmação pós-assinatura
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Reenviar notificação por e-mail
|
|
187
|
+
signer.notify(message: 'Lembrete: seu documento aguarda assinatura.')
|
|
188
|
+
|
|
189
|
+
# Ou via classe
|
|
190
|
+
Signer.notify(signer.id, envelope_id: envelope.id, message: 'Lembrete de assinatura')
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 4. Requisitos (requirements)
|
|
194
|
+
|
|
195
|
+
Cada requisito associa um signatário a um documento com uma ação (`agree`, `provide_evidence`, `rubricate`). O envelope precisa estar em **draft** para criar ou remover requisitos.
|
|
196
|
+
|
|
197
|
+
#### 4.1 Endpoint padrão
|
|
198
|
+
|
|
199
|
+
`POST /envelopes/:envelope_id/requirements` — uma operação por requisição.
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
Requirement = Clicksign::Resources::Notarial::Requirement
|
|
203
|
+
|
|
204
|
+
rels = {
|
|
205
|
+
document: { data: { type: 'documents', id: document.id } },
|
|
206
|
+
signer: { data: { type: 'signers', id: signer.id } },
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Concordância (agree)
|
|
210
|
+
agree = Requirement.create(
|
|
211
|
+
envelope_id: envelope.id,
|
|
212
|
+
action: 'agree',
|
|
213
|
+
role: 'sign',
|
|
214
|
+
relationships: rels
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Evidência de autenticação (ex.: e-mail)
|
|
218
|
+
Requirement.create(
|
|
219
|
+
envelope_id: envelope.id,
|
|
220
|
+
action: 'provide_evidence',
|
|
221
|
+
auth: 'email',
|
|
222
|
+
relationships: rels
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Rubrica em todas as páginas
|
|
226
|
+
Requirement.create(
|
|
227
|
+
envelope_id: envelope.id,
|
|
228
|
+
action: 'rubricate',
|
|
229
|
+
pages: 'all',
|
|
230
|
+
relationships: rels
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Rubrica em campo específico do documento
|
|
234
|
+
Requirement.create(
|
|
235
|
+
envelope_id: envelope.id,
|
|
236
|
+
action: 'rubricate',
|
|
237
|
+
rubric_field: 'campo_rubrica_1',
|
|
238
|
+
relationships: rels
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Consultar
|
|
242
|
+
Requirement.retrieve(agree.id, envelope_id: envelope.id)
|
|
243
|
+
Envelope.list_requirements(envelope.id, 'signer.key': signer.id)
|
|
244
|
+
Requirement.list_for_document(document.id)
|
|
245
|
+
Requirement.list_for_signer(signer.id)
|
|
246
|
+
|
|
247
|
+
# Remover (envelope em draft)
|
|
248
|
+
agree.delete
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### 4.2 Operações em lote (bulk)
|
|
252
|
+
|
|
253
|
+
`POST /envelopes/:envelope_id/bulk_requirements` — várias operações em uma requisição (`atomic:operations` → `atomic:results`). Indicado quando você monta o setup completo de uma vez.
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
BulkRequirement = Clicksign::Resources::Notarial::BulkRequirement
|
|
257
|
+
|
|
258
|
+
response = BulkRequirement.create(envelope_id: envelope.id) do |ops|
|
|
259
|
+
ops.add_agree(
|
|
260
|
+
signer_id: signer.id,
|
|
261
|
+
document_id: document.id,
|
|
262
|
+
role: 'sign'
|
|
263
|
+
)
|
|
264
|
+
ops.add_provide_evidence(
|
|
265
|
+
signer_id: signer.id,
|
|
266
|
+
document_id: document.id,
|
|
267
|
+
auth: 'email'
|
|
268
|
+
)
|
|
269
|
+
ops.add_rubricate(
|
|
270
|
+
signer_id: signer.id,
|
|
271
|
+
document_id: document.id,
|
|
272
|
+
pages: 'all'
|
|
273
|
+
)
|
|
274
|
+
# ops.remove(requirement_id: requisito_antigo.id)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
if response.success?
|
|
278
|
+
response.requirements.each { |r| puts "OK: #{r.id} (#{r.action})" }
|
|
279
|
+
else
|
|
280
|
+
response.failures.each do |failure|
|
|
281
|
+
puts "Falha na operação #{failure.index}: #{failure.errors}"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
| Abordagem | Endpoint | Quando usar |
|
|
287
|
+
|-----------|----------|-------------|
|
|
288
|
+
| **4.1 Padrão** | `/requirements` | Criar/alterar requisitos um a um, fluxos incrementais |
|
|
289
|
+
| **4.2 Bulk** | `/bulk_requirements` | Várias ações na mesma chamada; tratar sucesso/falha por slot |
|
|
290
|
+
|
|
291
|
+
### 5. Ativar o envelope
|
|
292
|
+
|
|
293
|
+
Atualize o `status` para `running` via PATCH:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
activated = envelope.update(status: 'running')
|
|
297
|
+
puts activated.status # => "running"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 6. Observadores e eventos
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
SignatureWatcher = Clicksign::Resources::Notarial::SignatureWatcher
|
|
304
|
+
Event = Clicksign::Resources::Notarial::Event
|
|
305
|
+
|
|
306
|
+
watcher = SignatureWatcher.create(
|
|
307
|
+
envelope_id: envelope.id,
|
|
308
|
+
email: 'compliance@empresa.com',
|
|
309
|
+
kind: 'all_steps',
|
|
310
|
+
attach_documents_enabled: true
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Eventos do envelope
|
|
314
|
+
Envelope.list_events(envelope.id)
|
|
315
|
+
|
|
316
|
+
# Eventos de um documento
|
|
317
|
+
Document.list_events(document.id, envelope_id: envelope.id)
|
|
318
|
+
|
|
319
|
+
# Criar evento customizado no documento
|
|
320
|
+
Event.create_for_document(
|
|
321
|
+
envelope_id: envelope.id,
|
|
322
|
+
document_id: document.id,
|
|
323
|
+
name: 'custom',
|
|
324
|
+
data: { description: 'Etapa interna concluída' }
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Filtros, ordenação e paginação
|
|
331
|
+
|
|
332
|
+
A API de listagem é chainable:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
Envelope
|
|
336
|
+
.filter(status: 'running', name: 'Contrato')
|
|
337
|
+
.order('-created')
|
|
338
|
+
.page(1)
|
|
339
|
+
.per(20)
|
|
340
|
+
.to_a
|
|
341
|
+
|
|
342
|
+
# Atalho quando há poucos filtros
|
|
343
|
+
Template.filter(name: 'NDA padrão').first
|
|
344
|
+
|
|
345
|
+
# Navegação no resultado
|
|
346
|
+
Envelope.order('-created').first # mais recente
|
|
347
|
+
Envelope.order('-created').last # mais antigo
|
|
348
|
+
Envelope.filter(status: 'draft').count # total de rascunhos
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Atributos dos objetos retornados são acessados como métodos ou por chave string:
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
envelope.name # método gerado dinamicamente
|
|
355
|
+
envelope['name'] # equivalente via operador []
|
|
356
|
+
envelope['status'] # útil quando a chave é uma variável
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
O `QueryProxy` inclui `Enumerable`, então `each`, `map`, `select` e afins funcionam diretamente na chain sem precisar de `.to_a`:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
Envelope.filter(status: 'draft').each { |e| puts e.id }
|
|
363
|
+
Envelope.filter(status: 'running').map(&:name)
|
|
364
|
+
Envelope.order('-created').select { |e| e.auto_close }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Outros recursos
|
|
370
|
+
|
|
371
|
+
| Recurso | Classe | Exemplo |
|
|
372
|
+
|---------|--------|---------|
|
|
373
|
+
| Webhook | `Clicksign::Resources::Webhook` | `Webhook.create(endpoint: 'https://...', events: ['envelope.completed'], status: 'active')` |
|
|
374
|
+
| Pasta | `Clicksign::Resources::Folder` | `Folder.create(name: 'Contratos 2026', folder_id: pai&.id)` |
|
|
375
|
+
| Template | `Clicksign::Resources::Template` | `Template.list` · `Template.list_template_fields(id)` |
|
|
376
|
+
| Usuário | `Clicksign::Resources::User` | `User.me` · `User.filter(email: '...')` |
|
|
377
|
+
| Membership | `Clicksign::Resources::Membership` | `Membership.create(role: 'member', user_id: user.id)` |
|
|
378
|
+
| Grupo | `Clicksign::Resources::Group` | `Group.add_users(group_id, [user.id])` |
|
|
379
|
+
| ACL pasta/grupo | `Clicksign::Resources::AccessControlList` | `AccessControlList.create(folder_id:, group_id:)` |
|
|
380
|
+
| Criação em lote de envelopes | `Clicksign::Resources::EnvelopeBulkCreation` | Ver exemplo abaixo |
|
|
381
|
+
| Auto assinatura | `Clicksign::Resources::AutoSignature::Term` | `AutoSignature::Term.create(...)` |
|
|
382
|
+
| Termo WhatsApp | `Clicksign::Resources::AcceptanceTerm::Whatsapp` | `AcceptanceTerm::Whatsapp.list` |
|
|
383
|
+
|
|
384
|
+
Exemplo de webhook:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
Webhook = Clicksign::Resources::Webhook
|
|
388
|
+
|
|
389
|
+
hook = Webhook.create(
|
|
390
|
+
endpoint: 'https://minhaapp.com/webhooks/clicksign',
|
|
391
|
+
events: %w[sign close cancel add_signer],
|
|
392
|
+
status: 'active'
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
hook.update(status: 'inactive')
|
|
396
|
+
hook.delete
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Exemplo de usuário e membership:
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
User = Clicksign::Resources::User
|
|
403
|
+
Membership = Clicksign::Resources::Membership
|
|
404
|
+
|
|
405
|
+
eu = User.me
|
|
406
|
+
puts "#{eu.name} <#{eu.email}>"
|
|
407
|
+
|
|
408
|
+
novo = User.create(
|
|
409
|
+
name: 'João Integração',
|
|
410
|
+
email: 'joao.integracao@example.com',
|
|
411
|
+
phone_number: '11988887777'
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
Membership.create(role: 'admin', user_id: novo.id)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Criação de envelope em lote (job assíncrono):
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
EnvelopeBulkCreation = Clicksign::Resources::EnvelopeBulkCreation
|
|
421
|
+
|
|
422
|
+
job = EnvelopeBulkCreation.create(
|
|
423
|
+
envelope: { name: 'Contrato Lote', locale: 'pt-BR', auto_close: true },
|
|
424
|
+
document: {
|
|
425
|
+
filename: 'contrato.docx',
|
|
426
|
+
content_base64: 'data:application/msword;base64,...'
|
|
427
|
+
},
|
|
428
|
+
signers: [
|
|
429
|
+
{
|
|
430
|
+
name: 'Carlos',
|
|
431
|
+
email: 'carlos@example.com',
|
|
432
|
+
requirements: [{ action: 'agree', role: 'sign', auth: 'email' }]
|
|
433
|
+
}
|
|
434
|
+
]
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
puts job.job_id # UUID do job enfileirado
|
|
438
|
+
puts job.enqueued_at # timestamp de enfileiramento
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Controle de acesso em pasta:
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
AccessControlList = Clicksign::Resources::AccessControlList
|
|
445
|
+
|
|
446
|
+
AccessControlList.create(folder_id: folder.id, group_id: group.id)
|
|
447
|
+
AccessControlList.destroy(folder_id: folder.id, group_id: group.id)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Tratamento de erros
|
|
453
|
+
|
|
454
|
+
Erros HTTP são convertidos em exceções antes de chegar ao seu código:
|
|
455
|
+
|
|
456
|
+
| HTTP | Exceção |
|
|
457
|
+
|------|---------|
|
|
458
|
+
| 401, 403 | `Clicksign::AuthenticationError` |
|
|
459
|
+
| 404 | `Clicksign::NotFoundError` |
|
|
460
|
+
| 400, 422 | `Clicksign::ValidationError` |
|
|
461
|
+
| 409 | `Clicksign::ConflictError` |
|
|
462
|
+
| 429 | `Clicksign::RateLimitError` |
|
|
463
|
+
| 5xx | `Clicksign::ServerError` |
|
|
464
|
+
| Timeout / conexão | `Clicksign::TimeoutError` |
|
|
465
|
+
|
|
466
|
+
Exemplo:
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
begin
|
|
470
|
+
Envelope.retrieve('00000000-0000-0000-0000-000000000000')
|
|
471
|
+
rescue Clicksign::NotFoundError
|
|
472
|
+
puts 'Envelope não encontrado'
|
|
473
|
+
rescue Clicksign::ValidationError => e
|
|
474
|
+
puts "Dados inválidos: #{e.message}"
|
|
475
|
+
rescue Clicksign::AuthenticationError
|
|
476
|
+
puts 'Verifique CLICKSIGN_API_KEY'
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Operações em lote (`BulkRequirement`) podem retornar falhas **por slot** em `response.failures` sem lançar exceção, quando a API responde com `atomic:results` parcial.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Ambientes
|
|
485
|
+
|
|
486
|
+
| Ambiente | `base_url` |
|
|
487
|
+
|----------|------------|
|
|
488
|
+
| Sandbox | `https://sandbox.clicksign.com/api/v3` |
|
|
489
|
+
| Produção | `https://app.clicksign.com/api/v3` |
|
|
490
|
+
|
|
491
|
+
O padrão da gem em `Clicksign::Configuration` é produção (`app.clicksign.com`). Para desenvolvimento, defina explicitamente o sandbox:
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
Clicksign.configure do |c|
|
|
495
|
+
c.api_key = ENV['CLICKSIGN_API_KEY']
|
|
496
|
+
c.base_url = 'https://sandbox.clicksign.com/api/v3'
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Gere tokens de API no painel da Clicksign do ambiente correspondente.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Desenvolvimento
|
|
505
|
+
|
|
506
|
+
Clone o repositório e instale dependências de desenvolvimento:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
bundle install
|
|
510
|
+
bundle exec rspec
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
| Variável | Uso |
|
|
514
|
+
|----------|-----|
|
|
515
|
+
| `CLICKSIGN_API_KEY` | Token para testes contra sandbox (opcional) |
|
|
516
|
+
| `CLICKSIGN_API_BASE_URL` | URL da API (padrão sandbox nos specs de integração legados) |
|
|
517
|
+
|
|
518
|
+
A suíte principal usa **WebMock** e não exige rede. Alguns specs antigos ainda podem usar **VCR** com gravação no sandbox.
|
|
519
|
+
|
|
520
|
+
Estrutura relevante:
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
lib/clicksign/
|
|
524
|
+
client.rb # HTTP (GET, POST, PATCH, DELETE)
|
|
525
|
+
resource.rb # CRUD base, filtros, nested lists
|
|
526
|
+
resources/notarial/ # Envelope, Document, Signer, Requirement, ...
|
|
527
|
+
json_api/ # Serializer, Parser, bulk operations
|
|
528
|
+
docs/SPEC.md # mapa completo de resources e rotas
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Licença
|
|
534
|
+
|
|
535
|
+
MIT — ver [`clicksign-ruby-sdk.gemspec`](clicksign-ruby-sdk.gemspec).
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Clicksign
|
|
8
|
+
class Client
|
|
9
|
+
HEADERS = {
|
|
10
|
+
'Content-Type' => 'application/vnd.api+json',
|
|
11
|
+
'Accept' => 'application/vnd.api+json',
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(api_key:, base_url:, open_timeout: 2, read_timeout: 10,
|
|
15
|
+
write_timeout: 10, max_retries: 0)
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@open_timeout = open_timeout
|
|
19
|
+
@read_timeout = read_timeout
|
|
20
|
+
@write_timeout = write_timeout
|
|
21
|
+
@max_retries = max_retries
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get(path, params: {})
|
|
25
|
+
uri = build_uri(path, params)
|
|
26
|
+
execute_with_retry(Net::HTTP::Get.new(uri, headers), uri)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def post(path, body:)
|
|
30
|
+
uri = build_uri(path)
|
|
31
|
+
request = Net::HTTP::Post.new(uri, headers)
|
|
32
|
+
request.body = body.to_json
|
|
33
|
+
execute_with_retry(request, uri)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def patch(path, body:)
|
|
37
|
+
uri = build_uri(path)
|
|
38
|
+
request = Net::HTTP::Patch.new(uri, headers)
|
|
39
|
+
request.body = body.to_json
|
|
40
|
+
execute_with_retry(request, uri)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete(path, body: nil)
|
|
44
|
+
uri = build_uri(path)
|
|
45
|
+
request = Net::HTTP::Delete.new(uri, headers)
|
|
46
|
+
request.body = body.to_json if body
|
|
47
|
+
execute_with_retry(request, uri)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def headers
|
|
53
|
+
HEADERS.merge('Authorization' => @api_key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_uri(path, params = {})
|
|
57
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
58
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
59
|
+
uri
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def execute_with_retry(request, uri)
|
|
63
|
+
attempts = 0
|
|
64
|
+
begin
|
|
65
|
+
attempts += 1
|
|
66
|
+
execute_once(request, uri, attempt: attempts)
|
|
67
|
+
rescue Clicksign::TimeoutError, Clicksign::RateLimitError,
|
|
68
|
+
Clicksign::ServerError => e
|
|
69
|
+
raise unless e.retryable? && attempts <= @max_retries
|
|
70
|
+
|
|
71
|
+
delay = backoff_delay(attempts)
|
|
72
|
+
Instrumentation.publish(:retry, {
|
|
73
|
+
method: request.method.downcase.to_sym,
|
|
74
|
+
path: resource_path(uri),
|
|
75
|
+
attempt: attempts,
|
|
76
|
+
max_retries: @max_retries,
|
|
77
|
+
error: e,
|
|
78
|
+
wait_ms: (delay * 1000).round,
|
|
79
|
+
})
|
|
80
|
+
sleep(delay)
|
|
81
|
+
retry
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def execute_once(request, uri, attempt: 1)
|
|
86
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
87
|
+
context = { method: request.method.downcase.to_sym, path: resource_path(uri),
|
|
88
|
+
attempt: attempt }
|
|
89
|
+
response = http_request(request, uri)
|
|
90
|
+
handle_response(response, context, start)
|
|
91
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
|
|
92
|
+
handle_network_error(e, context, elapsed_ms(start))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def http_request(request, uri)
|
|
96
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
97
|
+
use_ssl: uri.scheme == 'https',
|
|
98
|
+
open_timeout: @open_timeout,
|
|
99
|
+
read_timeout: @read_timeout,
|
|
100
|
+
write_timeout: @write_timeout,
|
|
101
|
+
&proc { |http| http.request(request) })
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def handle_response(response, context, start)
|
|
105
|
+
duration = elapsed_ms(start)
|
|
106
|
+
status = response.code.to_i
|
|
107
|
+
begin
|
|
108
|
+
ErrorHandler.call(response)
|
|
109
|
+
rescue Error => e
|
|
110
|
+
publish_event(:request, context, status: status, duration_ms: duration)
|
|
111
|
+
publish_event(:error, context, error: e, status: status, duration_ms: duration)
|
|
112
|
+
raise
|
|
113
|
+
end
|
|
114
|
+
publish_event(:request, context, status: status, duration_ms: duration)
|
|
115
|
+
return nil if response.body.nil? || response.body.empty?
|
|
116
|
+
|
|
117
|
+
JSON.parse(response.body)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def handle_network_error(error, context, duration)
|
|
121
|
+
err = TimeoutError.new(error.message)
|
|
122
|
+
publish_event(:error, context, error: err, status: nil, duration_ms: duration)
|
|
123
|
+
raise err, error.message, error.backtrace
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def publish_event(type, context, extra)
|
|
127
|
+
Instrumentation.publish(type, context.merge(extra))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def elapsed_ms(start)
|
|
131
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round(1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resource_path(uri)
|
|
135
|
+
base = URI.parse(@base_url).path
|
|
136
|
+
uri.path.delete_prefix(base)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def backoff_delay(attempt)
|
|
140
|
+
[0.5 * (2**(attempt - 1)), 30].min
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clicksign
|
|
4
|
+
class Configuration
|
|
5
|
+
ENVIRONMENTS = {
|
|
6
|
+
sandbox: 'https://sandbox.clicksign.com/api/v3',
|
|
7
|
+
production: 'https://app.clicksign.com/api/v3',
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
attr_accessor :api_key, :base_url, :open_timeout, :read_timeout,
|
|
11
|
+
:write_timeout, :max_retries
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@base_url = 'https://app.clicksign.com/api/v3'
|
|
15
|
+
@open_timeout = 2
|
|
16
|
+
@read_timeout = 10
|
|
17
|
+
@write_timeout = 10
|
|
18
|
+
@max_retries = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def environment=(env)
|
|
22
|
+
url = ENVIRONMENTS.fetch(env.to_sym) do
|
|
23
|
+
raise ArgumentError,
|
|
24
|
+
"Unknown environment: #{env}. Valid: #{ENVIRONMENTS.keys.join(', ')}"
|
|
25
|
+
end
|
|
26
|
+
self.base_url = url
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|