conexa 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +265 -1
- data/README.md +36 -4
- data/README_pt-BR.md +37 -3
- data/REFERENCE.md +53 -10
- data/lib/conexa/configuration.rb +48 -2
- data/lib/conexa/deprecation.rb +44 -0
- data/lib/conexa/errors.rb +79 -7
- data/lib/conexa/model.rb +80 -14
- data/lib/conexa/object.rb +24 -3
- data/lib/conexa/request.rb +90 -11
- data/lib/conexa/resources/charge.rb +61 -8
- data/lib/conexa/resources/company.rb +20 -1
- data/lib/conexa/resources/contract.rb +147 -30
- 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 +49 -9
- data/lib/conexa/version.rb +1 -1
- data/lib/conexa.rb +40 -3
- metadata +28 -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: af7a51117e4a311ae46b22248f416838e385336138f60d80c741aa671f47c4fe
|
|
4
|
+
data.tar.gz: 44aefe94b1b0fccf3b186be4377308bb6fe16e734d6c7c3fbd8adf7e5568cdc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52c72131519a01ee69239ac6f63b2f36f4f9f3252ec9d423ce74bd80096ae9e7d25b4eb229dae27a9ad7dac94883cd2b6a3c72db91e9f41cdf68db590a7d7db7
|
|
7
|
+
data.tar.gz: e85125857aa04e1ec23391063f7a93edaccadfde8dc726ff895fe50ce09dc19b9cd84d846d4ddd5a70046edfec8055c604dd14d64ec12b5d6dc56a0ffff85114
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,268 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
Found validating 0.2.1 against a live tenant before publishing, and by reviewing
|
|
13
|
+
the same release. Folded in rather than shipped separately — 0.2.1 was never
|
|
14
|
+
published.
|
|
15
|
+
|
|
16
|
+
### Fixed — found validating and reviewing
|
|
17
|
+
- **`Charge::STATUSES` was the filter list, not the field list.** It shipped with
|
|
18
|
+
nine values, built from the 400 the API returns for an unrecognised filter. The
|
|
19
|
+
field itself can also hold `excluded`, which `?status=` does not accept — two
|
|
20
|
+
different lists, now `STATUSES` (10) and `FILTERABLE_STATUSES` (9).
|
|
21
|
+
- **`REFERENCE.md` still documented the pre-0.2.1 behaviour** — `contract.status`,
|
|
22
|
+
`charge.pending?`, and `status` listed as `active, ended, cancelled` /
|
|
23
|
+
`pending, paid, overdue`. A reader following it wrote code against fields and
|
|
24
|
+
values that do not exist. Every doc example filtering charges by
|
|
25
|
+
`status: "pending"` was also rejected by the API; corrected to `unpaid`.
|
|
26
|
+
- **Deprecations warn once per process.** `charges.select(&:pending?)` over one
|
|
27
|
+
page emitted a hundred identical lines, which is how a warning stops being read.
|
|
28
|
+
- `HANDOFF-0.2.1` §2 offered `Charge.settle(1)` to verify read-only mode. That
|
|
29
|
+
calls `find` first — a GET the guard allows — so it answered `NotFound` and
|
|
30
|
+
never reached the guard; and making it work would require pointing a real
|
|
31
|
+
settlement at a real charge to test the brake. Replaced with a transport call.
|
|
32
|
+
(#26)
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- `Contract#active?` documents that `ended?` is the safe negation: `!active?`
|
|
36
|
+
reads an unknown contract as closed, because Ruby cannot tell `nil` from
|
|
37
|
+
`false` through `!`.
|
|
38
|
+
|
|
39
|
+
### Fixed — the original issue #23
|
|
40
|
+
- **`Contract#active?` and `#ended?` read `is_active`.** They compared a `status`
|
|
41
|
+
field contracts have never had, so `active?` answered `false` for an active
|
|
42
|
+
contract — the answer that biases a caller toward creating a second one. Both
|
|
43
|
+
return `nil` rather than a guess when the response did not carry `is_active`.
|
|
44
|
+
Deliberately not derived from `end_date`: an active contract can carry a future
|
|
45
|
+
closing date, so a present `end_date` does not mean closed. (#23)
|
|
46
|
+
- **`Charge#pending?` and `#overdue?` never matched anything.** API v2 has no
|
|
47
|
+
`pending` or `overdue` status — it rejects both and names the real set in the
|
|
48
|
+
400. The open state is **`unpaid`**. `#unpaid?` and `#cancelled?` are the
|
|
49
|
+
replacements; `pending?` stays as a deprecated alias, and `overdue?` warns and
|
|
50
|
+
returns false, since an overdue charge is `unpaid` with a `due_date` in the
|
|
51
|
+
past. `Charge::STATUSES` records the enum the API named.
|
|
52
|
+
- **`Contract`'s attribute documentation** described `status`, `payment_day`,
|
|
53
|
+
`value` and `billing_day`. None of the four exists. Replaced with the real
|
|
54
|
+
fields, verified against a live response rather than the collection alone.
|
|
55
|
+
|
|
56
|
+
`Sale`'s predicates were checked and left alone — `billed`, `paid` and
|
|
57
|
+
`notBilled` are all real values.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- `PostmanCollection.response_fields`, and specs asserting that the attributes
|
|
61
|
+
the predicates depend on appear in a documented response. The contract layer
|
|
62
|
+
compared verbs and paths only, which is exactly how a predicate could read a
|
|
63
|
+
field that has never existed. It is deliberately a union across examples and
|
|
64
|
+
never an exact set: the collection under-documents responses, so an exact-match
|
|
65
|
+
check would report real fields as missing.
|
|
66
|
+
|
|
67
|
+
## [0.2.0] - 2026-08-11
|
|
68
|
+
|
|
69
|
+
Aligns the gem with the published API v2 contract. **0.1.1 is broken**:
|
|
70
|
+
`Charge.settle` and `Contract.end_contract` 404 against a live tenant, and a
|
|
71
|
+
successful write that answers with an empty body raises `NoMethodError` after
|
|
72
|
+
the operation has already taken effect — in a billing integration that invites a
|
|
73
|
+
retry. Upgrading is strongly recommended.
|
|
74
|
+
|
|
75
|
+
Every fix below was verified against `docs/postman-collection.json`, Conexa's own
|
|
76
|
+
published documentation, and is now enforced by
|
|
77
|
+
`spec/contract/api_contract_spec.rb`.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- **Empty response bodies no longer raise, anywhere.** `Request#run` returns `{}`
|
|
81
|
+
for an empty, whitespace or `null` body at any status. With the Oj adapter
|
|
82
|
+
`MultiJson.decode("")` returns `nil` without raising, so the previous
|
|
83
|
+
`rescue MultiJson::ParseError` / `204` guard never fired — even a documented
|
|
84
|
+
`204` success (`PATCH /charge/settle/:id`) raised `NoMethodError`, which is not
|
|
85
|
+
a `Conexa::ConexaError` and escaped `rescue Conexa::ConexaError`.
|
|
86
|
+
|
|
87
|
+
`ConexaObject#update` treats anything carrying no attributes as "nothing to
|
|
88
|
+
merge" — `nil`, `{}`, and the arrays and scalars `Request#run` also produces —
|
|
89
|
+
and `Model#create` does the same. Fixing `Request#run` alone was not enough:
|
|
90
|
+
`Model#save` and `#destroy` fed the result straight into `update`, so **every**
|
|
91
|
+
resource's generic CRUD still raised `NoMethodError` on a documented `204`
|
|
92
|
+
(`DELETE /sale/:id` among them). Only the three action methods were safe, and
|
|
93
|
+
only because they discard their return value.
|
|
94
|
+
- **A write answering `200 {}` no longer wipes the object.** `update` removes
|
|
95
|
+
every attribute absent from the incoming hash, which is right for a full
|
|
96
|
+
refresh and destructive for an empty write response — it cleared the whole
|
|
97
|
+
object, primary key included, and reported success.
|
|
98
|
+
- **`Model#save` guards a blank id**, as `#destroy` already did. Without it an
|
|
99
|
+
id-less object issued `PATCH /customer/` instead of failing fast.
|
|
100
|
+
- **`Result#next_page` raises a `Conexa` error** when pagination carries no
|
|
101
|
+
`limit`, instead of `TypeError` from `nil + Integer`, and survives a
|
|
102
|
+
non-marshallable filter in the original query.
|
|
103
|
+
- **`Conexa::ValidationError` is usable.** It rendered as the bare string
|
|
104
|
+
`"Conexa::ValidationError"` and `#to_h` raised `NoMethodError`. It is only
|
|
105
|
+
reachable for an error body with no `message` key — which no documented
|
|
106
|
+
response has — but it now reports what actually arrived.
|
|
107
|
+
- **Exception messages read as prose.** The old format appended a dangling
|
|
108
|
+
`"=> Erros: "` to the 75 documented responses that carry no `errors` array, and
|
|
109
|
+
dumped Ruby's `#inspect` of an array of hashes for the ones that do. It now
|
|
110
|
+
uses the same normalisation as `#api_error_messages`:
|
|
111
|
+
`422 … => It was not possible to process your request — CHARGE_11: Only open charges can be settled.`
|
|
112
|
+
- **Error bodies that are not JSON objects no longer raise `TypeError`.** An
|
|
113
|
+
array or scalar error body reached `parsed_error['message']` and blew up inside
|
|
114
|
+
the error handler.
|
|
115
|
+
- **`Request#run` tolerates a top-level array or scalar body.** Defensive: every
|
|
116
|
+
list endpoint probed on 2026-08-12 used the `{data, pagination}` envelope, so
|
|
117
|
+
this shape has not been observed in the wild — it removes a crash class rather
|
|
118
|
+
than fixing a seen failure.
|
|
119
|
+
- **Action endpoints use the documented verb.** `Charge#settle`,
|
|
120
|
+
`Contract#end_contract` and `RecurringSale#end_recurring_sale` send `PATCH`;
|
|
121
|
+
`POST` 404s.
|
|
122
|
+
- **`Conexa::Company.all` requests the documented `/companies`.** `Model#url`
|
|
123
|
+
pluralizes by appending `"s"`, which produced `/companys`. A read-only probe of
|
|
124
|
+
a live tenant on 2026-08-12 showed the API routes **both** spellings, so this
|
|
125
|
+
was an undocumented path that worked rather than the 404 first reported — the
|
|
126
|
+
override stands because an undocumented alias can vanish without notice, but
|
|
127
|
+
nothing was broken by it.
|
|
128
|
+
- **Nested arrays are camelized.** `Util.camelize_hash` treated an `Array` as a
|
|
129
|
+
scalar, so snake_case keys inside arrays of objects were sent untouched and
|
|
130
|
+
rejected. Affects `complementaryServices`, `productQuotas`, `devices`,
|
|
131
|
+
`extraFields`, `bookingModels`, `visitors` and `costCenters` — ten documented
|
|
132
|
+
endpoints across seven resources.
|
|
133
|
+
- **`page`/`size` are converted to `limit`/`offset`** instead of being sent. The
|
|
134
|
+
API validates `page` and then ignores it, always answering `offset: 0`, so a
|
|
135
|
+
loop driven by `hasNext` never terminated and silently re-yielded the same
|
|
136
|
+
batch. Non-positive or non-integer values now raise `Conexa::RequestError`.
|
|
137
|
+
- **`Model#id` falls back to `attributes["id"]` again.** `primary_key_attribute`
|
|
138
|
+
aliased `#id` onto the resource key, making the documented fallback dead code —
|
|
139
|
+
write endpoints answer with `{"id": N}`, which is what `Model#create` reads.
|
|
140
|
+
- **`ResponseError` no longer raises `NoMethodError` on malformed bodies.** It was
|
|
141
|
+
handed a `RestClient::Response`, which has no `#message`.
|
|
142
|
+
- **A broken connection raises `Conexa::ConnectionError`** (issue #11). The
|
|
143
|
+
`rescue RestClient::ServerBrokeConnection` clause was unreachable —
|
|
144
|
+
`ServerBrokeConnection` subclasses `RestClient::Exception`, so the broader
|
|
145
|
+
clause listed above it always matched first, then tried to decode a `nil`
|
|
146
|
+
`http_body` and raised `NoMethodError` from inside the error handler.
|
|
147
|
+
Connection-level failures are now rescued first: `SocketError`,
|
|
148
|
+
`ServerBrokeConnection`, `SSLCertificateNotVerified` and
|
|
149
|
+
`RestClient::Exceptions::Timeout`. A real HTTP 408 stays in the response
|
|
150
|
+
taxonomy, since `RestClient::RequestTimeout` is a *superclass* of
|
|
151
|
+
`Exceptions::Timeout`.
|
|
152
|
+
- **An unusable id raises `Conexa::RequestError`, not `URI::InvalidURIError`**
|
|
153
|
+
(issue #12). `find(" 123 ")` leaked a URI error from inside RestClient, outside
|
|
154
|
+
the `Conexa::ConexaError` hierarchy. Surrounding whitespace is now stripped, and
|
|
155
|
+
`Request#full_api_url` validates the URL it builds.
|
|
156
|
+
- **`RecurringSale#end_recurring_sale` sends the documented `date`.** The verb was
|
|
157
|
+
corrected but the field was not, so `end_date:` still went out as the rejected
|
|
158
|
+
`endDate`, and its YARD named that field as the example. It now shares
|
|
159
|
+
`Util.normalize_end_date_param` with `Contract`, and gains a `set_end_date`
|
|
160
|
+
alias for symmetry.
|
|
161
|
+
- `README.md` configured a non-existent `config.subdomain`; the quick start now
|
|
162
|
+
runs.
|
|
163
|
+
- `lib/conexa/resources/supplier.rb` shipped as `0600`, breaking `require` on a
|
|
164
|
+
shared install.
|
|
165
|
+
|
|
166
|
+
- **`Conexa::CreditCard` is write-only.** API v2 exposes no read for it: `GET
|
|
167
|
+
/creditCard` answers "Unable to resolve the request" and `GET /creditCard/:id`
|
|
168
|
+
"unable to find the requested action" — neither is the permission wording the
|
|
169
|
+
API uses for a resource an account cannot see. `all`/`find`/`find_by` and their
|
|
170
|
+
aliases now raise `Conexa::RequestError` explaining that, instead of a bare
|
|
171
|
+
`NotFound` reading as "no such card", and `#create` skips `Model#create`'s
|
|
172
|
+
re-fetch. The integration spec that asserted those reads worked — against
|
|
173
|
+
stubs of endpoints that do not exist — was rewritten.
|
|
174
|
+
- **Listings always answer with a `Conexa::Result`.** An empty body yielded `nil`
|
|
175
|
+
and a bare-array body an `Array`, so `.data`/`.pagination`/`.next_page` failed
|
|
176
|
+
far from the cause.
|
|
177
|
+
- **`Model#fetch` raises instead of silently keeping stale attributes** when the
|
|
178
|
+
refresh comes back with no body. The nil-guard that makes an empty *write*
|
|
179
|
+
response harmless must not make an empty *read* look successful.
|
|
180
|
+
- **`Result#has_next?` returns a boolean**, not `nil`, when there is no
|
|
181
|
+
pagination at all.
|
|
182
|
+
|
|
183
|
+
### Added
|
|
184
|
+
- **Read-only mode.** `config.read_only = true`, `CONEXA_READ_ONLY=1`, or a
|
|
185
|
+
`Conexa.read_only { ... }` block. Any non-`GET` request raises
|
|
186
|
+
`Conexa::ReadOnlyError` before it leaves the process. Settling a charge moves
|
|
187
|
+
money and can issue an NF-e, so this is a guard worth having when you only mean
|
|
188
|
+
to read.
|
|
189
|
+
|
|
190
|
+
The exemption for `POST /auth` is matched on the **path**, not on the caller's
|
|
191
|
+
`auth:` flag — `Request.auth` is public, so keying it off the flag would have
|
|
192
|
+
let any write opt out of the guard. `CONEXA_READ_ONLY` is consulted at each
|
|
193
|
+
check rather than captured at configure time, and an unrecognised value warns
|
|
194
|
+
instead of silently failing open. The block form is fiber-local and does not
|
|
195
|
+
reach a `Thread` or `Fiber` spawned inside it — documented in the READMEs; use
|
|
196
|
+
`config.read_only` for concurrent work.
|
|
197
|
+
- **`ResponseError#api_errors`, `#api_error_codes`, `#api_error_messages`,
|
|
198
|
+
`#api_response`** — normalise the API's two error shapes (`{field, messages}`
|
|
199
|
+
and `{code, message}`). Consumers that only handled the first rendered
|
|
200
|
+
business-rule errors as blank strings, which is how
|
|
201
|
+
`CONTRACT_RECURRING_SALE_10` stayed invisible through eight attempts.
|
|
202
|
+
`api_error_codes` makes documented codes such as `CHARGE_11` usable as control
|
|
203
|
+
flow — the way to tell an already-settled charge from a real failure.
|
|
204
|
+
- **`Contract#set_end_date`** (with `end_contract` kept as an alias), taking the
|
|
205
|
+
documented `date:`, `reason_id:` and `unlink_customer:`. The endpoint both
|
|
206
|
+
closes a contract and amends its end date, and a future date on a closed
|
|
207
|
+
contract reopens it.
|
|
208
|
+
- **Contract-level test coverage**: `spec/contract/api_contract_spec.rb` checks
|
|
209
|
+
every emitted URL and action verb against the vendored collection, with an
|
|
210
|
+
explicit allowlist and a guard that fails when an endpoint is added without one.
|
|
211
|
+
`spec/support/request_capture.rb` asserts the request the gem actually emits
|
|
212
|
+
rather than stubbing the expected one.
|
|
213
|
+
- `verify_partial_doubles = true`.
|
|
214
|
+
- YARD documentation of `POST /contract`'s fields, including the conditional
|
|
215
|
+
`due_day` rule and the atomic `firstOccurrenceSettleRetroactive` flow.
|
|
216
|
+
|
|
217
|
+
### Changed
|
|
218
|
+
- **Breaking**: `end_contract` sends the documented `date`; `end_date:` still
|
|
219
|
+
works as a deprecated alias and will be removed in 0.3.0.
|
|
220
|
+
- **Breaking**: writes that answer with an empty body now return normally instead
|
|
221
|
+
of raising. Callers treating the exception as a signal must be reviewed.
|
|
222
|
+
- **Breaking**: `page`/`size` no longer reach the wire.
|
|
223
|
+
- Minimum Ruby is now **3.1** (was 2.6, never tested below 3.1). CI covers
|
|
224
|
+
3.1–3.4.
|
|
225
|
+
- `rest-client` and `multi_json` are version-bounded. `multi_json`'s adapter
|
|
226
|
+
behaviour is load-bearing — the empty-body defect exists because Oj returns
|
|
227
|
+
`nil` rather than raising.
|
|
228
|
+
- The gemspec's development dependencies move to bundler's `:test` group, and
|
|
229
|
+
`debug`/`byebug` to an optional `:development` group, so a native-extension
|
|
230
|
+
failure in `io-console` can no longer stop `bundle exec rspec`.
|
|
231
|
+
- **The packaged gem is 48 KB, down from 163 KB.** `spec.files` shipped the whole
|
|
232
|
+
repository — including `docs/postman-collection.json` (1.7 MB) — against 58 KB
|
|
233
|
+
of library code. It now ships `lib/` and the documentation only.
|
|
234
|
+
- **`Gemfile.lock` is no longer committed**, as is conventional for a gem: a
|
|
235
|
+
pinned lockfile defeats testing against the range the gemspec allows, and CI
|
|
236
|
+
had been working around it by deleting the file before `bundle install`.
|
|
237
|
+
- **`rake` passes again.** The default task is `spec` + `rubocop`, and RuboCop's
|
|
238
|
+
~2000 pre-existing offences made it fail, which is why CI ran lint with
|
|
239
|
+
`continue-on-error`. A generated `.rubocop_todo.yml` grandfathers them, so new
|
|
240
|
+
code is genuinely linted and CI can gate on it.
|
|
241
|
+
- `rake spec:all` runs the suite across every supported Ruby via mise; `rake ci`
|
|
242
|
+
adds RuboCop. CI uses `bundler-cache: true`.
|
|
243
|
+
- **Releases publish from CI via Trusted Publishing.** Pushing a `v*` tag runs
|
|
244
|
+
the full gate and then exchanges GitHub's OIDC token for a short-lived,
|
|
245
|
+
push-scoped RubyGems credential — no API key is stored anywhere. Two gates run
|
|
246
|
+
first, both aimed at how 0.1.1 went out: the tag must match `Conexa::VERSION`
|
|
247
|
+
(the `v0.1.1` tag points at code carrying `0.1.0`), and the packaged gem must
|
|
248
|
+
contain only `lib/` and the documentation.
|
|
249
|
+
|
|
250
|
+
### Removed
|
|
251
|
+
- **Breaking**: `Conexa::Client`, `Conexa::Authenticator` and
|
|
252
|
+
`Conexa::TokenManager`. They referenced five `Conexa` module methods that never
|
|
253
|
+
existed (`credentials`, `secret_key`, `access_key`, `client_id`,
|
|
254
|
+
`default_client_key`), so any real use raised `NoMethodError`; their specs
|
|
255
|
+
passed only because they stubbed those methods into being. The `jwt` dependency
|
|
256
|
+
goes with them — `Conexa::Auth` (the v2 `/auth` resource) is unaffected.
|
|
257
|
+
- **Breaking**: `Conexa::OrderCommon`. Unlike the three above, this one *ran* —
|
|
258
|
+
it built `/order/:id` and `/order/:id/refund` and issued a real `DELETE`. It is
|
|
259
|
+
removed because **API v2 documents no `/order` endpoint at all**: nothing in
|
|
260
|
+
the gem used it, no spec covered it, and it targets a surface that no longer
|
|
261
|
+
exists. Note that its documented backwards-compatible alias never worked:
|
|
262
|
+
`OrderCommom = self` inside the class body defines
|
|
263
|
+
`Conexa::OrderCommon::OrderCommom`, not `Conexa::OrderCommom`, so the pre-0.1.0
|
|
264
|
+
name has raised `NameError` since the rename.
|
|
265
|
+
|
|
266
|
+
### Testing
|
|
267
|
+
- VCR cassettes recorded against a real tenant are anonymised, and
|
|
268
|
+
`spec_helper.rb` filters the API token out of new recordings. The
|
|
269
|
+
`claude_scripts/sanitize_cassettes/` script does the anonymisation in place,
|
|
270
|
+
keeping the JSON shape so the specs still exercise the same structure.
|
|
271
|
+
|
|
10
272
|
## [0.1.1] - 2026-03-31
|
|
11
273
|
|
|
12
274
|
### Added
|
|
@@ -133,7 +395,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
133
395
|
- Charge with settle and PIX methods
|
|
134
396
|
- Pagination support
|
|
135
397
|
|
|
136
|
-
[Unreleased]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.
|
|
398
|
+
[Unreleased]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.2.1...HEAD
|
|
399
|
+
[0.2.1]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.2.0...v0.2.1
|
|
400
|
+
[0.2.0]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.1.1...v0.2.0
|
|
137
401
|
[0.1.1]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.1.0...v0.1.1
|
|
138
402
|
[0.1.0]: https://github.com/guilhermegazzinelli/conexa-ruby/compare/v0.0.9...v0.1.0
|
|
139
403
|
[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: 'unpaid') # 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
|
|
|
@@ -240,7 +272,7 @@ charge.due_date # => "2024-02-10"
|
|
|
240
272
|
# List charges
|
|
241
273
|
charges = Conexa::Charge.all(
|
|
242
274
|
customer_id: [127],
|
|
243
|
-
status: '
|
|
275
|
+
status: 'unpaid',
|
|
244
276
|
due_date_from: '2024-01-01',
|
|
245
277
|
due_date_to: '2024-12-31'
|
|
246
278
|
)
|
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: 'unpaid') # 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
|
|
|
@@ -166,7 +200,7 @@ Conexa::Company.all(
|
|
|
166
200
|
)
|
|
167
201
|
|
|
168
202
|
# Filtrar faturas por status
|
|
169
|
-
Conexa::Bill.all(status: "
|
|
203
|
+
Conexa::Bill.all(status: "unpaid", limit: 20)
|
|
170
204
|
|
|
171
205
|
# Filtrar vendas
|
|
172
206
|
Conexa::Sale.all(page: 2, size: 6)
|
|
@@ -494,7 +528,7 @@ Conexa::Bill.destroy(bill_id)
|
|
|
494
528
|
##### Listando Faturas
|
|
495
529
|
|
|
496
530
|
```ruby
|
|
497
|
-
faturas = Conexa::Bill.
|
|
531
|
+
faturas = Conexa::Bill.find_by({ status: 'unpaid', limit: 20 })
|
|
498
532
|
```
|
|
499
533
|
|
|
500
534
|
#### Cobranças (Charges)
|
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: 'unpaid')
|
|
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.
|
|
@@ -311,10 +339,18 @@ contract = Conexa::Contract.create_with_products(
|
|
|
311
339
|
|
|
312
340
|
# Find contract
|
|
313
341
|
contract = Conexa::Contract.find(456)
|
|
314
|
-
contract.
|
|
342
|
+
contract.is_active # => true (contracts have no `status` field)
|
|
315
343
|
contract.active? # => true
|
|
316
344
|
contract.ended? # => false
|
|
317
345
|
|
|
346
|
+
# An ACTIVE contract may carry a future end_date — a scheduled close is not a
|
|
347
|
+
# close. Do not derive "ended" from end_date.
|
|
348
|
+
contract.end_date # => "2026-11-30" while still active?
|
|
349
|
+
|
|
350
|
+
# active?/ended? return nil when the response did not carry is_active, rather
|
|
351
|
+
# than guessing. Prefer ended? over !active?: !nil is true, which would read an
|
|
352
|
+
# unknown contract as closed.
|
|
353
|
+
|
|
318
354
|
# List contracts
|
|
319
355
|
contracts = Conexa::Contract.all(
|
|
320
356
|
customer_id: [127],
|
|
@@ -368,7 +404,9 @@ contract.destroy
|
|
|
368
404
|
|
|
369
405
|
**Read-only attributes:**
|
|
370
406
|
- `contract_id` - ID
|
|
371
|
-
- `
|
|
407
|
+
- `is_active` - whether the contract is open (there is **no** `status` field)
|
|
408
|
+
- `end_date` - closing date; may be in the future on an active contract
|
|
409
|
+
- `due_day`, `amount`, `payment_frequency`, `end_reason_id`, `first_due_date`
|
|
372
410
|
|
|
373
411
|
**Helper methods:**
|
|
374
412
|
- `active?` - Check if active
|
|
@@ -391,17 +429,19 @@ charge = Conexa::Charge.create(
|
|
|
391
429
|
|
|
392
430
|
# Find charge
|
|
393
431
|
charge = Conexa::Charge.find(789)
|
|
394
|
-
charge.status # => "
|
|
432
|
+
charge.status # => "unpaid"
|
|
395
433
|
charge.amount # => 199.90
|
|
396
434
|
charge.due_date # => "2024-02-10"
|
|
397
435
|
charge.paid? # => false
|
|
398
|
-
charge.
|
|
399
|
-
charge.
|
|
436
|
+
charge.unpaid? # => true (the open state is `unpaid`, not `pending`)
|
|
437
|
+
charge.cancelled? # => false
|
|
438
|
+
Conexa::Charge::STATUSES # every value the field can take
|
|
439
|
+
Conexa::Charge::FILTERABLE_STATUSES # what ?status= accepts — not the same list
|
|
400
440
|
|
|
401
441
|
# List charges
|
|
402
442
|
charges = Conexa::Charge.all(
|
|
403
443
|
customer_id: [127],
|
|
404
|
-
status: '
|
|
444
|
+
status: 'unpaid',
|
|
405
445
|
due_date_from: '2024-01-01',
|
|
406
446
|
due_date_to: '2024-01-31'
|
|
407
447
|
)
|
|
@@ -458,8 +498,11 @@ Conexa::Charge.cancel(789)
|
|
|
458
498
|
|
|
459
499
|
**Helper methods:**
|
|
460
500
|
- `paid?` - Check if paid
|
|
461
|
-
- `
|
|
462
|
-
- `
|
|
501
|
+
- `unpaid?` - Check if still open
|
|
502
|
+
- `cancelled?` - Check if cancelled
|
|
503
|
+
- `pending?` - **deprecated** alias of `unpaid?`; the API has no `pending`
|
|
504
|
+
- `overdue?` - **deprecated**, always false. The API has no `overdue`: an overdue
|
|
505
|
+
charge is `unpaid` with a `due_date` in the past, so compare the date yourself
|
|
463
506
|
|
|
464
507
|
**Special methods:**
|
|
465
508
|
- `settle(params)` / `Charge.settle(id, params)` - Mark as paid
|
|
@@ -1222,7 +1265,7 @@ Conexa::Sale.all(status: 'notBilled')
|
|
|
1222
1265
|
# Combined
|
|
1223
1266
|
Conexa::Charge.all(
|
|
1224
1267
|
customer_id: [127],
|
|
1225
|
-
status: '
|
|
1268
|
+
status: 'unpaid',
|
|
1226
1269
|
due_date_from: '2024-01-01',
|
|
1227
1270
|
page: 1,
|
|
1228
1271
|
size: 100
|
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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conexa
|
|
4
|
+
# Emits each deprecation once per process.
|
|
5
|
+
#
|
|
6
|
+
# The warnings exist to be read once and acted on. Emitting per call turns them
|
|
7
|
+
# into noise a caller learns to filter — `charges.select(&:pending?)` over one
|
|
8
|
+
# page of results produced a hundred identical lines, which is how a warning
|
|
9
|
+
# stops being read.
|
|
10
|
+
module Deprecation
|
|
11
|
+
@seen = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# @param key [Object] identifies the deprecation, not the call site
|
|
16
|
+
# @param message [String] what changed and what to do instead
|
|
17
|
+
# @return [nil]
|
|
18
|
+
def warn_once(key, message)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
return nil if @seen[key]
|
|
21
|
+
|
|
22
|
+
@seen[key] = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Kernel.warn("DEPRECATION WARNING: #{message}")
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Only for tests: lets a spec observe a warning that another example
|
|
30
|
+
# already consumed.
|
|
31
|
+
# @api private
|
|
32
|
+
def reset!
|
|
33
|
+
@mutex.synchronize { @seen = {} }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Mixed into the classes that carry deprecated methods.
|
|
39
|
+
module Deprecatable
|
|
40
|
+
def deprecate(key, message)
|
|
41
|
+
Deprecation.warn_once([self, key], message)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|