conexa 0.1.1 → 0.2.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 +207 -1
- data/README.md +35 -3
- data/README_pt-BR.md +35 -1
- data/REFERENCE.md +29 -1
- data/lib/conexa/configuration.rb +48 -2
- data/lib/conexa/errors.rb +79 -7
- data/lib/conexa/model.rb +77 -14
- data/lib/conexa/object.rb +24 -3
- data/lib/conexa/request.rb +90 -11
- data/lib/conexa/resources/charge.rb +13 -2
- data/lib/conexa/resources/company.rb +20 -1
- data/lib/conexa/resources/contract.rb +89 -15
- data/lib/conexa/resources/credit_card.rb +46 -0
- data/lib/conexa/resources/recurring_sale.rb +16 -5
- data/lib/conexa/resources/result.rb +23 -5
- data/lib/conexa/util.rb +46 -9
- data/lib/conexa/version.rb +1 -1
- data/lib/conexa.rb +38 -3
- metadata +27 -70
- data/.editorconfig +0 -30
- data/.rspec +0 -3
- data/.rubocop.yml +0 -13
- data/.solargraph.yml +0 -2
- data/.vscode/launch.json +0 -30
- data/Gemfile +0 -18
- data/Gemfile.lock +0 -159
- data/Rakefile +0 -12
- data/bin/console +0 -61
- data/bin/setup +0 -8
- data/docs/postman-collection.json +0 -30785
- data/lib/conexa/authenticator.rb +0 -116
- data/lib/conexa/order_common.rb +0 -44
- data/lib/conexa/token_manager.rb +0 -136
- data/scripts/extract_fixtures.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ff60656e2731455f658e616416581ede3b7a062e184097f756bcaaca9b523d4
|
|
4
|
+
data.tar.gz: d3c3a28eb846199bfebfbe963b160af18baab1be9fcfeee2382466a8916c145a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3108d62cff165810c56ae8e5ba738b54ebfa223995617cc7099abebd6511db45a1bab061f7c99df229512ef3004da6d05a0cab623e9662bed464606b9322a1dd
|
|
7
|
+
data.tar.gz: 1c15d0d6ed78d606e5cfb5a40b56399ab05dacaf8b28bd6562c12ec6f1cae40a87bf3d00e2f09140de061ab36a411eefd63cc2361ae3208907241ec51c23f89e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,211 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-08-11
|
|
11
|
+
|
|
12
|
+
Aligns the gem with the published API v2 contract. **0.1.1 is broken**:
|
|
13
|
+
`Charge.settle` and `Contract.end_contract` 404 against a live tenant, and a
|
|
14
|
+
successful write that answers with an empty body raises `NoMethodError` after
|
|
15
|
+
the operation has already taken effect — in a billing integration that invites a
|
|
16
|
+
retry. Upgrading is strongly recommended.
|
|
17
|
+
|
|
18
|
+
Every fix below was verified against `docs/postman-collection.json`, Conexa's own
|
|
19
|
+
published documentation, and is now enforced by
|
|
20
|
+
`spec/contract/api_contract_spec.rb`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Empty response bodies no longer raise, anywhere.** `Request#run` returns `{}`
|
|
24
|
+
for an empty, whitespace or `null` body at any status. With the Oj adapter
|
|
25
|
+
`MultiJson.decode("")` returns `nil` without raising, so the previous
|
|
26
|
+
`rescue MultiJson::ParseError` / `204` guard never fired — even a documented
|
|
27
|
+
`204` success (`PATCH /charge/settle/:id`) raised `NoMethodError`, which is not
|
|
28
|
+
a `Conexa::ConexaError` and escaped `rescue Conexa::ConexaError`.
|
|
29
|
+
|
|
30
|
+
`ConexaObject#update` treats anything carrying no attributes as "nothing to
|
|
31
|
+
merge" — `nil`, `{}`, and the arrays and scalars `Request#run` also produces —
|
|
32
|
+
and `Model#create` does the same. Fixing `Request#run` alone was not enough:
|
|
33
|
+
`Model#save` and `#destroy` fed the result straight into `update`, so **every**
|
|
34
|
+
resource's generic CRUD still raised `NoMethodError` on a documented `204`
|
|
35
|
+
(`DELETE /sale/:id` among them). Only the three action methods were safe, and
|
|
36
|
+
only because they discard their return value.
|
|
37
|
+
- **A write answering `200 {}` no longer wipes the object.** `update` removes
|
|
38
|
+
every attribute absent from the incoming hash, which is right for a full
|
|
39
|
+
refresh and destructive for an empty write response — it cleared the whole
|
|
40
|
+
object, primary key included, and reported success.
|
|
41
|
+
- **`Model#save` guards a blank id**, as `#destroy` already did. Without it an
|
|
42
|
+
id-less object issued `PATCH /customer/` instead of failing fast.
|
|
43
|
+
- **`Result#next_page` raises a `Conexa` error** when pagination carries no
|
|
44
|
+
`limit`, instead of `TypeError` from `nil + Integer`, and survives a
|
|
45
|
+
non-marshallable filter in the original query.
|
|
46
|
+
- **`Conexa::ValidationError` is usable.** It rendered as the bare string
|
|
47
|
+
`"Conexa::ValidationError"` and `#to_h` raised `NoMethodError`. It is only
|
|
48
|
+
reachable for an error body with no `message` key — which no documented
|
|
49
|
+
response has — but it now reports what actually arrived.
|
|
50
|
+
- **Exception messages read as prose.** The old format appended a dangling
|
|
51
|
+
`"=> Erros: "` to the 75 documented responses that carry no `errors` array, and
|
|
52
|
+
dumped Ruby's `#inspect` of an array of hashes for the ones that do. It now
|
|
53
|
+
uses the same normalisation as `#api_error_messages`:
|
|
54
|
+
`422 … => It was not possible to process your request — CHARGE_11: Only open charges can be settled.`
|
|
55
|
+
- **Error bodies that are not JSON objects no longer raise `TypeError`.** An
|
|
56
|
+
array or scalar error body reached `parsed_error['message']` and blew up inside
|
|
57
|
+
the error handler.
|
|
58
|
+
- **`Request#run` tolerates a top-level array or scalar body.** Defensive: every
|
|
59
|
+
list endpoint probed on 2026-08-12 used the `{data, pagination}` envelope, so
|
|
60
|
+
this shape has not been observed in the wild — it removes a crash class rather
|
|
61
|
+
than fixing a seen failure.
|
|
62
|
+
- **Action endpoints use the documented verb.** `Charge#settle`,
|
|
63
|
+
`Contract#end_contract` and `RecurringSale#end_recurring_sale` send `PATCH`;
|
|
64
|
+
`POST` 404s.
|
|
65
|
+
- **`Conexa::Company.all` requests the documented `/companies`.** `Model#url`
|
|
66
|
+
pluralizes by appending `"s"`, which produced `/companys`. A read-only probe of
|
|
67
|
+
a live tenant on 2026-08-12 showed the API routes **both** spellings, so this
|
|
68
|
+
was an undocumented path that worked rather than the 404 first reported — the
|
|
69
|
+
override stands because an undocumented alias can vanish without notice, but
|
|
70
|
+
nothing was broken by it.
|
|
71
|
+
- **Nested arrays are camelized.** `Util.camelize_hash` treated an `Array` as a
|
|
72
|
+
scalar, so snake_case keys inside arrays of objects were sent untouched and
|
|
73
|
+
rejected. Affects `complementaryServices`, `productQuotas`, `devices`,
|
|
74
|
+
`extraFields`, `bookingModels`, `visitors` and `costCenters` — ten documented
|
|
75
|
+
endpoints across seven resources.
|
|
76
|
+
- **`page`/`size` are converted to `limit`/`offset`** instead of being sent. The
|
|
77
|
+
API validates `page` and then ignores it, always answering `offset: 0`, so a
|
|
78
|
+
loop driven by `hasNext` never terminated and silently re-yielded the same
|
|
79
|
+
batch. Non-positive or non-integer values now raise `Conexa::RequestError`.
|
|
80
|
+
- **`Model#id` falls back to `attributes["id"]` again.** `primary_key_attribute`
|
|
81
|
+
aliased `#id` onto the resource key, making the documented fallback dead code —
|
|
82
|
+
write endpoints answer with `{"id": N}`, which is what `Model#create` reads.
|
|
83
|
+
- **`ResponseError` no longer raises `NoMethodError` on malformed bodies.** It was
|
|
84
|
+
handed a `RestClient::Response`, which has no `#message`.
|
|
85
|
+
- **A broken connection raises `Conexa::ConnectionError`** (issue #11). The
|
|
86
|
+
`rescue RestClient::ServerBrokeConnection` clause was unreachable —
|
|
87
|
+
`ServerBrokeConnection` subclasses `RestClient::Exception`, so the broader
|
|
88
|
+
clause listed above it always matched first, then tried to decode a `nil`
|
|
89
|
+
`http_body` and raised `NoMethodError` from inside the error handler.
|
|
90
|
+
Connection-level failures are now rescued first: `SocketError`,
|
|
91
|
+
`ServerBrokeConnection`, `SSLCertificateNotVerified` and
|
|
92
|
+
`RestClient::Exceptions::Timeout`. A real HTTP 408 stays in the response
|
|
93
|
+
taxonomy, since `RestClient::RequestTimeout` is a *superclass* of
|
|
94
|
+
`Exceptions::Timeout`.
|
|
95
|
+
- **An unusable id raises `Conexa::RequestError`, not `URI::InvalidURIError`**
|
|
96
|
+
(issue #12). `find(" 123 ")` leaked a URI error from inside RestClient, outside
|
|
97
|
+
the `Conexa::ConexaError` hierarchy. Surrounding whitespace is now stripped, and
|
|
98
|
+
`Request#full_api_url` validates the URL it builds.
|
|
99
|
+
- **`RecurringSale#end_recurring_sale` sends the documented `date`.** The verb was
|
|
100
|
+
corrected but the field was not, so `end_date:` still went out as the rejected
|
|
101
|
+
`endDate`, and its YARD named that field as the example. It now shares
|
|
102
|
+
`Util.normalize_end_date_param` with `Contract`, and gains a `set_end_date`
|
|
103
|
+
alias for symmetry.
|
|
104
|
+
- `README.md` configured a non-existent `config.subdomain`; the quick start now
|
|
105
|
+
runs.
|
|
106
|
+
- `lib/conexa/resources/supplier.rb` shipped as `0600`, breaking `require` on a
|
|
107
|
+
shared install.
|
|
108
|
+
|
|
109
|
+
- **`Conexa::CreditCard` is write-only.** API v2 exposes no read for it: `GET
|
|
110
|
+
/creditCard` answers "Unable to resolve the request" and `GET /creditCard/:id`
|
|
111
|
+
"unable to find the requested action" — neither is the permission wording the
|
|
112
|
+
API uses for a resource an account cannot see. `all`/`find`/`find_by` and their
|
|
113
|
+
aliases now raise `Conexa::RequestError` explaining that, instead of a bare
|
|
114
|
+
`NotFound` reading as "no such card", and `#create` skips `Model#create`'s
|
|
115
|
+
re-fetch. The integration spec that asserted those reads worked — against
|
|
116
|
+
stubs of endpoints that do not exist — was rewritten.
|
|
117
|
+
- **Listings always answer with a `Conexa::Result`.** An empty body yielded `nil`
|
|
118
|
+
and a bare-array body an `Array`, so `.data`/`.pagination`/`.next_page` failed
|
|
119
|
+
far from the cause.
|
|
120
|
+
- **`Model#fetch` raises instead of silently keeping stale attributes** when the
|
|
121
|
+
refresh comes back with no body. The nil-guard that makes an empty *write*
|
|
122
|
+
response harmless must not make an empty *read* look successful.
|
|
123
|
+
- **`Result#has_next?` returns a boolean**, not `nil`, when there is no
|
|
124
|
+
pagination at all.
|
|
125
|
+
|
|
126
|
+
### Added
|
|
127
|
+
- **Read-only mode.** `config.read_only = true`, `CONEXA_READ_ONLY=1`, or a
|
|
128
|
+
`Conexa.read_only { ... }` block. Any non-`GET` request raises
|
|
129
|
+
`Conexa::ReadOnlyError` before it leaves the process. Settling a charge moves
|
|
130
|
+
money and can issue an NF-e, so this is a guard worth having when you only mean
|
|
131
|
+
to read.
|
|
132
|
+
|
|
133
|
+
The exemption for `POST /auth` is matched on the **path**, not on the caller's
|
|
134
|
+
`auth:` flag — `Request.auth` is public, so keying it off the flag would have
|
|
135
|
+
let any write opt out of the guard. `CONEXA_READ_ONLY` is consulted at each
|
|
136
|
+
check rather than captured at configure time, and an unrecognised value warns
|
|
137
|
+
instead of silently failing open. The block form is fiber-local and does not
|
|
138
|
+
reach a `Thread` or `Fiber` spawned inside it — documented in the READMEs; use
|
|
139
|
+
`config.read_only` for concurrent work.
|
|
140
|
+
- **`ResponseError#api_errors`, `#api_error_codes`, `#api_error_messages`,
|
|
141
|
+
`#api_response`** — normalise the API's two error shapes (`{field, messages}`
|
|
142
|
+
and `{code, message}`). Consumers that only handled the first rendered
|
|
143
|
+
business-rule errors as blank strings, which is how
|
|
144
|
+
`CONTRACT_RECURRING_SALE_10` stayed invisible through eight attempts.
|
|
145
|
+
`api_error_codes` makes documented codes such as `CHARGE_11` usable as control
|
|
146
|
+
flow — the way to tell an already-settled charge from a real failure.
|
|
147
|
+
- **`Contract#set_end_date`** (with `end_contract` kept as an alias), taking the
|
|
148
|
+
documented `date:`, `reason_id:` and `unlink_customer:`. The endpoint both
|
|
149
|
+
closes a contract and amends its end date, and a future date on a closed
|
|
150
|
+
contract reopens it.
|
|
151
|
+
- **Contract-level test coverage**: `spec/contract/api_contract_spec.rb` checks
|
|
152
|
+
every emitted URL and action verb against the vendored collection, with an
|
|
153
|
+
explicit allowlist and a guard that fails when an endpoint is added without one.
|
|
154
|
+
`spec/support/request_capture.rb` asserts the request the gem actually emits
|
|
155
|
+
rather than stubbing the expected one.
|
|
156
|
+
- `verify_partial_doubles = true`.
|
|
157
|
+
- YARD documentation of `POST /contract`'s fields, including the conditional
|
|
158
|
+
`due_day` rule and the atomic `firstOccurrenceSettleRetroactive` flow.
|
|
159
|
+
|
|
160
|
+
### Changed
|
|
161
|
+
- **Breaking**: `end_contract` sends the documented `date`; `end_date:` still
|
|
162
|
+
works as a deprecated alias and will be removed in 0.3.0.
|
|
163
|
+
- **Breaking**: writes that answer with an empty body now return normally instead
|
|
164
|
+
of raising. Callers treating the exception as a signal must be reviewed.
|
|
165
|
+
- **Breaking**: `page`/`size` no longer reach the wire.
|
|
166
|
+
- Minimum Ruby is now **3.1** (was 2.6, never tested below 3.1). CI covers
|
|
167
|
+
3.1–3.4.
|
|
168
|
+
- `rest-client` and `multi_json` are version-bounded. `multi_json`'s adapter
|
|
169
|
+
behaviour is load-bearing — the empty-body defect exists because Oj returns
|
|
170
|
+
`nil` rather than raising.
|
|
171
|
+
- The gemspec's development dependencies move to bundler's `:test` group, and
|
|
172
|
+
`debug`/`byebug` to an optional `:development` group, so a native-extension
|
|
173
|
+
failure in `io-console` can no longer stop `bundle exec rspec`.
|
|
174
|
+
- **The packaged gem is 48 KB, down from 163 KB.** `spec.files` shipped the whole
|
|
175
|
+
repository — including `docs/postman-collection.json` (1.7 MB) — against 58 KB
|
|
176
|
+
of library code. It now ships `lib/` and the documentation only.
|
|
177
|
+
- **`Gemfile.lock` is no longer committed**, as is conventional for a gem: a
|
|
178
|
+
pinned lockfile defeats testing against the range the gemspec allows, and CI
|
|
179
|
+
had been working around it by deleting the file before `bundle install`.
|
|
180
|
+
- **`rake` passes again.** The default task is `spec` + `rubocop`, and RuboCop's
|
|
181
|
+
~2000 pre-existing offences made it fail, which is why CI ran lint with
|
|
182
|
+
`continue-on-error`. A generated `.rubocop_todo.yml` grandfathers them, so new
|
|
183
|
+
code is genuinely linted and CI can gate on it.
|
|
184
|
+
- `rake spec:all` runs the suite across every supported Ruby via mise; `rake ci`
|
|
185
|
+
adds RuboCop. CI uses `bundler-cache: true`.
|
|
186
|
+
- **Releases publish from CI via Trusted Publishing.** Pushing a `v*` tag runs
|
|
187
|
+
the full gate and then exchanges GitHub's OIDC token for a short-lived,
|
|
188
|
+
push-scoped RubyGems credential — no API key is stored anywhere. Two gates run
|
|
189
|
+
first, both aimed at how 0.1.1 went out: the tag must match `Conexa::VERSION`
|
|
190
|
+
(the `v0.1.1` tag points at code carrying `0.1.0`), and the packaged gem must
|
|
191
|
+
contain only `lib/` and the documentation.
|
|
192
|
+
|
|
193
|
+
### Removed
|
|
194
|
+
- **Breaking**: `Conexa::Client`, `Conexa::Authenticator` and
|
|
195
|
+
`Conexa::TokenManager`. They referenced five `Conexa` module methods that never
|
|
196
|
+
existed (`credentials`, `secret_key`, `access_key`, `client_id`,
|
|
197
|
+
`default_client_key`), so any real use raised `NoMethodError`; their specs
|
|
198
|
+
passed only because they stubbed those methods into being. The `jwt` dependency
|
|
199
|
+
goes with them — `Conexa::Auth` (the v2 `/auth` resource) is unaffected.
|
|
200
|
+
- **Breaking**: `Conexa::OrderCommon`. Unlike the three above, this one *ran* —
|
|
201
|
+
it built `/order/:id` and `/order/:id/refund` and issued a real `DELETE`. It is
|
|
202
|
+
removed because **API v2 documents no `/order` endpoint at all**: nothing in
|
|
203
|
+
the gem used it, no spec covered it, and it targets a surface that no longer
|
|
204
|
+
exists. Note that its documented backwards-compatible alias never worked:
|
|
205
|
+
`OrderCommom = self` inside the class body defines
|
|
206
|
+
`Conexa::OrderCommon::OrderCommom`, not `Conexa::OrderCommom`, so the pre-0.1.0
|
|
207
|
+
name has raised `NameError` since the rename.
|
|
208
|
+
|
|
209
|
+
### Testing
|
|
210
|
+
- VCR cassettes recorded against a real tenant are anonymised, and
|
|
211
|
+
`spec_helper.rb` filters the API token out of new recordings. The
|
|
212
|
+
`claude_scripts/sanitize_cassettes/` script does the anonymisation in place,
|
|
213
|
+
keeping the JSON shape so the specs still exercise the same structure.
|
|
214
|
+
|
|
10
215
|
## [0.1.1] - 2026-03-31
|
|
11
216
|
|
|
12
217
|
### Added
|
|
@@ -133,7 +338,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
133
338
|
- Charge with settle and PIX methods
|
|
134
339
|
- Pagination support
|
|
135
340
|
|
|
136
|
-
[Unreleased]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.
|
|
341
|
+
[Unreleased]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.2.0...HEAD
|
|
342
|
+
[0.2.0]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.1.1...v0.2.0
|
|
137
343
|
[0.1.1]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.1.0...v0.1.1
|
|
138
344
|
[0.1.0]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.0.9...v0.1.0
|
|
139
345
|
[0.0.9]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.0.8...v0.0.9
|
data/README.md
CHANGED
|
@@ -25,8 +25,8 @@ gem install conexa
|
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
27
|
Conexa.configure do |config|
|
|
28
|
-
config.
|
|
29
|
-
config.api_token = 'YOUR_API_TOKEN'
|
|
28
|
+
config.api_host = 'https://your-company.conexa.app' # your tenant
|
|
29
|
+
config.api_token = 'YOUR_API_TOKEN' # Application Token from Conexa
|
|
30
30
|
end
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -35,13 +35,45 @@ end
|
|
|
35
35
|
1. **Application Token** (recommended): Created in Conexa at **Config > Integrações > API / Token**
|
|
36
36
|
2. **Username/Password**: Use the `/auth` endpoint to get a JWT token
|
|
37
37
|
|
|
38
|
+
### Read-only mode
|
|
39
|
+
|
|
40
|
+
This gem talks to a billing system: settling a charge moves money and can issue
|
|
41
|
+
an NF-e. When you only mean to read — auditing, reporting, investigating — turn
|
|
42
|
+
writing off and let the gem refuse instead of trusting yourself to be careful.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Conexa.configure { |config| config.read_only = true }
|
|
46
|
+
|
|
47
|
+
Conexa::Charge.all(status: 'pending') # fine
|
|
48
|
+
Conexa::Charge.settle(789) # raises Conexa::ReadOnlyError
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Set `CONEXA_READ_ONLY=1` in the environment to default it on — useful in CI, or
|
|
52
|
+
in any shell where you would rather not discover you were pointed at production.
|
|
53
|
+
|
|
54
|
+
For a single block, without reconfiguring the client:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Conexa.read_only do
|
|
58
|
+
report = Conexa::Charge.all(limit: 100) # anything here is read-only
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The guard runs before the request leaves the process, so a blocked call never
|
|
63
|
+
reaches your tenant. `GET` is allowed, and so is `POST /auth` — without it
|
|
64
|
+
read-only mode could not obtain a token.
|
|
65
|
+
|
|
66
|
+
**The block form is fiber-local.** It does not reach a `Thread` or `Fiber`
|
|
67
|
+
spawned inside it, nor external iteration on an `Enumerator`. For anything
|
|
68
|
+
concurrent use `config.read_only` (or `CONEXA_READ_ONLY`), which is global.
|
|
69
|
+
|
|
38
70
|
## Quick Start
|
|
39
71
|
|
|
40
72
|
```ruby
|
|
41
73
|
require 'conexa'
|
|
42
74
|
|
|
43
75
|
Conexa.configure do |config|
|
|
44
|
-
config.
|
|
76
|
+
config.api_host = 'https://mycompany.conexa.app'
|
|
45
77
|
config.api_token = ENV['CONEXA_API_TOKEN']
|
|
46
78
|
end
|
|
47
79
|
|
data/README_pt-BR.md
CHANGED
|
@@ -37,7 +37,41 @@ Conexa.configure do |config|
|
|
|
37
37
|
end
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Certifique-se de que as variáveis de ambiente `API_TOKEN` e `API_HOST` estejam configuradas com seu token e URL da API Conexa.
|
|
40
|
+
Certifique-se de que as variáveis de ambiente `API_TOKEN` e `API_HOST` estejam configuradas com seu token e URL da API Conexa. O `api_host` é o subdomínio do seu tenant, por exemplo `https://minhaempresa.conexa.app`.
|
|
41
|
+
|
|
42
|
+
### Modo somente-leitura
|
|
43
|
+
|
|
44
|
+
Esta gem conversa com um sistema de cobrança: quitar uma cobrança movimenta
|
|
45
|
+
dinheiro e pode emitir NF-e. Quando a intenção é apenas consultar — auditoria,
|
|
46
|
+
relatório, investigação — desligue a escrita e deixe a gem recusar, em vez de
|
|
47
|
+
confiar na própria atenção.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
Conexa.configure { |config| config.read_only = true }
|
|
51
|
+
|
|
52
|
+
Conexa::Charge.all(status: 'pending') # ok
|
|
53
|
+
Conexa::Charge.settle(789) # levanta Conexa::ReadOnlyError
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Defina `CONEXA_READ_ONLY=1` no ambiente para ligar por padrão — útil em CI, ou em
|
|
57
|
+
qualquer shell onde você prefira não descobrir que estava apontando para
|
|
58
|
+
produção.
|
|
59
|
+
|
|
60
|
+
Para um trecho específico, sem reconfigurar o cliente:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Conexa.read_only do
|
|
64
|
+
relatorio = Conexa::Charge.all(limit: 100) # tudo aqui dentro é leitura
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A verificação acontece antes de a requisição sair do processo, então uma chamada
|
|
69
|
+
bloqueada nunca chega ao seu tenant. `GET` é permitido, e o `POST /auth` também —
|
|
70
|
+
sem ele o modo leitura não conseguiria obter token.
|
|
71
|
+
|
|
72
|
+
**O bloco é fiber-local.** Ele não alcança uma `Thread` ou `Fiber` criada lá
|
|
73
|
+
dentro, nem iteração externa de `Enumerator`. Para qualquer coisa concorrente use
|
|
74
|
+
`config.read_only` (ou `CONEXA_READ_ONLY`), que é global.
|
|
41
75
|
|
|
42
76
|
## Uso
|
|
43
77
|
|
data/REFERENCE.md
CHANGED
|
@@ -42,8 +42,9 @@ gem install conexa
|
|
|
42
42
|
|
|
43
43
|
```ruby
|
|
44
44
|
Conexa.configure do |config|
|
|
45
|
-
config.api_host
|
|
45
|
+
config.api_host = 'https://mycompany.conexa.app' # your tenant subdomain
|
|
46
46
|
config.api_token = 'your_api_token_here'
|
|
47
|
+
config.read_only = false # see below
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Rails: use generator
|
|
@@ -51,6 +52,33 @@ rails generate conexa:install
|
|
|
51
52
|
# Creates config/initializers/conexa.rb
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
| Setting | Default | Purpose |
|
|
56
|
+
|---------|---------|---------|
|
|
57
|
+
| `api_host` | `''` | Tenant base URL. `/index.php/api/v2` is appended automatically. |
|
|
58
|
+
| `api_token` | `''` | Application Token — Config > Integrações > API / Token. |
|
|
59
|
+
| `read_only` | `ENV['CONEXA_READ_ONLY']` | Refuse every non-GET request. |
|
|
60
|
+
|
|
61
|
+
### Read-only mode
|
|
62
|
+
|
|
63
|
+
Any request other than `GET` raises `Conexa::ReadOnlyError` before it leaves the
|
|
64
|
+
process. `POST /auth` stays allowed — matched on the path, not on a caller flag —
|
|
65
|
+
since read-only mode would otherwise be unable to obtain a token.
|
|
66
|
+
|
|
67
|
+
`Conexa.read_only { }` is fiber-local: it does not reach a `Thread` or `Fiber`
|
|
68
|
+
spawned inside the block. Use `config.read_only` for concurrent work.
|
|
69
|
+
|
|
70
|
+
`CONEXA_READ_ONLY` is read at each check, so it applies even when set after
|
|
71
|
+
`Conexa.configure`. Recognised: `1/true/yes/on` and `0/false/no/off`; anything
|
|
72
|
+
else warns and is treated as off.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
Conexa.configure { |c| c.read_only = true } # or CONEXA_READ_ONLY=1
|
|
76
|
+
|
|
77
|
+
Conexa.read_only do # block-scoped, thread-local
|
|
78
|
+
Conexa::Charge.all(status: 'pending')
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
54
82
|
## Convention: snake_case
|
|
55
83
|
|
|
56
84
|
This gem follows Ruby/Rails conventions. Use **snake_case** for all parameters - the gem automatically converts to camelCase for the API.
|
data/lib/conexa/configuration.rb
CHANGED
|
@@ -2,11 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
module Conexa
|
|
4
4
|
class Configuration
|
|
5
|
-
|
|
5
|
+
# @return [String] the Application Token, created in Conexa under
|
|
6
|
+
# Config > Integrações > API / Token
|
|
7
|
+
attr_accessor :api_token
|
|
8
|
+
|
|
9
|
+
# @return [String] the tenant base URL, e.g. "https://mycompany.conexa.app"
|
|
10
|
+
attr_accessor :api_host
|
|
11
|
+
|
|
12
|
+
# Set to force read-only mode on or off, overriding the environment.
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
attr_writer :read_only
|
|
15
|
+
|
|
16
|
+
# Values that turn read-only mode on via CONEXA_READ_ONLY.
|
|
17
|
+
TRUTHY = %w[1 true yes on].freeze
|
|
18
|
+
|
|
19
|
+
# Values that explicitly turn it off. Anything else is a typo worth warning
|
|
20
|
+
# about — silently failing open on `CONEXA_READ_ONLY=treu` would leave an
|
|
21
|
+
# operator believing writes were blocked when they are not.
|
|
22
|
+
FALSEY = %w[0 false no off].freeze
|
|
6
23
|
|
|
7
24
|
def initialize
|
|
8
25
|
@api_token = ''
|
|
9
26
|
@api_host = ''
|
|
27
|
+
@read_only = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# When true, any request other than GET raises {Conexa::ReadOnlyError} before
|
|
31
|
+
# reaching the network.
|
|
32
|
+
#
|
|
33
|
+
# Read lazily so that CONEXA_READ_ONLY still applies when it is set after
|
|
34
|
+
# {Conexa.configure} has run — it used to be captured once at initialization,
|
|
35
|
+
# which made setting it later a silent no-op. An explicit assignment always
|
|
36
|
+
# wins over the environment.
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def read_only
|
|
40
|
+
return @read_only unless @read_only.nil?
|
|
41
|
+
|
|
42
|
+
env_read_only
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def env_read_only
|
|
48
|
+
value = ENV["CONEXA_READ_ONLY"].to_s.strip.downcase
|
|
49
|
+
return false if value.empty?
|
|
50
|
+
return true if TRUTHY.include?(value)
|
|
51
|
+
return false if FALSEY.include?(value)
|
|
52
|
+
|
|
53
|
+
warn "Conexa: CONEXA_READ_ONLY=#{value.inspect} não é reconhecido e foi ignorado " \
|
|
54
|
+
"(modo somente-leitura DESLIGADO). Use um de: #{(TRUTHY + FALSEY).join(", ")}."
|
|
55
|
+
false
|
|
10
56
|
end
|
|
11
57
|
end
|
|
12
|
-
end
|
|
58
|
+
end
|
data/lib/conexa/errors.rb
CHANGED
|
@@ -16,40 +16,112 @@ module Conexa
|
|
|
16
16
|
class RequestError < ConexaError
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Raised instead of performing a mutating request while Conexa.read_only?.
|
|
20
|
+
# The request never reaches the network.
|
|
21
|
+
class ReadOnlyError < ConexaError
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
class ResponseError < ConexaError
|
|
20
25
|
attr_reader :request_params, :error
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
# The decoded error body, when the API returned one.
|
|
28
|
+
# @return [Hash]
|
|
29
|
+
attr_reader :api_response
|
|
30
|
+
|
|
31
|
+
def initialize(request_params, error, message=nil, api_response=nil)
|
|
23
32
|
@request_params, @error = request_params, error
|
|
24
|
-
|
|
33
|
+
@api_response = api_response.is_a?(Hash) ? api_response : {}
|
|
34
|
+
msg = describe_error(error)
|
|
25
35
|
msg += " => " + message if message
|
|
26
36
|
super msg
|
|
27
37
|
end
|
|
38
|
+
|
|
39
|
+
# The API's errors, normalised across its two shapes.
|
|
40
|
+
#
|
|
41
|
+
# Field validation answers `{"field": …, "messages": [...]}`; business rules
|
|
42
|
+
# answer `{"code": …, "message": …}`. Consumers that only handled the first
|
|
43
|
+
# rendered business-rule errors as blank strings — which is how
|
|
44
|
+
# CONTRACT_RECURRING_SALE_10 stayed invisible through eight attempts.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Hash{Symbol=>String,nil}>] entries of {field:, code:, message:}
|
|
47
|
+
def api_errors
|
|
48
|
+
Array(api_response["errors"]).filter_map do |entry|
|
|
49
|
+
next unless entry.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
{ field: entry["field"],
|
|
52
|
+
code: entry["code"],
|
|
53
|
+
message: entry["message"] || Array(entry["messages"]).join("; ") }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Documented business-rule codes, e.g. "CHARGE_11" or
|
|
58
|
+
# "CONTRACT_RECURRING_SALE_10". These are what a caller branches on — for
|
|
59
|
+
# instance to tell an already-settled charge from a real settlement failure.
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<String>]
|
|
62
|
+
def api_error_codes
|
|
63
|
+
api_errors.filter_map { |entry| entry[:code] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# One readable line per error, whichever shape it arrived in.
|
|
67
|
+
# @return [Array<String>]
|
|
68
|
+
def api_error_messages
|
|
69
|
+
api_errors.map do |entry|
|
|
70
|
+
label = entry[:field] || entry[:code]
|
|
71
|
+
label ? "#{label}: #{entry[:message]}" : entry[:message]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# `error` is usually a RestClient::Exception, but the malformed-body path
|
|
78
|
+
# hands us a RestClient::Response, which has no #message — that used to raise
|
|
79
|
+
# NoMethodError from inside the error constructor itself.
|
|
80
|
+
def describe_error(error)
|
|
81
|
+
return error.message if error.respond_to?(:message) && error.message
|
|
82
|
+
return "HTTP #{error.code}: #{error.body.to_s[0, 200]}" if error.respond_to?(:code)
|
|
83
|
+
|
|
84
|
+
error.to_s
|
|
85
|
+
end
|
|
28
86
|
end
|
|
29
87
|
|
|
30
88
|
class NotFound < ResponseError
|
|
31
89
|
attr_reader :response
|
|
32
90
|
def initialize(response, request_params, error)
|
|
33
91
|
@response = response
|
|
34
|
-
super request_params, error, response&.dig('message')
|
|
92
|
+
super request_params, error, response&.dig('message'), response
|
|
35
93
|
end
|
|
36
94
|
end
|
|
37
95
|
|
|
96
|
+
# Raised for an error body with no `message` key.
|
|
97
|
+
#
|
|
98
|
+
# Every error response the published collection documents carries a `message`,
|
|
99
|
+
# so in practice this is reached only by an undocumented or malformed body. It
|
|
100
|
+
# used to render as the bare string "Conexa::ValidationError" and `#to_h` raised
|
|
101
|
+
# NoMethodError; both now degrade to something a caller can act on.
|
|
38
102
|
class ValidationError < ConexaError
|
|
39
103
|
attr_reader :response, :errors
|
|
40
104
|
|
|
41
105
|
def initialize(response)
|
|
42
106
|
@response = response
|
|
43
|
-
@errors = response['message']
|
|
44
|
-
|
|
45
|
-
|
|
107
|
+
@errors = Array(response.is_a?(Hash) ? response['message'] : nil).filter_map do |msg|
|
|
108
|
+
next unless msg.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
ParamError.new(*msg.values_at('message', 'parameter_name', 'type', 'url'))
|
|
46
111
|
end
|
|
47
|
-
|
|
112
|
+
|
|
113
|
+
super(@errors.any? ? @errors.map(&:message).join(', ') : describe(response))
|
|
48
114
|
end
|
|
49
115
|
|
|
50
116
|
def to_h
|
|
51
117
|
@errors.map(&:to_h)
|
|
52
118
|
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def describe(response)
|
|
123
|
+
"The API returned an error with no message: #{response.inspect[0, 200]}"
|
|
124
|
+
end
|
|
53
125
|
end
|
|
54
126
|
|
|
55
127
|
class MissingCredentialsError < ConexaError
|