dinie-sdk-sandbox 1.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/CHANGELOG.md +40 -0
- data/LICENSE +21 -0
- data/README.md +280 -0
- data/lib/dinie/generated/api_version.rb +8 -0
- data/lib/dinie/generated/client.rb +96 -0
- data/lib/dinie/generated/errors/registry.rb +40 -0
- data/lib/dinie/generated/events/base.rb +11 -0
- data/lib/dinie/generated/events/credit_offer.rb +56 -0
- data/lib/dinie/generated/events/customer_created.rb +42 -0
- data/lib/dinie/generated/events/customer_denied.rb +39 -0
- data/lib/dinie/generated/events/customer_kyc_updated.rb +36 -0
- data/lib/dinie/generated/events/customer_status.rb +48 -0
- data/lib/dinie/generated/events/deserializers.rb +35 -0
- data/lib/dinie/generated/events/loan_active.rb +35 -0
- data/lib/dinie/generated/events/loan_created.rb +38 -0
- data/lib/dinie/generated/events/loan_payment_received.rb +46 -0
- data/lib/dinie/generated/events/loan_processing.rb +37 -0
- data/lib/dinie/generated/events/loan_signature_received.rb +48 -0
- data/lib/dinie/generated/events/loan_status.rb +73 -0
- data/lib/dinie/generated/events.rb +4 -0
- data/lib/dinie/generated/resources/banks.rb +25 -0
- data/lib/dinie/generated/resources/biometrics.rb +27 -0
- data/lib/dinie/generated/resources/credentials.rb +56 -0
- data/lib/dinie/generated/resources/credit_offers.rb +59 -0
- data/lib/dinie/generated/resources/customers.rb +200 -0
- data/lib/dinie/generated/resources/loans.rb +70 -0
- data/lib/dinie/generated/resources/webhook_endpoints.rb +97 -0
- data/lib/dinie/generated/resources.rb +9 -0
- data/lib/dinie/generated/types/bank.rb +17 -0
- data/lib/dinie/generated/types/biometrics_session.rb +16 -0
- data/lib/dinie/generated/types/biometrics_session_exchange_response.rb +23 -0
- data/lib/dinie/generated/types/credential.rb +52 -0
- data/lib/dinie/generated/types/credit_offer.rb +62 -0
- data/lib/dinie/generated/types/customer.rb +46 -0
- data/lib/dinie/generated/types/customer_bank_account.rb +33 -0
- data/lib/dinie/generated/types/ids.rb +18 -0
- data/lib/dinie/generated/types/kyc.rb +458 -0
- data/lib/dinie/generated/types/kyc_attachment_response.rb +16 -0
- data/lib/dinie/generated/types/loan.rb +51 -0
- data/lib/dinie/generated/types/money.rb +4 -0
- data/lib/dinie/generated/types/simulation.rb +35 -0
- data/lib/dinie/generated/types/transaction.rb +43 -0
- data/lib/dinie/generated/types/webhook_endpoint.rb +52 -0
- data/lib/dinie/generated/types/webhook_secret_rotation.rb +17 -0
- data/lib/dinie/generated/types.rb +18 -0
- data/lib/dinie/runtime/errors.rb +295 -0
- data/lib/dinie/runtime/http.rb +327 -0
- data/lib/dinie/runtime/idempotency.rb +34 -0
- data/lib/dinie/runtime/logger.rb +326 -0
- data/lib/dinie/runtime/model.rb +162 -0
- data/lib/dinie/runtime/multipart.rb +77 -0
- data/lib/dinie/runtime/paginator.rb +164 -0
- data/lib/dinie/runtime/rate_limit.rb +150 -0
- data/lib/dinie/runtime/request_options.rb +112 -0
- data/lib/dinie/runtime/retry.rb +74 -0
- data/lib/dinie/runtime/token_manager.rb +341 -0
- data/lib/dinie/runtime/webhooks.rb +194 -0
- data/lib/dinie/version.rb +7 -0
- data/lib/dinie.rb +37 -0
- data/sig/_external/faraday.rbs +44 -0
- data/sig/dinie/generated/client.rbs +45 -0
- data/sig/dinie/generated/errors/registry.rbs +40 -0
- data/sig/dinie/generated/events/base.rbs +17 -0
- data/sig/dinie/generated/events/credit_offer.rbs +33 -0
- data/sig/dinie/generated/events/customer_created.rbs +27 -0
- data/sig/dinie/generated/events/customer_denied.rbs +25 -0
- data/sig/dinie/generated/events/customer_kyc_updated.rbs +21 -0
- data/sig/dinie/generated/events/customer_status.rbs +26 -0
- data/sig/dinie/generated/events/deserializers.rbs +9 -0
- data/sig/dinie/generated/events/loan_active.rbs +20 -0
- data/sig/dinie/generated/events/loan_created.rbs +23 -0
- data/sig/dinie/generated/events/loan_payment_received.rbs +28 -0
- data/sig/dinie/generated/events/loan_processing.rbs +23 -0
- data/sig/dinie/generated/events/loan_signature_received.rbs +30 -0
- data/sig/dinie/generated/events/loan_status.rbs +40 -0
- data/sig/dinie/generated/resources/banks.rbs +15 -0
- data/sig/dinie/generated/resources/credentials.rbs +21 -0
- data/sig/dinie/generated/resources/credit_offers.rbs +19 -0
- data/sig/dinie/generated/resources/customers.rbs +58 -0
- data/sig/dinie/generated/resources/loans.rbs +26 -0
- data/sig/dinie/generated/resources/webhook_endpoints.rbs +35 -0
- data/sig/dinie/generated/types/bank.rbs +12 -0
- data/sig/dinie/generated/types/biometrics_session.rbs +11 -0
- data/sig/dinie/generated/types/credential.rbs +26 -0
- data/sig/dinie/generated/types/credit_offer.rbs +24 -0
- data/sig/dinie/generated/types/customer.rbs +25 -0
- data/sig/dinie/generated/types/customer_bank_account.rbs +26 -0
- data/sig/dinie/generated/types/enums.rbs +66 -0
- data/sig/dinie/generated/types/ids.rbs +21 -0
- data/sig/dinie/generated/types/kyc/attachment.rbs +14 -0
- data/sig/dinie/generated/types/kyc/common.rbs +42 -0
- data/sig/dinie/generated/types/kyc/requirements.rbs +117 -0
- data/sig/dinie/generated/types/kyc/submitted.rbs +21 -0
- data/sig/dinie/generated/types/kyc/uploads.rbs +24 -0
- data/sig/dinie/generated/types/loan.rbs +32 -0
- data/sig/dinie/generated/types/money.rbs +6 -0
- data/sig/dinie/generated/types/simulation.rbs +28 -0
- data/sig/dinie/generated/types/transaction.rbs +24 -0
- data/sig/dinie/generated/types/webhook_endpoint.rbs +38 -0
- data/sig/dinie/runtime/errors.rbs +106 -0
- data/sig/dinie/runtime/http.rbs +59 -0
- data/sig/dinie/runtime/idempotency.rbs +15 -0
- data/sig/dinie/runtime/logger.rbs +89 -0
- data/sig/dinie/runtime/model.rbs +51 -0
- data/sig/dinie/runtime/multipart.rbs +25 -0
- data/sig/dinie/runtime/paginator.rbs +50 -0
- data/sig/dinie/runtime/rate_limit.rbs +46 -0
- data/sig/dinie/runtime/request_options.rbs +35 -0
- data/sig/dinie/runtime/retry.rbs +29 -0
- data/sig/dinie/runtime/token_manager.rbs +51 -0
- data/sig/dinie/runtime/webhooks.rbs +31 -0
- data/sig/dinie/version.rbs +7 -0
- metadata +316 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2a373cf8978cc140407d0d105f1c2028474ed59774e29b425c8f32428f8a4beb
|
|
4
|
+
data.tar.gz: ae691f9d8933e27c5a9c197564226372d6169c571d4460477ffd689a1160dab2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 81da4d452ff69a12f68f0054d5d681018c47871a3a35427fdc9d2d4ef50fb6c16e97ba93a38e053486671748c116cce0d3e3bd79dc57e135c940aa4c28c2d98e
|
|
7
|
+
data.tar.gz: f75696efcbb3c2d616ee42d4be9d863ad317c4e2acfe1b415c702a79f6fc14946ffdd97b930a22cefa9ccbaa0165e12b785e948f63b2cf6949011a1002502bd4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `dinie` gem are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.1.0] - 2026-06-09
|
|
11
|
+
|
|
12
|
+
## [1.0.0] — 2026-06-08
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Publishable gem** (`dinie-sdk-sandbox` on RubyGems.org): `allowed_push_host` wired;
|
|
17
|
+
`api_version` gemspec metadata (`2026-03-01`); `rubygems_mfa_required` satisfied by OIDC
|
|
18
|
+
Trusted Publishing.
|
|
19
|
+
- **`Dinie::VERSION = "1.0.0"`** — first stable public release.
|
|
20
|
+
- **api_version constant** (`lib/dinie/generated/api_version.rb`): `Dinie::Generated::API_VERSION`
|
|
21
|
+
sourced from `openapi.info.version`; User-Agent now reads it (no more `2026-05-10` literal).
|
|
22
|
+
- **YARD documentation** on every public method and class in `lib/dinie/generated/` (0 undocumented
|
|
23
|
+
methods — was 128/206 undocumented).
|
|
24
|
+
- **CI/CD workflows**: `publish.yml` (OIDC RubyGems via `rubygems/release-gem@v1`, keyless,
|
|
25
|
+
no `RUBYGEMS_API_KEY`); `tag-release.yml` (GitHub App token auto-tag on `generator-bump` PR merge).
|
|
26
|
+
- **Drift gate** in CI: hermetic `sdk-generator check --target ruby` — edit ⇒ red, revert ⇒ green.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Project scaffold: `dinie.gemspec` (private gem, Ruby >= 3.1; `faraday ~> 2.0` +
|
|
31
|
+
`net-http-persistent ~> 4.0`), `Gemfile`, `Rakefile`.
|
|
32
|
+
- `lib/dinie.rb` deterministic barrel (explicit `require_relative`, no Zeitwerk) and
|
|
33
|
+
`lib/dinie/version.rb` (`Dinie::VERSION`).
|
|
34
|
+
- `lib/dinie/runtime/` <-> `lib/dinie/generated/` directory boundary with `CODEOWNERS`
|
|
35
|
+
(human-owned runtime, bot-owned generated layer — architecture §4.2).
|
|
36
|
+
- RSpec + WebMock (zero network) test tree with a `Dinie::VERSION` smoke spec.
|
|
37
|
+
- RuboCop config, RBS `sig/` skeleton + `Steepfile`, YARD `.yardopts`.
|
|
38
|
+
- CI workflow: `bundle install` -> RuboCop -> RSpec -> Steep (informative) on Ruby
|
|
39
|
+
3.1, 3.2, 3.3.
|
|
40
|
+
- `.env.example` for the deferred (non-gate) live smoke E2E.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dinie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# dinie
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [Dinie](https://dinie.com.br) V3 API (backend-only).
|
|
4
|
+
|
|
5
|
+
A hand-written, synchronous Ruby client for the Dinie credit-as-a-service platform: OAuth2
|
|
6
|
+
client-credentials auth (handled for you), automatic retries with idempotency, cursor pagination,
|
|
7
|
+
typed errors, and webhook verification. Snake_case throughout — the wire and the Ruby surface match,
|
|
8
|
+
so there is no casing to translate.
|
|
9
|
+
|
|
10
|
+
> **Status — `1.0.0`, published to RubyGems.** The public surface is frozen against the Dinie V3
|
|
11
|
+
> contract (api-version `2026-03-01`).
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Ruby >= 3.1
|
|
16
|
+
- A Dinie API credential (`client_id` + `client_secret`)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Gemfile
|
|
22
|
+
gem "dinie-sdk-sandbox"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The SDK depends on `faraday` (with the `net-http-persistent` adapter for a real connection pool)
|
|
30
|
+
and `faraday-multipart`. Both are pulled in automatically.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require "dinie"
|
|
36
|
+
|
|
37
|
+
client = Dinie::Client.new(client_id: "dinie_ci_…", client_secret: "…")
|
|
38
|
+
|
|
39
|
+
customer = client.customers.create(
|
|
40
|
+
email: "ana@example.com",
|
|
41
|
+
phone: "+5511999999999",
|
|
42
|
+
cpf: "123.456.789-09",
|
|
43
|
+
cnpj: "12.345.678/0001-95"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
customer.id # => "cust_…"
|
|
47
|
+
customer.status # => "pending_kyc"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The client owns a connection pool and an in-memory OAuth2 token cache, so **construct it once and
|
|
51
|
+
reuse it**. Tokens are fetched and refreshed transparently — you never call the token endpoint
|
|
52
|
+
yourself.
|
|
53
|
+
|
|
54
|
+
### Configuration
|
|
55
|
+
|
|
56
|
+
Pass options to the constructor, or let the SDK read them from the environment.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
client = Dinie::Client.new(
|
|
60
|
+
client_id: "dinie_ci_…", # or ENV["DINIE_CLIENT_ID"]
|
|
61
|
+
client_secret: "…", # or ENV["DINIE_CLIENT_SECRET"]
|
|
62
|
+
base_url: nil, # or ENV["DINIE_BASE_URL"]; default https://api.dinie.com.br/api/v3
|
|
63
|
+
timeout: 30, # per-request timeout, in SECONDS
|
|
64
|
+
max_retries: 3, # retries after the first attempt
|
|
65
|
+
idempotency: true, # auto X-Idempotency-Key on POST/PATCH
|
|
66
|
+
log_level: :off # :off | :error | :warn | :info | :debug (or ENV["DINIE_LOG"])
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Variable | Used for |
|
|
71
|
+
| -------------------- | -------------------------------------------------------------- |
|
|
72
|
+
| `DINIE_CLIENT_ID` | OAuth2 client id (when `client_id:` is omitted) |
|
|
73
|
+
| `DINIE_CLIENT_SECRET`| OAuth2 client secret (when `client_secret:` is omitted) |
|
|
74
|
+
| `DINIE_BASE_URL` | API base URL incl. `/api/v3` (when `base_url:` is omitted) |
|
|
75
|
+
| `DINIE_LOG` | log level (when `log_level:` is omitted) |
|
|
76
|
+
|
|
77
|
+
Need a one-off override (a tighter timeout for a single call path)? Clone the client — the clone
|
|
78
|
+
**shares the same token cache and connection pool**, so it never triggers a re-auth:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
fast = client.with_options(timeout: 5)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Logging is opt-in and redacts credentials and PII (`authorization`, `cpf`, `cnpj`, `client_secret`,
|
|
85
|
+
`phone`, …) before anything is written.
|
|
86
|
+
|
|
87
|
+
## End-to-end flow: Customer → Credit Offer → Loan
|
|
88
|
+
|
|
89
|
+
The credit lifecycle is: register a customer, let Dinie run KYC and underwriting (you are notified by
|
|
90
|
+
webhook — see below), then read the resulting **credit offer**, **simulate** it, and **contract** a
|
|
91
|
+
loan from the accepted simulation.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
client = Dinie::Client.new(client_id: "dinie_ci_test", client_secret: "dinie_secret_test")
|
|
95
|
+
|
|
96
|
+
# 1. Register the customer.
|
|
97
|
+
customer = client.customers.create(
|
|
98
|
+
email: "ana@example.com",
|
|
99
|
+
phone: "+5511999999999",
|
|
100
|
+
cpf: "123.456.789-09",
|
|
101
|
+
cnpj: "12.345.678/0001-95"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# 2. Dinie runs KYC + underwriting. When the customer is approved, a `credit_offer.available`
|
|
105
|
+
# webhook fires. Read the offers (they are NOT created by you):
|
|
106
|
+
offer = client.customers.credit_offers.list(customer.id).first
|
|
107
|
+
# …or fetch a known offer directly:
|
|
108
|
+
offer = client.credit_offers.retrieve("co_0550e8400e29b41d4a716446655440000")
|
|
109
|
+
|
|
110
|
+
offer.approved_amount # => 25000.0 (Money — a Float, BRL)
|
|
111
|
+
offer.monthly_interest_rate # => 4.5
|
|
112
|
+
|
|
113
|
+
# 3. Simulate the offer for the amount and term the customer wants.
|
|
114
|
+
simulation = client.credit_offers.create_simulation(
|
|
115
|
+
offer.id,
|
|
116
|
+
requested_amount: 25_000.0,
|
|
117
|
+
installment_count: 12
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
simulation.installment_amount # => 2_343.21
|
|
121
|
+
simulation.total_amount # => 28_118.52
|
|
122
|
+
|
|
123
|
+
# 4. Contract the loan from the accepted simulation.
|
|
124
|
+
loan = client.loans.create(
|
|
125
|
+
credit_offer_id: offer.id,
|
|
126
|
+
simulation_id: simulation.id,
|
|
127
|
+
installment_count: simulation.installment_count,
|
|
128
|
+
installment_amount: simulation.installment_amount,
|
|
129
|
+
first_due_date: "2026-07-10" # ISO-8601 date
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
loan.id # => "ln_…"
|
|
133
|
+
loan.status # => "awaiting_signatures"
|
|
134
|
+
loan.signing_url # => CCB signature URL (present while awaiting signatures)
|
|
135
|
+
|
|
136
|
+
# Later, inspect the amortization schedule:
|
|
137
|
+
client.loans.transactions.list(loan.id).each do |installment|
|
|
138
|
+
puts "#{installment.due_date}: #{installment.amount_due} (#{installment.status})"
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> **Note.** All monetary fields are `Money` — a plain `Float` in BRL. All timestamps
|
|
143
|
+
> (`created_at`, `valid_until`, …) are integer Unix epoch seconds; only `RateLimit#reset_at` is a
|
|
144
|
+
> `Time`. ID prefixes (`cust_`, `co_`, `sim_`, `ln_`, `tx_`) are documented on each field; the value
|
|
145
|
+
> itself is a `String`.
|
|
146
|
+
|
|
147
|
+
## Pagination
|
|
148
|
+
|
|
149
|
+
List endpoints return a `Dinie::Page` that auto-paginates by cursor. It does **not** load every page
|
|
150
|
+
eagerly — `#each` walks pages on demand, following `has_more` (never the page size).
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# Iterate every customer across all pages:
|
|
154
|
+
client.customers.list(limit: 50).each do |customer|
|
|
155
|
+
puts customer.id
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Page-by-page, for manual control:
|
|
159
|
+
client.customers.list.each_page do |page|
|
|
160
|
+
process(page.data) # Array<Dinie::Customer>
|
|
161
|
+
page.has_more # => true / false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Just the first N, fetched lazily (only as many pages as needed):
|
|
165
|
+
top = client.customers.list.first(10)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Without a block, `#each` and `#each_page` return an `Enumerator`, so `.map` / `.lazy` work too.
|
|
169
|
+
|
|
170
|
+
`client.banks.list` is the one exception: the bank directory is **not** paginated, so it returns a
|
|
171
|
+
flat `Array<Dinie::Bank>` in a single call.
|
|
172
|
+
|
|
173
|
+
## Webhooks
|
|
174
|
+
|
|
175
|
+
Dinie delivers events (Standard Webhooks v1, HMAC-SHA256). `Dinie::Webhooks.extract` verifies the
|
|
176
|
+
signature **and** the timestamp, then returns the typed event — or raises. It never returns an
|
|
177
|
+
unverified payload, and it needs no client (verification is just your signing secret).
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# In your Rack / Rails / Sinatra handler:
|
|
181
|
+
event = Dinie::Webhooks.extract(
|
|
182
|
+
headers: request.headers.to_h, # must include webhook-id / webhook-timestamp / webhook-signature
|
|
183
|
+
body: request.raw_post, # the RAW body, before JSON parsing
|
|
184
|
+
secret: ENV["DINIE_WEBHOOK_SECRET"] # "whsec_…" (or an Array of secrets during rotation)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
case event
|
|
188
|
+
when Dinie::Events::CustomerActive
|
|
189
|
+
activate_customer(event.data.id)
|
|
190
|
+
when Dinie::Events::CreditOfferAvailable
|
|
191
|
+
notify_offer(event.data.customer_id, event.data.approved_amount)
|
|
192
|
+
when Dinie::Events::LoanActive
|
|
193
|
+
release_funds(event.data.id)
|
|
194
|
+
when Dinie::Events::LoanPaymentReceived
|
|
195
|
+
record_payment(event.data.id, event.data.payment.amount)
|
|
196
|
+
else
|
|
197
|
+
Rails.logger.info("Unhandled Dinie event: #{event.type}")
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Header lookup is case-insensitive and accepts string or symbol keys. The signed payload is
|
|
202
|
+
`"{webhook-id}.{webhook-timestamp}.{body}"`, the comparison is constant-time, and multiple
|
|
203
|
+
space-separated `v1,<sig>` signatures are accepted (so secret rotation works from either side).
|
|
204
|
+
|
|
205
|
+
Failures raise, so you can map them to HTTP responses:
|
|
206
|
+
|
|
207
|
+
| Raised | When |
|
|
208
|
+
| ------------------------------------- | --------------------------------------------------------------- |
|
|
209
|
+
| `Dinie::WebhookSignatureError` | a header is missing, no secret matched, or the HMAC differs |
|
|
210
|
+
| `Dinie::WebhookTimestampError` | the timestamp is malformed or outside the ±300s window |
|
|
211
|
+
| `Dinie::UnknownWebhookEventError` | the signature is valid but the `type` is not in the SDK's catalog |
|
|
212
|
+
|
|
213
|
+
## Error handling
|
|
214
|
+
|
|
215
|
+
Every non-2xx response becomes a typed error. The hierarchy lets you rescue broadly or narrowly, and
|
|
216
|
+
each error carries the machine-readable `code` and the `request_id` for support tickets.
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
begin
|
|
220
|
+
client.credit_offers.retrieve("co_does_not_exist")
|
|
221
|
+
rescue Dinie::NotFoundError => e
|
|
222
|
+
warn "Not found (#{e.code}) — request #{e.request_id}"
|
|
223
|
+
rescue Dinie::ValidationError => e
|
|
224
|
+
warn "Invalid: #{e.detail}" # RFC 9457 Problem Details
|
|
225
|
+
rescue Dinie::RateLimitError
|
|
226
|
+
# transient — the SDK already retried with backoff; back off further if you see this
|
|
227
|
+
rescue Dinie::APIStatusError => e
|
|
228
|
+
warn "API error #{e.status}: #{e.title}"
|
|
229
|
+
rescue Dinie::APIConnectionError
|
|
230
|
+
# network/DNS/timeout — no response was received
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
Dinie::Error
|
|
236
|
+
├── Dinie::APIError
|
|
237
|
+
│ ├── Dinie::APIConnectionError → Dinie::APITimeoutError
|
|
238
|
+
│ └── Dinie::APIStatusError
|
|
239
|
+
│ ├── Dinie::BadRequestError (400) Dinie::ConflictError (409)
|
|
240
|
+
│ ├── Dinie::AuthError (401) Dinie::ValidationError (422)
|
|
241
|
+
│ ├── Dinie::PermissionError (403) Dinie::RateLimitError (429)
|
|
242
|
+
│ └── Dinie::NotFoundError (404) Dinie::ServerError (500 + ≥500 fallback)
|
|
243
|
+
├── Dinie::OAuthError # token handshake failed
|
|
244
|
+
├── Dinie::WebhookSignatureError
|
|
245
|
+
├── Dinie::WebhookTimestampError
|
|
246
|
+
└── Dinie::UnknownWebhookEventError
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Retries are automatic for `408/429/500/502/503/504` and transport errors (exponential backoff with
|
|
250
|
+
jitter, honoring `Retry-After` up to 60s). `409`/`410` are never retried. A stable
|
|
251
|
+
`X-Idempotency-Key` is minted once per logical write and reused across retries, so a retry never
|
|
252
|
+
creates a duplicate resource.
|
|
253
|
+
|
|
254
|
+
After any call you can read the latest rate-limit snapshot:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
rl = client.rate_limit # => Dinie::RateLimit or nil
|
|
258
|
+
rl&.remaining # => 87
|
|
259
|
+
rl&.reset_at # => 2026-06-02 12:00:30 UTC (a Time)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Types
|
|
263
|
+
|
|
264
|
+
The full public surface is mirrored as RBS signatures under [`sig/`](sig/) — install the gem and
|
|
265
|
+
point [Steep](https://github.com/soutaro/steep) or Solargraph at it for static type checking and
|
|
266
|
+
editor hover. YARD documentation covers every public class and method.
|
|
267
|
+
|
|
268
|
+
## Development
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
bundle install
|
|
272
|
+
bundle exec rspec # tests — WebMock, zero network
|
|
273
|
+
bundle exec rubocop # lint
|
|
274
|
+
bundle exec steep check # type check (informative in v1 — see Steepfile)
|
|
275
|
+
bundle exec yard doc # build the API docs
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
[MIT](LICENSE) © Dinie
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/multipart"
|
|
5
|
+
require "faraday/net_http_persistent"
|
|
6
|
+
|
|
7
|
+
module Dinie
|
|
8
|
+
class Client
|
|
9
|
+
DEFAULT_TIMEOUT_SECONDS = Internal::HttpClient::DEFAULT_TIMEOUT_SECONDS
|
|
10
|
+
DEFAULT_MAX_RETRIES = Internal::HttpClient::DEFAULT_MAX_RETRIES
|
|
11
|
+
|
|
12
|
+
def initialize(client_id: nil, client_secret: nil, code: nil, base_url: nil, timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
13
|
+
max_retries: DEFAULT_MAX_RETRIES, idempotency: true, log_level: :off, logger: nil,
|
|
14
|
+
adapter: nil, token_manager: nil, connection: nil)
|
|
15
|
+
@base_url = first_present(base_url, ENV.fetch("DINIE_BASE_URL", nil)) || Internal::HttpClient::DEFAULT_BASE_URL
|
|
16
|
+
@options = {
|
|
17
|
+
timeout: timeout, max_retries: max_retries, idempotency: idempotency,
|
|
18
|
+
log_level: log_level, logger: logger, adapter: adapter
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@connection = connection || build_connection(log_level: log_level, logger: logger, adapter: adapter)
|
|
22
|
+
@token_manager = token_manager || build_token_manager(client_id, client_secret, code)
|
|
23
|
+
@http = Internal::HttpClient.new(
|
|
24
|
+
token_manager: @token_manager, base_url: @base_url, timeout: timeout,
|
|
25
|
+
max_retries: max_retries, idempotency: idempotency, connection: @connection
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def banks
|
|
30
|
+
@banks ||= Resources::Banks.new(@http)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def biometrics
|
|
34
|
+
@biometrics ||= Resources::Biometrics.new(@http)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def credentials
|
|
38
|
+
@credentials ||= Resources::Credentials.new(@http)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def credit_offers
|
|
42
|
+
@credit_offers ||= Resources::CreditOffers.new(@http)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def customers
|
|
46
|
+
@customers ||= Resources::Customers.new(@http)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def loans
|
|
50
|
+
@loans ||= Resources::Loans.new(@http)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def webhook_endpoints
|
|
54
|
+
@webhook_endpoints ||= Resources::WebhookEndpoints.new(@http)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rate_limit
|
|
58
|
+
@http.rate_limit
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def with_options(**overrides)
|
|
62
|
+
self.class.new(
|
|
63
|
+
base_url: @base_url, token_manager: @token_manager, connection: @connection,
|
|
64
|
+
**@options.merge(overrides)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def build_token_manager(client_id, client_secret, code = nil)
|
|
71
|
+
resolved_id = first_present(client_id, ENV.fetch("DINIE_CLIENT_ID", nil))
|
|
72
|
+
resolved_secret = first_present(client_secret, ENV.fetch("DINIE_CLIENT_SECRET", nil))
|
|
73
|
+
raise Dinie::Error, "Missing Dinie client id: pass `client_id:` or set DINIE_CLIENT_ID." if resolved_id.nil?
|
|
74
|
+
if resolved_secret.nil?
|
|
75
|
+
raise Dinie::Error, "Missing Dinie client secret: pass `client_secret:` or set DINIE_CLIENT_SECRET."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Internal::TokenManager.new(
|
|
79
|
+
client_id: resolved_id, client_secret: resolved_secret,
|
|
80
|
+
base_url: @base_url, connection: @connection, code: code
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_connection(log_level:, logger:, adapter:)
|
|
85
|
+
Faraday.new(url: @base_url) do |faraday|
|
|
86
|
+
faraday.request :multipart
|
|
87
|
+
faraday.use Internal::Middleware::Logging, level: log_level, logger: logger
|
|
88
|
+
faraday.adapter(adapter || :net_http_persistent)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def first_present(*candidates)
|
|
93
|
+
candidates.find { |value| !value.nil? && value != "" }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../runtime/errors"
|
|
4
|
+
|
|
5
|
+
module Dinie
|
|
6
|
+
class AuthError < APIStatusError; end
|
|
7
|
+
class BadRequestError < APIStatusError; end
|
|
8
|
+
class ConflictError < APIStatusError; end
|
|
9
|
+
class NotFoundError < APIStatusError; end
|
|
10
|
+
class PermissionDeniedError < APIStatusError; end
|
|
11
|
+
class RateLimitError < APIStatusError; end
|
|
12
|
+
class ServerError < APIStatusError; end
|
|
13
|
+
class ValidationError < APIStatusError; end
|
|
14
|
+
|
|
15
|
+
module Internal
|
|
16
|
+
ERROR_REGISTRY = {
|
|
17
|
+
by_type: {
|
|
18
|
+
"https://docs.dinie.com/errors/invalid-request" => Dinie::BadRequestError,
|
|
19
|
+
"https://docs.dinie.com/errors/authentication-failed" => Dinie::AuthError,
|
|
20
|
+
"https://docs.dinie.com/errors/forbidden" => Dinie::PermissionDeniedError,
|
|
21
|
+
"https://docs.dinie.com/errors/not-found" => Dinie::NotFoundError,
|
|
22
|
+
"https://docs.dinie.com/errors/conflict" => Dinie::ConflictError,
|
|
23
|
+
"https://docs.dinie.com/errors/validation-failed" => Dinie::ValidationError,
|
|
24
|
+
"https://docs.dinie.com/errors/rate-limit-exceeded" => Dinie::RateLimitError,
|
|
25
|
+
"https://docs.dinie.com/errors/internal" => Dinie::ServerError
|
|
26
|
+
}.freeze,
|
|
27
|
+
by_status: {
|
|
28
|
+
400 => Dinie::BadRequestError,
|
|
29
|
+
401 => Dinie::AuthError,
|
|
30
|
+
403 => Dinie::PermissionDeniedError,
|
|
31
|
+
404 => Dinie::NotFoundError,
|
|
32
|
+
409 => Dinie::ConflictError,
|
|
33
|
+
422 => Dinie::ValidationError,
|
|
34
|
+
429 => Dinie::RateLimitError,
|
|
35
|
+
500 => Dinie::ServerError
|
|
36
|
+
}.freeze,
|
|
37
|
+
fallback_5xx: Dinie::ServerError
|
|
38
|
+
}.freeze
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../runtime/model"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Dinie
|
|
7
|
+
module Events
|
|
8
|
+
class CreditOfferData < Internal::Model
|
|
9
|
+
attribute :approved_amount, :customer_id, :due_date_rule, :external_id, :id, :installments, :min_amount,
|
|
10
|
+
:monthly_interest_rate, :status, :valid_until
|
|
11
|
+
|
|
12
|
+
def self.deserialize(raw)
|
|
13
|
+
new(
|
|
14
|
+
approved_amount: raw[:approved_amount],
|
|
15
|
+
customer_id: raw[:customer_id],
|
|
16
|
+
due_date_rule: raw[:due_date_rule],
|
|
17
|
+
external_id: raw[:external_id],
|
|
18
|
+
id: raw[:id],
|
|
19
|
+
installments: raw[:installments],
|
|
20
|
+
min_amount: raw[:min_amount],
|
|
21
|
+
monthly_interest_rate: raw[:monthly_interest_rate],
|
|
22
|
+
status: raw[:status],
|
|
23
|
+
valid_until: raw[:valid_until]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class CreditOfferAvailable < WebhookEventBase
|
|
29
|
+
def self.deserialize(raw)
|
|
30
|
+
new(
|
|
31
|
+
api_version: raw[:api_version],
|
|
32
|
+
created_at: raw[:created_at],
|
|
33
|
+
data: CreditOfferData.deserialize(raw[:data]),
|
|
34
|
+
delivery_id: raw[:delivery_id],
|
|
35
|
+
id: raw[:id],
|
|
36
|
+
timestamp: raw[:timestamp],
|
|
37
|
+
type: "credit_offer.available"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class CreditOfferExpired < WebhookEventBase
|
|
43
|
+
def self.deserialize(raw)
|
|
44
|
+
new(
|
|
45
|
+
api_version: raw[:api_version],
|
|
46
|
+
created_at: raw[:created_at],
|
|
47
|
+
data: CreditOfferData.deserialize(raw[:data]),
|
|
48
|
+
delivery_id: raw[:delivery_id],
|
|
49
|
+
id: raw[:id],
|
|
50
|
+
timestamp: raw[:timestamp],
|
|
51
|
+
type: "credit_offer.expired"
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../runtime/model"
|
|
4
|
+
require_relative "../types/kyc"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Dinie
|
|
8
|
+
module Events
|
|
9
|
+
class CustomerCreatedData < Internal::Model
|
|
10
|
+
attribute :cnpj, :cpf, :email, :external_id, :id, :kyc, :name, :phone, :status, :trading_name
|
|
11
|
+
|
|
12
|
+
def self.deserialize(raw)
|
|
13
|
+
new(
|
|
14
|
+
cnpj: raw[:cnpj],
|
|
15
|
+
cpf: raw[:cpf],
|
|
16
|
+
email: raw[:email],
|
|
17
|
+
external_id: raw[:external_id],
|
|
18
|
+
id: raw[:id],
|
|
19
|
+
kyc: raw[:kyc].map { |requirement| Dinie.deserialize_kyc_requirement(requirement) },
|
|
20
|
+
name: raw[:name],
|
|
21
|
+
phone: raw[:phone],
|
|
22
|
+
status: raw[:status],
|
|
23
|
+
trading_name: raw[:trading_name]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class CustomerCreated < WebhookEventBase
|
|
29
|
+
def self.deserialize(raw)
|
|
30
|
+
new(
|
|
31
|
+
api_version: raw[:api_version],
|
|
32
|
+
created_at: raw[:created_at],
|
|
33
|
+
data: CustomerCreatedData.deserialize(raw[:data]),
|
|
34
|
+
delivery_id: raw[:delivery_id],
|
|
35
|
+
id: raw[:id],
|
|
36
|
+
timestamp: raw[:timestamp],
|
|
37
|
+
type: "customer.created"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../runtime/model"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Dinie
|
|
7
|
+
module Events
|
|
8
|
+
class CustomerDeniedData < Internal::Model
|
|
9
|
+
attribute :cnpj, :cpf, :email, :external_id, :id, :name, :phone, :status
|
|
10
|
+
|
|
11
|
+
def self.deserialize(raw)
|
|
12
|
+
new(
|
|
13
|
+
cnpj: raw[:cnpj],
|
|
14
|
+
cpf: raw[:cpf],
|
|
15
|
+
email: raw[:email],
|
|
16
|
+
external_id: raw[:external_id],
|
|
17
|
+
id: raw[:id],
|
|
18
|
+
name: raw[:name],
|
|
19
|
+
phone: raw[:phone],
|
|
20
|
+
status: raw[:status]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class CustomerDenied < WebhookEventBase
|
|
26
|
+
def self.deserialize(raw)
|
|
27
|
+
new(
|
|
28
|
+
api_version: raw[:api_version],
|
|
29
|
+
created_at: raw[:created_at],
|
|
30
|
+
data: CustomerDeniedData.deserialize(raw[:data]),
|
|
31
|
+
delivery_id: raw[:delivery_id],
|
|
32
|
+
id: raw[:id],
|
|
33
|
+
timestamp: raw[:timestamp],
|
|
34
|
+
type: "customer.denied"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../runtime/model"
|
|
4
|
+
require_relative "../types/kyc"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Dinie
|
|
8
|
+
module Events
|
|
9
|
+
class CustomerKycUpdatedData < Internal::Model
|
|
10
|
+
attribute :external_id, :id, :kyc, :status
|
|
11
|
+
|
|
12
|
+
def self.deserialize(raw)
|
|
13
|
+
new(
|
|
14
|
+
external_id: raw[:external_id],
|
|
15
|
+
id: raw[:id],
|
|
16
|
+
kyc: raw[:kyc].map { |requirement| Dinie.deserialize_kyc_requirement(requirement) },
|
|
17
|
+
status: raw[:status]
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class CustomerKycUpdated < WebhookEventBase
|
|
23
|
+
def self.deserialize(raw)
|
|
24
|
+
new(
|
|
25
|
+
api_version: raw[:api_version],
|
|
26
|
+
created_at: raw[:created_at],
|
|
27
|
+
data: CustomerKycUpdatedData.deserialize(raw[:data]),
|
|
28
|
+
delivery_id: raw[:delivery_id],
|
|
29
|
+
id: raw[:id],
|
|
30
|
+
timestamp: raw[:timestamp],
|
|
31
|
+
type: "customer.kyc_updated"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|