Files
invoiceninja-sdk/CHANGELOG.md
T
awkawbandClaude Opus 5 23c660d2f9 Add retry with backoff, and narrow the untyped responses
Retry:
- `RetryPolicy` (3 retries, full-jitter exponential backoff) applied in a
  new `execute` dispatch chokepoint, configurable via
  `with_retry_policy`. `Retry-After` is honoured, and one longer than
  `max_delay` is surfaced rather than slept through.
- What may be replayed depends on the method, not just the status. A 429
  means the server refused the request without processing it, so any
  method is safe. A 5xx or timeout gives no such guarantee, so only
  idempotent methods are retried: this is a billing API, and replaying a
  POST the server had already applied would duplicate an invoice,
  payment, or email.

Response types:
- `statics`, `search`, and `health_check` return `Statics`,
  `SearchResults`, and `HealthCheck` instead of being generic.
  `HealthCheck` covers both deployments — hosted returns a bare message
  rather than a report — and `SearchResults` covers both the
  Elasticsearch and non-Elasticsearch bucket sets.
- Each keeps the `extra` convention, and `statics_as` / `search_as` /
  `health_check_as` remain for pinning shapes left untyped. Nested config
  blobs (gateways, templates, extensions) stay `Value`, same reasoning as
  `Company::settings`.
- `send_email` previously decoded the response as a bare body, but the
  controller returns `itemResponse(...)` — a `data`-wrapped entity. It now
  unwraps, so `T` is the entity. Still generic, since the endpoint serves
  invoices, quotes, credits, and purchase orders.
- `chart` stays generic on purpose: the eleven charts share no response
  shape.
- Adds `id_as_string`, since `/statics` returns `payment_types.id` as a
  string and the other tables return numbers.

Adds CHANGELOG.md (Keep a Changelog), and a CLAUDE.md note to keep it
current. 17 new tests, 43 in client_requests.rs total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-23 18:18:12 -04:00

3.7 KiB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • Wrappers for every one of the API's 34 Route::resource groups, plus users — 265 methods. See COVERAGE.md for the per-endpoint inventory.
  • COVERAGE.md, a detailed record of what is wrapped, what is not, and why, checked against routes/api.php on the v5-stable branch.
  • Retry with full-jitter exponential backoff, configurable through RetryPolicy and InvoiceNinjaClient::with_retry_policy. Defaults to 3 retries. Retry-After is honoured when the server sends it.
  • InvoiceNinjaClient::with_timeout and a DEFAULT_TIMEOUT of 60 seconds applied to every request.
  • Typed responses for previously untyped endpoints: Statics, StaticItem, Currency, DateFormat, SearchResults, SearchHit, HealthCheck, and PhpVersion.
  • statics_as, search_as, and health_check_as as generic escape hatches for callers who need to pin a shape the typed models leave in extra.
  • tests/client_requests.rs: 43 tests covering the request plumbing against a loopback wiremock server. No network or credentials required.

Changed

  • statics, search, and health_check return concrete types instead of being generic over T: DeserializeOwned.
  • send_email now unwraps the data envelope, so T is the entity (Invoice, Quote, …) rather than a wrapper the caller had to model. It stays generic because the endpoint serves all four entity types.
  • chart remains generic, deliberately: the eleven chart endpoints share no common response shape, so there is nothing to narrow to.
  • The base URL is parsed and validated when the client is constructed. A missing scheme used to produce a 404 on every call instead of one error.
  • list_all_* terminates on meta.pagination.total_pages when the server sends it, falling back to the short-page heuristic only when it does not.

Fixed

  • list_all_* silently truncated results. The server caps page size at min(abs(per_page), 5000), so a page shorter than requested did not mean the last page — the walk stopped early and returned a partial list as though it were complete. per_page is now clamped to the server maximum so the fallback heuristic holds.
  • list_all_* with per_page(0) never terminated: the check got < 0 has no solution. It now falls back to the default page size, and a MAX_PAGES backstop raises Error::PaginationLimit rather than quietly truncating.
  • Requests could hang indefinitely. reqwest applies no timeout of its own and none was configured.
  • Ids were interpolated into request paths unescaped, so a /, ?, or # in an id changed which endpoint was addressed.

Security

  • Ids used as URL path segments are escaped, and ids that are empty or consist only of dots are rejected. Escaping alone is insufficient: the WHATWG rules the url crate implements decode %2E back to . before resolving dot segments, so an escaped .. still collapsed the path. An empty id would have addressed the collection route, making delete_client("") mean DELETE /clients.
  • POST is never replayed on a 5xx or timeout, since the server may have applied it before the response was lost and a replay would duplicate an invoice, payment, or email. A 429 is the exception — the server states it refused the request, so no work was done.

[0.1.0] - 2026-10-23

Added

  • Initial release: async client for the Invoice Ninja v5 API covering clients, invoices, quotes, credits, payments, products, expenses, projects, tasks, and vendors.