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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c71329999c6d13c4e935ef1e37a8eca6df5811c44d0326184cc1676d31fb199b
4
- data.tar.gz: c6f6946375a05712ed7d952a4314738e86a2429bf3a249e269d2fd40b254c11f
3
+ metadata.gz: af7a51117e4a311ae46b22248f416838e385336138f60d80c741aa671f47c4fe
4
+ data.tar.gz: 44aefe94b1b0fccf3b186be4377308bb6fe16e734d6c7c3fbd8adf7e5568cdc9
5
5
  SHA512:
6
- metadata.gz: 13fc7859937cd156cd8ae68b78d6c303b62460d4f6e682f270f58927e87c8a99e29e7d43a4814318b53ed5c5e20ce5e0ea3892d46db8d4d08147e9aea79a690e
7
- data.tar.gz: c1ae89758b0cea23df6c5f5cc16895c060baef04da9a31fefd58eb7ac9c7ba1b1483dfc44b81c00ddfb592cada2e34cace170237e5f298b3ea8b3217572bd1c9
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.1.1...HEAD
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.subdomain = 'YOUR_SUBDOMAIN' # your-company.conexa.app
29
- config.api_token = 'YOUR_API_TOKEN' # Application Token from Conexa
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.subdomain = 'mycompany'
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: 'pending',
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: "pending", page: 1, size: 20)
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.find({ status: 'pending' }, 1, 20)
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 = 'https://api.conexa.com.br' # or sandbox URL
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.status # => "active"
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
- - `status` - active, ended, cancelled
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 # => "pending"
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.pending? # => true
399
- charge.overdue? # => false
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: 'pending',
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
- - `pending?` - Check if pending
462
- - `overdue?` - Check if overdue
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: 'pending',
1268
+ status: 'unpaid',
1226
1269
  due_date_from: '2024-01-01',
1227
1270
  page: 1,
1228
1271
  size: 100
@@ -2,11 +2,57 @@
2
2
 
3
3
  module Conexa
4
4
  class Configuration
5
- attr_accessor :api_token, :api_host
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