zazu-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +21 -0
- data/README.md +178 -0
- data/lib/zazu/client.rb +169 -0
- data/lib/zazu/errors.rb +74 -0
- data/lib/zazu/page.rb +67 -0
- data/lib/zazu/resources/accounts.rb +61 -0
- data/lib/zazu/resources/base.rb +136 -0
- data/lib/zazu/resources/customers.rb +40 -0
- data/lib/zazu/resources/entity.rb +14 -0
- data/lib/zazu/resources/invoices.rb +69 -0
- data/lib/zazu/resources/payment_links.rb +34 -0
- data/lib/zazu/resources/webhook_endpoints.rb +60 -0
- data/lib/zazu/response.rb +64 -0
- data/lib/zazu/version.rb +5 -0
- data/lib/zazu.rb +30 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bb2684661a728ba3f69777037fa66bbc8c9a0ed50b168709b3be159dabb9d5d4
|
|
4
|
+
data.tar.gz: 8ff94850c110055ea939adc202010df5f8f2622f64d4935e79b67e848a48e986
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fef8dcde5e3433ece2c550573fc717a5bd172ebaf9d13284eb3e77b44503793d10859fa7324669551189ce4230e059c9ba436b173e5a90d8c5cec90cab2270d0
|
|
7
|
+
data.tar.gz: ad848283fcb72e9c17d385fba58f19588370db028f673963a49ac682d9c7aefc718969586867e67e542da966df0e2038ac78a1398c533b3dbf70471011092ca9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `zazu-ruby` are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0]
|
|
11
|
+
|
|
12
|
+
Initial release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `Zazu::Client` — Faraday + HTTPX adapter, JSON request/response, retry middleware.
|
|
17
|
+
- Resource modules: `Accounts`, `Customers`, `Entity`, `Invoices`, `PaymentLinks`, `WebhookEndpoints`.
|
|
18
|
+
- Cursor-based pagination via `Zazu::Page` (max 100 records per page; no auto-pagination).
|
|
19
|
+
- Error hierarchy: `AuthenticationError`, `ForbiddenError`, `NotFoundError`,
|
|
20
|
+
`ValidationError`, `RateLimitError`, `ServerError`, `ConnectionError`,
|
|
21
|
+
`ConfigurationError`, `ArgumentError` — all under `Zazu::Error`.
|
|
22
|
+
- VCR-backed RSpec suite covering every public method.
|
|
23
|
+
- Cassette tarball published as a release asset for cross-language SDK reuse.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zazu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Zazu Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ruby SDK for the [Zazu API](https://zazu.ma). Faraday + HTTPX adapter for HTTP/2 + persistent connections.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gem "zazu-ruby"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The gem is published as `zazu-ruby` on RubyGems but loaded as `zazu` in code (the `zazu` name was already taken by an unrelated 2014-era gem).
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "zazu"
|
|
15
|
+
|
|
16
|
+
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])
|
|
17
|
+
# Or with explicit base URL (defaults to https://zazu.ma):
|
|
18
|
+
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"], base_url: "https://zazu.africa")
|
|
19
|
+
|
|
20
|
+
entity = zazu.entity.get
|
|
21
|
+
# => #<Zazu::Response status=200 ...>
|
|
22
|
+
entity.body["name"]
|
|
23
|
+
# => "Acme Corp"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Environment variables `ZAZU_API_KEY`, `ZAZU_BASE_URL`, `ZAZU_API_VERSION`, and `ZAZU_TIMEOUT` are read by default.
|
|
27
|
+
|
|
28
|
+
## Resources
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
zazu.entity.get
|
|
32
|
+
|
|
33
|
+
zazu.accounts.list(currency_code: "MAD", limit: 50)
|
|
34
|
+
zazu.accounts.get("019dde7d-...")
|
|
35
|
+
zazu.accounts.list_transactions("019dde7d-...", operation: "credit")
|
|
36
|
+
zazu.accounts.get_transaction("019dde7d-...", "01a0e1...")
|
|
37
|
+
|
|
38
|
+
zazu.customers.list(q: "acme")
|
|
39
|
+
zazu.customers.get("01a0...")
|
|
40
|
+
zazu.customers.create(
|
|
41
|
+
customer_type: "business",
|
|
42
|
+
company_name: "Acme Corp",
|
|
43
|
+
email: "billing@acme.com",
|
|
44
|
+
ice_number: "000000000000000"
|
|
45
|
+
)
|
|
46
|
+
zazu.customers.update("01a0...", email: "new@example.com")
|
|
47
|
+
zazu.customers.delete("01a0...")
|
|
48
|
+
|
|
49
|
+
zazu.invoices.list(status: "sent", limit: 50)
|
|
50
|
+
zazu.invoices.create(
|
|
51
|
+
customer_id: "01a0...",
|
|
52
|
+
currency_code: "MAD",
|
|
53
|
+
issue_date: "2026-05-03",
|
|
54
|
+
due_date: "2026-06-03",
|
|
55
|
+
items: [{ description: "Consulting", quantity: 10, unit_price: "150.00" }]
|
|
56
|
+
)
|
|
57
|
+
zazu.invoices.send_invoice("01a0...")
|
|
58
|
+
zazu.invoices.mark_as_paid("01a0...")
|
|
59
|
+
zazu.invoices.cancel("01a0...")
|
|
60
|
+
zazu.invoices.credit_note("01a0...")
|
|
61
|
+
zazu.invoices.create_payment_link("01a0...", account_id: "019dde7d-...")
|
|
62
|
+
|
|
63
|
+
zazu.payment_links.list(status: "active")
|
|
64
|
+
zazu.payment_links.create(
|
|
65
|
+
account_id: "019dde7d-...",
|
|
66
|
+
amount: "1500.00",
|
|
67
|
+
description: "March consulting",
|
|
68
|
+
link_type: "single"
|
|
69
|
+
)
|
|
70
|
+
zazu.payment_links.cancel("01a0...")
|
|
71
|
+
|
|
72
|
+
zazu.webhook_endpoints.list
|
|
73
|
+
zazu.webhook_endpoints.create(
|
|
74
|
+
url: "https://example.com/webhooks/zazu",
|
|
75
|
+
events: ["invoice.sent", "payment_link.paid"]
|
|
76
|
+
)
|
|
77
|
+
zazu.webhook_endpoints.test_endpoint("01a0...")
|
|
78
|
+
zazu.webhook_endpoints.regenerate_secret("01a0...")
|
|
79
|
+
zazu.webhook_endpoints.enable("01a0...")
|
|
80
|
+
zazu.webhook_endpoints.disable("01a0...")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Pagination
|
|
84
|
+
|
|
85
|
+
Every list endpoint returns a `Zazu::Page`. The SDK enforces a hard cap of **100 records per page** — there is no auto-pagination across pages.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
page = zazu.invoices.list(limit: 100)
|
|
89
|
+
page.data # => Array of invoice hashes
|
|
90
|
+
page.has_more # => true / false
|
|
91
|
+
page.next_cursor # => string or nil
|
|
92
|
+
|
|
93
|
+
# Walk pages explicitly:
|
|
94
|
+
while page
|
|
95
|
+
page.data.each { |inv| process(inv) }
|
|
96
|
+
page = page.next # returns nil when has_more is false
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For capped iteration, use the underlying `each_page_record` helper on a resource (private; access via `send` if you need it). The deliberate restriction is a guardrail — accidentally pulling 50,000 records in a single SDK call should be impossible without explicit per-page consent.
|
|
101
|
+
|
|
102
|
+
## Errors
|
|
103
|
+
|
|
104
|
+
Every non-2xx response raises a subclass of `Zazu::Error`:
|
|
105
|
+
|
|
106
|
+
| Status | Class |
|
|
107
|
+
|---|---|
|
|
108
|
+
| 401 | `Zazu::AuthenticationError` |
|
|
109
|
+
| 403 | `Zazu::ForbiddenError` |
|
|
110
|
+
| 404 | `Zazu::NotFoundError` |
|
|
111
|
+
| 422 | `Zazu::ValidationError` |
|
|
112
|
+
| 429 | `Zazu::RateLimitError` (carries `#retry_after`) |
|
|
113
|
+
| 5xx | `Zazu::ServerError` |
|
|
114
|
+
| network | `Zazu::ConnectionError` |
|
|
115
|
+
|
|
116
|
+
Each error exposes `#status`, `#request_id`, `#type`, `#param`, and the raw `#body`.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
begin
|
|
120
|
+
zazu.invoices.get("does-not-exist")
|
|
121
|
+
rescue Zazu::NotFoundError => e
|
|
122
|
+
e.status # => 404
|
|
123
|
+
e.request_id # => "req_..."
|
|
124
|
+
e.type # => "not_found_error"
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Versioning the API contract
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
zazu = Zazu.new(api_key: "...", api_version: "2026-03-27")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or via env: `ZAZU_API_VERSION=2026-03-27`. The header is sent on every request; the API echoes it back in `Zazu-Version`.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
bundle install
|
|
140
|
+
bundle exec rspec
|
|
141
|
+
bundle exec rubocop
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The spec suite is VCR-backed — cassettes live in `spec/fixtures/cassettes/` and are committed to the repo.
|
|
145
|
+
|
|
146
|
+
To re-record cassettes against staging:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cp .env.example .env
|
|
150
|
+
# fill in ZAZU_STAGING_API_KEY and the ZAZU_FIXTURE_*_ID values
|
|
151
|
+
bundle exec rake fixtures:record
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Cassettes are scrubbed before write — bearer tokens and request IDs are rewritten to placeholders. Even if a real key is in `.env`, the committed cassette never contains it.
|
|
155
|
+
|
|
156
|
+
## Cassettes for other-language SDKs
|
|
157
|
+
|
|
158
|
+
Each release of `zazu-ruby` publishes the cassette directory as a tarball release asset:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
https://github.com/getzazu/zazu-ruby/releases/download/v0.1.0/cassettes-v0.1.0.tar.gz
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`zazu-go`, `zazu-python`, etc. pin a specific version in their `.zazu-fixtures` file and download the tarball during CI. This guarantees every SDK is tested against the same recorded API interactions, surfacing cross-SDK inconsistencies immediately.
|
|
165
|
+
|
|
166
|
+
VCR's YAML format is supported natively by:
|
|
167
|
+
|
|
168
|
+
- Ruby — VCR (this gem)
|
|
169
|
+
- Go — go-vcr
|
|
170
|
+
- Python — VCR.py
|
|
171
|
+
- PHP — PHP-VCR
|
|
172
|
+
- Crystal — vcr-crystal / hi8.cr
|
|
173
|
+
- Rust — http_replayer (or a small custom YAML reader)
|
|
174
|
+
- JavaScript / TypeScript — Talkback or polly.js (slight format adapter needed)
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
data/lib/zazu/client.rb
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "httpx/adapters/faraday"
|
|
6
|
+
require "json"
|
|
7
|
+
require "securerandom"
|
|
8
|
+
|
|
9
|
+
module Zazu
|
|
10
|
+
# The main SDK entry point.
|
|
11
|
+
#
|
|
12
|
+
# zazu = Zazu::Client.new(api_key: "sk_live_...")
|
|
13
|
+
# zazu.entity.get
|
|
14
|
+
# zazu.accounts.list(limit: 50)
|
|
15
|
+
#
|
|
16
|
+
# All public state is set at construction time. The client is
|
|
17
|
+
# thread-safe in the sense that the underlying Faraday connection
|
|
18
|
+
# uses a connection pool via the HTTPX adapter — multiple threads
|
|
19
|
+
# can share one client.
|
|
20
|
+
class Client
|
|
21
|
+
DEFAULT_BASE_URL = "https://zazu.ma"
|
|
22
|
+
DEFAULT_TIMEOUT = 30
|
|
23
|
+
USER_AGENT = "zazu-ruby/#{VERSION}".freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :api_key, :base_url, :api_version, :timeout, :logger
|
|
26
|
+
|
|
27
|
+
def initialize(
|
|
28
|
+
api_key: ENV.fetch("ZAZU_API_KEY", nil),
|
|
29
|
+
base_url: ENV.fetch("ZAZU_BASE_URL", DEFAULT_BASE_URL),
|
|
30
|
+
api_version: ENV.fetch("ZAZU_API_VERSION", nil),
|
|
31
|
+
timeout: Integer(ENV.fetch("ZAZU_TIMEOUT", DEFAULT_TIMEOUT)),
|
|
32
|
+
logger: nil
|
|
33
|
+
)
|
|
34
|
+
raise ConfigurationError, "Missing api_key. Pass api_key: or set ZAZU_API_KEY." if api_key.to_s.empty?
|
|
35
|
+
|
|
36
|
+
@api_key = api_key
|
|
37
|
+
@base_url = base_url.to_s.chomp("/")
|
|
38
|
+
@api_version = api_version
|
|
39
|
+
@timeout = timeout
|
|
40
|
+
@logger = logger
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resource accessors — each returns a memoized resource module.
|
|
44
|
+
def accounts
|
|
45
|
+
@accounts ||= Resources::Accounts.new(self)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def customers
|
|
49
|
+
@customers ||= Resources::Customers.new(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def entity
|
|
53
|
+
@entity ||= Resources::Entity.new(self)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def invoices
|
|
57
|
+
@invoices ||= Resources::Invoices.new(self)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def payment_links
|
|
61
|
+
@payment_links ||= Resources::PaymentLinks.new(self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def webhook_endpoints
|
|
65
|
+
@webhook_endpoints ||= Resources::WebhookEndpoints.new(self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Performs an HTTP request and returns a {Zazu::Response} on
|
|
69
|
+
# success. Translates non-2xx responses into the matching
|
|
70
|
+
# {Zazu::Error} subclass.
|
|
71
|
+
def request(method, path, params: nil, body: nil, headers: {})
|
|
72
|
+
raw = connection.send(method) do |req|
|
|
73
|
+
req.url(path)
|
|
74
|
+
req.params.update(params) if params
|
|
75
|
+
req.body = body unless body.nil?
|
|
76
|
+
headers.each { |k, v| req.headers[k] = v }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
response = Response.new(raw)
|
|
80
|
+
return response if response.success?
|
|
81
|
+
|
|
82
|
+
raise build_error(response)
|
|
83
|
+
rescue Faraday::TimeoutError => e
|
|
84
|
+
raise ConnectionError, "Request timed out after #{timeout}s: #{e.message}"
|
|
85
|
+
rescue Faraday::ConnectionFailed => e
|
|
86
|
+
raise ConnectionError, "Connection failed: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def connection
|
|
92
|
+
@connection ||= Faraday.new(url: base_url) do |f|
|
|
93
|
+
f.headers["Authorization"] = "Bearer #{api_key}"
|
|
94
|
+
f.headers["User-Agent"] = USER_AGENT
|
|
95
|
+
f.headers["Accept"] = "application/json"
|
|
96
|
+
f.headers["Zazu-Version"] = api_version if api_version
|
|
97
|
+
f.request :json
|
|
98
|
+
f.response :json, content_type: /\bjson$/
|
|
99
|
+
f.options.timeout = timeout
|
|
100
|
+
f.options.open_timeout = [timeout, 10].min
|
|
101
|
+
f.response :logger, logger if logger
|
|
102
|
+
f.adapter(*adapter_args)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The HTTPX adapter ships its own WebMock plugin that wraps every
|
|
107
|
+
# connection. When VCR's WebMock library hook is also active and
|
|
108
|
+
# net-connect is allowed (recording mode), the two interceptors
|
|
109
|
+
# layer in a way that deadlocks on the first real request. For
|
|
110
|
+
# cassette recording we drop down to Net::HTTP, which has rock-
|
|
111
|
+
# solid WebMock + VCR integration. Cassettes are adapter-agnostic
|
|
112
|
+
# so replay continues to use the production HTTPX adapter.
|
|
113
|
+
def adapter_args
|
|
114
|
+
ENV["VCR_RECORD"] ? [:net_http] : [:httpx]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Lookup table for status → (error class, default message). 5xx
|
|
118
|
+
# is matched separately because Range keys don't work in Hash
|
|
119
|
+
# lookup the way exact integers do.
|
|
120
|
+
ERROR_BY_STATUS = {
|
|
121
|
+
401 => [AuthenticationError, "Authentication failed"],
|
|
122
|
+
403 => [ForbiddenError, "Forbidden"],
|
|
123
|
+
404 => [NotFoundError, "Not found"],
|
|
124
|
+
422 => [ValidationError, "Validation failed"]
|
|
125
|
+
}.freeze
|
|
126
|
+
private_constant :ERROR_BY_STATUS
|
|
127
|
+
|
|
128
|
+
def build_error(response)
|
|
129
|
+
payload = error_payload(response.body)
|
|
130
|
+
message = payload["message"]
|
|
131
|
+
kwargs = error_kwargs(response, payload)
|
|
132
|
+
|
|
133
|
+
if (mapping = ERROR_BY_STATUS[response.status])
|
|
134
|
+
klass, default_message = mapping
|
|
135
|
+
return klass.new(message || default_message, **kwargs)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
build_special_error(response, message, kwargs)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def error_payload(body)
|
|
142
|
+
return {} unless body.is_a?(Hash) && body["error"].is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
body["error"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def error_kwargs(response, payload)
|
|
148
|
+
{
|
|
149
|
+
status: response.status,
|
|
150
|
+
request_id: response.request_id,
|
|
151
|
+
type: payload["type"],
|
|
152
|
+
param: payload["param"],
|
|
153
|
+
body: response.body
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def build_special_error(response, message, kwargs)
|
|
158
|
+
case response.status
|
|
159
|
+
when 429
|
|
160
|
+
retry_after = response.headers["retry-after"]&.to_i
|
|
161
|
+
RateLimitError.new(message || "Rate limited", retry_after: retry_after, **kwargs)
|
|
162
|
+
when 500..599
|
|
163
|
+
ServerError.new(message || "Server error (#{response.status})", **kwargs)
|
|
164
|
+
else
|
|
165
|
+
Error.new(message || "Unexpected status #{response.status}", **kwargs)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/zazu/errors.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
# Base class for every Zazu SDK error.
|
|
5
|
+
#
|
|
6
|
+
# Carries the HTTP status, the API request id (header `X-Request-Id`),
|
|
7
|
+
# the parsed error type from the API (`error.type`), and the raw
|
|
8
|
+
# response body so callers can introspect anything the SDK didn't
|
|
9
|
+
# explicitly model.
|
|
10
|
+
class Error < StandardError
|
|
11
|
+
attr_reader :status, :request_id, :type, :param, :body
|
|
12
|
+
|
|
13
|
+
def initialize(message = nil, status: nil, request_id: nil, type: nil, param: nil, body: nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@status = status
|
|
16
|
+
@request_id = request_id
|
|
17
|
+
@type = type
|
|
18
|
+
@param = param
|
|
19
|
+
@body = body
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
error: self.class.name.split("::").last,
|
|
25
|
+
message:,
|
|
26
|
+
status:,
|
|
27
|
+
request_id:,
|
|
28
|
+
type:,
|
|
29
|
+
param:
|
|
30
|
+
}.compact
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 401 — bearer token missing, malformed, or revoked.
|
|
35
|
+
class AuthenticationError < Error; end
|
|
36
|
+
|
|
37
|
+
# 403 — token valid but lacks the required scope, OR the entity is
|
|
38
|
+
# not yet active, OR the API feature flag is off for this entity.
|
|
39
|
+
class ForbiddenError < Error; end
|
|
40
|
+
|
|
41
|
+
# 404 — the requested resource does not exist (or this entity cannot
|
|
42
|
+
# see it).
|
|
43
|
+
class NotFoundError < Error; end
|
|
44
|
+
|
|
45
|
+
# 422 — request body or query params failed validation. `#param`
|
|
46
|
+
# carries the offending field name when the API supplies it.
|
|
47
|
+
class ValidationError < Error; end
|
|
48
|
+
|
|
49
|
+
# 429 — rate limited. Retry after the `Retry-After` header (seconds).
|
|
50
|
+
class RateLimitError < Error
|
|
51
|
+
attr_reader :retry_after
|
|
52
|
+
|
|
53
|
+
def initialize(message = nil, retry_after: nil, **)
|
|
54
|
+
super(message, **)
|
|
55
|
+
@retry_after = retry_after
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 5xx — server error. Worth retrying once with backoff.
|
|
60
|
+
class ServerError < Error; end
|
|
61
|
+
|
|
62
|
+
# Network timeout, connection refused, DNS failure — anything that
|
|
63
|
+
# prevents the SDK from hearing back from the API.
|
|
64
|
+
class ConnectionError < Error; end
|
|
65
|
+
|
|
66
|
+
# The SDK was misconfigured (no API key, invalid base URL, etc.).
|
|
67
|
+
# Raised before any HTTP request is attempted.
|
|
68
|
+
class ConfigurationError < Error; end
|
|
69
|
+
|
|
70
|
+
# Caller passed a value the SDK refuses to send (e.g. `limit > 100`).
|
|
71
|
+
# Distinct from `ValidationError`, which represents server-side
|
|
72
|
+
# validation rejection.
|
|
73
|
+
class ArgumentError < Error; end
|
|
74
|
+
end
|
data/lib/zazu/page.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
# A single page of a list endpoint's response.
|
|
5
|
+
#
|
|
6
|
+
# The Zazu API uses cursor pagination. Every list endpoint returns
|
|
7
|
+
# `{data: [...], has_more: bool, next_cursor: string|null}`. This
|
|
8
|
+
# class wraps that shape and exposes a cursor-walking helper.
|
|
9
|
+
#
|
|
10
|
+
# Pages are intentionally not auto-paginating. The SDK refuses to
|
|
11
|
+
# let callers iterate every record across many pages with a single
|
|
12
|
+
# call — that's the failure mode that caused unbounded fetches in
|
|
13
|
+
# the CLI's `--all` flag. Callers walk pages explicitly:
|
|
14
|
+
#
|
|
15
|
+
# page = client.invoices.list(limit: 100)
|
|
16
|
+
# while page
|
|
17
|
+
# page.data.each { |inv| ... }
|
|
18
|
+
# page = page.next
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Or with a max-items cap that the caller can reason about:
|
|
22
|
+
#
|
|
23
|
+
# client.invoices.each_page(max_items: 500) { |inv| ... }
|
|
24
|
+
class Page
|
|
25
|
+
# Hard ceiling on per-page size. Server enforces this too; we
|
|
26
|
+
# refuse to send a larger value rather than silently get clamped.
|
|
27
|
+
MAX_PER_PAGE = 100
|
|
28
|
+
|
|
29
|
+
attr_reader :response, :data, :has_more, :next_cursor
|
|
30
|
+
|
|
31
|
+
def initialize(response, fetcher:)
|
|
32
|
+
@response = response
|
|
33
|
+
@fetcher = fetcher
|
|
34
|
+
|
|
35
|
+
body = response.body
|
|
36
|
+
unless body.is_a?(Hash) && body["data"].is_a?(Array)
|
|
37
|
+
raise Zazu::Error.new("List response missing 'data' array",
|
|
38
|
+
body:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@data = body["data"]
|
|
42
|
+
@has_more = body.fetch("has_more", false)
|
|
43
|
+
@next_cursor = body["next_cursor"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def request_id
|
|
47
|
+
response.request_id
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Fetches the next page. Returns nil when there are no more pages.
|
|
51
|
+
def next
|
|
52
|
+
return nil unless has_more && next_cursor
|
|
53
|
+
|
|
54
|
+
@fetcher.call(next_cursor)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def each(&)
|
|
58
|
+
data.each(&)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
include Enumerable
|
|
62
|
+
|
|
63
|
+
def inspect
|
|
64
|
+
"#<#{self.class.name} count=#{data.size} has_more=#{has_more} next_cursor=#{next_cursor.inspect}>"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Accounts and their transactions.
|
|
6
|
+
class Accounts < Base
|
|
7
|
+
# GET /api/accounts
|
|
8
|
+
#
|
|
9
|
+
# @param status [String, nil] filter by account status
|
|
10
|
+
# @param currency_code [String, nil] e.g. "MAD" or "ZAR"
|
|
11
|
+
# @param limit [Integer] page size (max 100)
|
|
12
|
+
# @param cursor [String, nil] pagination cursor
|
|
13
|
+
# @return [Zazu::Page]
|
|
14
|
+
def list(status: nil, currency_code: nil, limit: MAX_PER_PAGE, cursor: nil)
|
|
15
|
+
list_page(
|
|
16
|
+
"api/accounts",
|
|
17
|
+
status: status,
|
|
18
|
+
currency_code: currency_code,
|
|
19
|
+
limit: limit,
|
|
20
|
+
cursor: cursor
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /api/accounts/:id
|
|
25
|
+
def get(id)
|
|
26
|
+
http_get(encode_path("api/accounts", id))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# GET /api/accounts/:account_id/transactions
|
|
30
|
+
#
|
|
31
|
+
# @param operation [String, nil] filter by movement operation
|
|
32
|
+
# @param posted_after [String, Time, nil] ISO-8601 timestamp lower bound
|
|
33
|
+
# @param posted_before [String, Time, nil] ISO-8601 timestamp upper bound
|
|
34
|
+
def list_transactions(account_id, operation: nil, posted_after: nil, posted_before: nil, limit: MAX_PER_PAGE,
|
|
35
|
+
cursor: nil)
|
|
36
|
+
list_page(
|
|
37
|
+
encode_path("api/accounts", account_id, "transactions"),
|
|
38
|
+
operation: operation,
|
|
39
|
+
posted_after: serialize_time(posted_after),
|
|
40
|
+
posted_before: serialize_time(posted_before),
|
|
41
|
+
limit: limit,
|
|
42
|
+
cursor: cursor
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# GET /api/accounts/:account_id/transactions/:id
|
|
47
|
+
def get_transaction(account_id, transaction_id)
|
|
48
|
+
http_get(encode_path("api/accounts", account_id, "transactions", transaction_id))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def serialize_time(value)
|
|
54
|
+
return nil if value.nil?
|
|
55
|
+
return value if value.is_a?(String)
|
|
56
|
+
|
|
57
|
+
value.iso8601
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Shared scaffolding for every resource module. Carries a back-
|
|
6
|
+
# reference to the client and exposes thin HTTP helpers that
|
|
7
|
+
# delegate to {Zazu::Client#request}.
|
|
8
|
+
#
|
|
9
|
+
# Note on naming: the helpers are `http_get`, `http_post`, etc.
|
|
10
|
+
# rather than `get`/`post` so they do not shadow the public
|
|
11
|
+
# methods on resource subclasses. Public resources commonly
|
|
12
|
+
# define a `get(id)` method, and a same-named private helper on
|
|
13
|
+
# the base class would let `Base#list_page` accidentally dispatch
|
|
14
|
+
# to the subclass version when a list endpoint is hit.
|
|
15
|
+
#
|
|
16
|
+
# Pagination:
|
|
17
|
+
#
|
|
18
|
+
# Every resource that has a list endpoint exposes `#list` which
|
|
19
|
+
# returns a {Zazu::Page}. Callers can walk pages explicitly via
|
|
20
|
+
# `page.next` or use `each_page_record` for capped iteration.
|
|
21
|
+
class Base
|
|
22
|
+
MAX_PER_PAGE = Page::MAX_PER_PAGE
|
|
23
|
+
|
|
24
|
+
attr_reader :client
|
|
25
|
+
|
|
26
|
+
def initialize(client)
|
|
27
|
+
@client = client
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def http_get(path, params: nil)
|
|
33
|
+
client.request(:get, path, params: params)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def http_post(path, body: nil)
|
|
37
|
+
client.request(:post, path, body: body)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def http_patch(path, body: nil)
|
|
41
|
+
client.request(:patch, path, body: body)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def http_delete(path)
|
|
45
|
+
client.request(:delete, path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Builds a paginated list. `path` is the collection endpoint;
|
|
49
|
+
# `params` is everything else (filters, etc.). `limit` is enforced
|
|
50
|
+
# at MAX_PER_PAGE; the caller can pass `cursor:` to fetch a
|
|
51
|
+
# specific page.
|
|
52
|
+
def list_page(path, limit: MAX_PER_PAGE, cursor: nil, **params)
|
|
53
|
+
validated_limit = validate_limit!(limit)
|
|
54
|
+
|
|
55
|
+
fetcher = lambda { |next_cursor|
|
|
56
|
+
query = params.merge(limit: validated_limit, cursor: next_cursor).compact
|
|
57
|
+
response = http_get(path, params: query)
|
|
58
|
+
Page.new(response, fetcher: fetcher)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
initial_query = params.merge(limit: validated_limit, cursor: cursor).compact
|
|
62
|
+
response = http_get(path, params: initial_query)
|
|
63
|
+
Page.new(response, fetcher: fetcher)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Iterates list-endpoint records up to `max_items`, fetching
|
|
67
|
+
# additional pages on demand. Caps ensure callers never
|
|
68
|
+
# accidentally pull a full table.
|
|
69
|
+
#
|
|
70
|
+
# Pass either a block or get an Enumerator back.
|
|
71
|
+
def each_page_record(path, max_items:, **params, &block)
|
|
72
|
+
return enum_for(:each_page_record, path, max_items: max_items, **params) unless block
|
|
73
|
+
|
|
74
|
+
unless max_items.is_a?(Integer) && max_items.positive?
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"max_items must be a positive integer"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
seen = 0
|
|
80
|
+
page = list_page(path, **params)
|
|
81
|
+
|
|
82
|
+
loop do
|
|
83
|
+
page.data.each do |record|
|
|
84
|
+
return seen if seen >= max_items
|
|
85
|
+
|
|
86
|
+
yield record
|
|
87
|
+
seen += 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
break unless page.has_more && seen < max_items
|
|
91
|
+
|
|
92
|
+
page = page.next
|
|
93
|
+
break if page.nil?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
seen
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def validate_limit!(limit)
|
|
100
|
+
return MAX_PER_PAGE if limit.nil?
|
|
101
|
+
|
|
102
|
+
raise Zazu::ArgumentError, "limit must be a positive integer (got #{limit.inspect})" unless limit.is_a?(Integer) && limit.positive?
|
|
103
|
+
|
|
104
|
+
raise Zazu::ArgumentError, "limit cannot exceed #{MAX_PER_PAGE} (got #{limit})" if limit > MAX_PER_PAGE
|
|
105
|
+
|
|
106
|
+
limit
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Builds a request path by joining a literal base path with one
|
|
110
|
+
# or more dynamic segments. The base is appended verbatim; each
|
|
111
|
+
# dynamic segment is percent-encoded so an ID containing `/` or
|
|
112
|
+
# other special characters cannot escape the intended path.
|
|
113
|
+
#
|
|
114
|
+
# encode_path('api/accounts', 'acc_xyz')
|
|
115
|
+
# # => "api/accounts/acc_xyz"
|
|
116
|
+
#
|
|
117
|
+
# encode_path('api/accounts', 'acc 1', 'transactions', 'tx 1')
|
|
118
|
+
# # => "api/accounts/acc%201/transactions/tx%201"
|
|
119
|
+
def encode_path(base, *segments)
|
|
120
|
+
encoded_segments = segments.map do |s|
|
|
121
|
+
str = s.to_s
|
|
122
|
+
# An empty segment would silently turn `/things/:id` into
|
|
123
|
+
# `/things/`, which on most APIs redispatches to the list
|
|
124
|
+
# endpoint and returns a Page-shaped body. Surface it loudly.
|
|
125
|
+
raise Zazu::ArgumentError, "path segment cannot be blank" if str.empty?
|
|
126
|
+
|
|
127
|
+
# CGI.escape replaces ' ' with '+', which is wrong for path
|
|
128
|
+
# segments. Use a manual escape that targets only characters
|
|
129
|
+
# that would change path semantics.
|
|
130
|
+
str.gsub(/[^A-Za-z0-9._~-]/) { |c| format("%%%02X", c.ord) }
|
|
131
|
+
end
|
|
132
|
+
([base] + encoded_segments).join("/")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Customers — individuals or businesses the entity invoices.
|
|
6
|
+
class Customers < Base
|
|
7
|
+
# GET /api/customers
|
|
8
|
+
#
|
|
9
|
+
# @param q [String, nil] search query (matches company name, person name, email)
|
|
10
|
+
def list(q: nil, limit: MAX_PER_PAGE, cursor: nil)
|
|
11
|
+
list_page("api/customers", q: q, limit: limit, cursor: cursor)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /api/customers/:id
|
|
15
|
+
def get(id)
|
|
16
|
+
http_get(encode_path("api/customers", id))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# POST /api/customers
|
|
20
|
+
#
|
|
21
|
+
# @param attributes [Hash] customer attributes — see API docs.
|
|
22
|
+
# Common keys: customer_type ("individual"|"business"),
|
|
23
|
+
# person_name, company_name, email, phone, tax_id, ice_number,
|
|
24
|
+
# billing_address (Hash with street/city/postal_code/country/country_code).
|
|
25
|
+
def create(**attributes)
|
|
26
|
+
http_post("api/customers", body: attributes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# PATCH /api/customers/:id
|
|
30
|
+
def update(id, **attributes)
|
|
31
|
+
http_patch(encode_path("api/customers", id), body: attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# DELETE /api/customers/:id
|
|
35
|
+
def delete(id)
|
|
36
|
+
http_delete(encode_path("api/customers", id))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# The current entity (the tenant the API key belongs to).
|
|
6
|
+
#
|
|
7
|
+
# client.entity.get # => Zazu::Response
|
|
8
|
+
class Entity < Base
|
|
9
|
+
def get
|
|
10
|
+
http_get("api/entity")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Invoices and their lifecycle actions.
|
|
6
|
+
class Invoices < Base
|
|
7
|
+
# GET /api/invoices
|
|
8
|
+
def list(status: nil, customer_id: nil, limit: MAX_PER_PAGE, cursor: nil)
|
|
9
|
+
list_page(
|
|
10
|
+
"api/invoices",
|
|
11
|
+
status: status,
|
|
12
|
+
customer_id: customer_id,
|
|
13
|
+
limit: limit,
|
|
14
|
+
cursor: cursor
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GET /api/invoices/:id
|
|
19
|
+
def get(id)
|
|
20
|
+
http_get(encode_path("api/invoices", id))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# POST /api/invoices
|
|
24
|
+
def create(**attributes)
|
|
25
|
+
http_post("api/invoices", body: attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# PATCH /api/invoices/:id
|
|
29
|
+
def update(id, **attributes)
|
|
30
|
+
http_patch(encode_path("api/invoices", id), body: attributes)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# POST /api/invoices/:id/send
|
|
34
|
+
def send_invoice(id)
|
|
35
|
+
http_post(encode_path("api/invoices", id, "send"))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# POST /api/invoices/:id/mark_as_paid
|
|
39
|
+
def mark_as_paid(id)
|
|
40
|
+
http_post(encode_path("api/invoices", id, "mark_as_paid"))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# POST /api/invoices/:id/cancel
|
|
44
|
+
def cancel(id)
|
|
45
|
+
http_post(encode_path("api/invoices", id, "cancel"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# POST /api/invoices/:id/credit_note
|
|
49
|
+
def credit_note(id)
|
|
50
|
+
http_post(encode_path("api/invoices", id, "credit_note"))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# DELETE /api/invoices/:id
|
|
54
|
+
def delete(id)
|
|
55
|
+
http_delete(encode_path("api/invoices", id))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# POST /api/invoices/:invoice_id/payment_link
|
|
59
|
+
#
|
|
60
|
+
# @param account_id [String] the funding account for the link
|
|
61
|
+
def create_payment_link(invoice_id, account_id:)
|
|
62
|
+
http_post(
|
|
63
|
+
encode_path("api/invoices", invoice_id, "payment_link"),
|
|
64
|
+
body: { account_id: account_id }
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Standalone payment links (not attached to an invoice).
|
|
6
|
+
class PaymentLinks < Base
|
|
7
|
+
# GET /api/payment_links
|
|
8
|
+
def list(status: nil, link_type: nil, limit: MAX_PER_PAGE, cursor: nil)
|
|
9
|
+
list_page(
|
|
10
|
+
"api/payment_links",
|
|
11
|
+
status: status,
|
|
12
|
+
link_type: link_type,
|
|
13
|
+
limit: limit,
|
|
14
|
+
cursor: cursor
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GET /api/payment_links/:id
|
|
19
|
+
def get(id)
|
|
20
|
+
http_get(encode_path("api/payment_links", id))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# POST /api/payment_links
|
|
24
|
+
def create(**attributes)
|
|
25
|
+
http_post("api/payment_links", body: attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# POST /api/payment_links/:id/cancel
|
|
29
|
+
def cancel(id)
|
|
30
|
+
http_post(encode_path("api/payment_links", id, "cancel"))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Webhook endpoint configuration.
|
|
6
|
+
class WebhookEndpoints < Base
|
|
7
|
+
# GET /api/webhook_endpoints
|
|
8
|
+
def list(limit: MAX_PER_PAGE, cursor: nil)
|
|
9
|
+
list_page("api/webhook_endpoints", limit: limit, cursor: cursor)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# GET /api/webhook_endpoints/:id
|
|
13
|
+
def get(id)
|
|
14
|
+
http_get(encode_path("api/webhook_endpoints", id))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# POST /api/webhook_endpoints
|
|
18
|
+
#
|
|
19
|
+
# @param url [String] the URL to deliver events to
|
|
20
|
+
# @param events [Array<String>] event names to subscribe to
|
|
21
|
+
# @param description [String, nil]
|
|
22
|
+
def create(url:, events:, description: nil)
|
|
23
|
+
http_post(
|
|
24
|
+
"api/webhook_endpoints",
|
|
25
|
+
body: { url: url, events: events, description: description }.compact
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# PATCH /api/webhook_endpoints/:id
|
|
30
|
+
def update(id, **attributes)
|
|
31
|
+
http_patch(encode_path("api/webhook_endpoints", id), body: attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# DELETE /api/webhook_endpoints/:id
|
|
35
|
+
def delete(id)
|
|
36
|
+
http_delete(encode_path("api/webhook_endpoints", id))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# POST /api/webhook_endpoints/:id/test
|
|
40
|
+
def test_endpoint(id)
|
|
41
|
+
http_post(encode_path("api/webhook_endpoints", id, "test"))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# POST /api/webhook_endpoints/:id/regenerate_secret
|
|
45
|
+
def regenerate_secret(id)
|
|
46
|
+
http_post(encode_path("api/webhook_endpoints", id, "regenerate_secret"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# POST /api/webhook_endpoints/:id/enable
|
|
50
|
+
def enable(id)
|
|
51
|
+
http_post(encode_path("api/webhook_endpoints", id, "enable"))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# POST /api/webhook_endpoints/:id/disable
|
|
55
|
+
def disable(id)
|
|
56
|
+
http_post(encode_path("api/webhook_endpoints", id, "disable"))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
# Wraps a Faraday response with the few helpers callers actually
|
|
5
|
+
# want. Cheap value object — no parsing or normalization is done
|
|
6
|
+
# eagerly; `body`, `data`, `headers` all read straight through.
|
|
7
|
+
#
|
|
8
|
+
# The SDK's resource methods return one of these directly when the
|
|
9
|
+
# endpoint is a single-record fetch. List endpoints return a
|
|
10
|
+
# {Zazu::Page} instead, which composes a Response.
|
|
11
|
+
class Response
|
|
12
|
+
attr_reader :raw, :request_id
|
|
13
|
+
|
|
14
|
+
def initialize(raw, request_id: nil)
|
|
15
|
+
@raw = raw
|
|
16
|
+
@request_id = request_id || raw.headers["x-request-id"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def status
|
|
20
|
+
raw.status
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def headers
|
|
24
|
+
raw.headers
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def body
|
|
28
|
+
raw.body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def success?
|
|
32
|
+
status.between?(200, 299)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the response body without the `data` envelope when one
|
|
36
|
+
# is present. List endpoints wrap their items in `{"data": [...]}`
|
|
37
|
+
# — this returns the array. Single-record endpoints return the
|
|
38
|
+
# body as-is.
|
|
39
|
+
def data
|
|
40
|
+
return body unless body.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
body.key?("data") ? body["data"] : body
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The Zazu-Version header echoed by the server. Useful for
|
|
46
|
+
# debugging migration mismatches.
|
|
47
|
+
def api_version
|
|
48
|
+
headers["zazu-version"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
{
|
|
53
|
+
status:,
|
|
54
|
+
request_id:,
|
|
55
|
+
api_version:,
|
|
56
|
+
body:
|
|
57
|
+
}.compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def inspect
|
|
61
|
+
"#<#{self.class.name} status=#{status} request_id=#{request_id.inspect}>"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/zazu/version.rb
ADDED
data/lib/zazu.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby SDK for the Zazu API.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
#
|
|
7
|
+
# zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])
|
|
8
|
+
# zazu.entity.get
|
|
9
|
+
# zazu.accounts.list(limit: 50)
|
|
10
|
+
#
|
|
11
|
+
# See README.md for full documentation.
|
|
12
|
+
module Zazu
|
|
13
|
+
# Module-level shortcut. Equivalent to Zazu::Client.new(...).
|
|
14
|
+
def self.new(**)
|
|
15
|
+
Client.new(**)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative "zazu/version"
|
|
20
|
+
require_relative "zazu/errors"
|
|
21
|
+
require_relative "zazu/response"
|
|
22
|
+
require_relative "zazu/page"
|
|
23
|
+
require_relative "zazu/resources/base"
|
|
24
|
+
require_relative "zazu/resources/accounts"
|
|
25
|
+
require_relative "zazu/resources/customers"
|
|
26
|
+
require_relative "zazu/resources/entity"
|
|
27
|
+
require_relative "zazu/resources/invoices"
|
|
28
|
+
require_relative "zazu/resources/payment_links"
|
|
29
|
+
require_relative "zazu/resources/webhook_endpoints"
|
|
30
|
+
require_relative "zazu/client"
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zazu-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Zazu
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-retry
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: httpx
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.0'
|
|
54
|
+
description: Faraday-based Ruby SDK for the Zazu payment platform API. Wraps accounts,
|
|
55
|
+
customers, invoices, payment links, transactions, and webhook endpoints. HTTPX adapter
|
|
56
|
+
for HTTP/2 + persistent connections.
|
|
57
|
+
email:
|
|
58
|
+
- hello@get-zazu.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- LICENSE
|
|
65
|
+
- README.md
|
|
66
|
+
- lib/zazu.rb
|
|
67
|
+
- lib/zazu/client.rb
|
|
68
|
+
- lib/zazu/errors.rb
|
|
69
|
+
- lib/zazu/page.rb
|
|
70
|
+
- lib/zazu/resources/accounts.rb
|
|
71
|
+
- lib/zazu/resources/base.rb
|
|
72
|
+
- lib/zazu/resources/customers.rb
|
|
73
|
+
- lib/zazu/resources/entity.rb
|
|
74
|
+
- lib/zazu/resources/invoices.rb
|
|
75
|
+
- lib/zazu/resources/payment_links.rb
|
|
76
|
+
- lib/zazu/resources/webhook_endpoints.rb
|
|
77
|
+
- lib/zazu/response.rb
|
|
78
|
+
- lib/zazu/version.rb
|
|
79
|
+
homepage: https://github.com/getzazu/zazu-ruby
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata:
|
|
83
|
+
source_code_uri: https://github.com/getzazu/zazu-ruby/tree/main
|
|
84
|
+
changelog_uri: https://github.com/getzazu/zazu-ruby/blob/main/CHANGELOG.md
|
|
85
|
+
bug_tracker_uri: https://github.com/getzazu/zazu-ruby/issues
|
|
86
|
+
documentation_uri: https://github.com/getzazu/zazu-ruby#readme
|
|
87
|
+
rubygems_mfa_required: 'true'
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 3.3.0
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 4.0.6
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Ruby SDK for the Zazu API
|
|
105
|
+
test_files: []
|