zazu-ruby 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +82 -5
- data/lib/zazu/client.rb +11 -3
- data/lib/zazu/errors.rb +21 -2
- data/lib/zazu/resources/beneficiaries.rb +32 -4
- data/lib/zazu/resources/checkout_sessions.rb +5 -3
- data/lib/zazu/resources/customers.rb +4 -2
- data/lib/zazu/resources/payee_trust_requests.rb +23 -0
- data/lib/zazu/resources/payment_links.rb +3 -0
- data/lib/zazu/resources/transfer_drafts.rb +41 -7
- data/lib/zazu/resources/webhook_endpoints.rb +7 -0
- data/lib/zazu/transfer_authorization.rb +55 -0
- data/lib/zazu/version.rb +1 -1
- data/lib/zazu.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78fd31e6e0f84fd07c1e55e9af4b5828d232d39d1d040829a6d8518c33e176d2
|
|
4
|
+
data.tar.gz: 9760600e49b71b6bed3383677d7e108874740ca665c2e601cc6b7a87c1c8210b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 283a136fd5ca23c3db9d38f5007758bbc021eeeff6336cc117eb8342585d8b29ac09d8349a4db823c9f9743bdf1842c564ef4c2cffbe59aef005402dd64c1971
|
|
7
|
+
data.tar.gz: 5542a59251e3f5608c03ef92c4eadc2147ade849576662af02e67bd98b4c3e78673474ef0f82d929ec12db7e6ea6079b4e186254fe1f41298a17de7b2bad9b05
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Zazu::ConflictError` (409), the 10th error class. A duplicate
|
|
13
|
+
`client_reference` on a transfer draft raises it with `#payment_id`
|
|
14
|
+
naming the existing draft. 400 now maps to `Zazu::ValidationError`
|
|
15
|
+
(lists return 400 for a malformed `limit`/`cursor`).
|
|
16
|
+
- `TransferDrafts#authorize(id, authorization_id:, signature:)` and
|
|
17
|
+
`#decline(id, authorization_id:, reason: nil)` for machine-authorized
|
|
18
|
+
transfers. A blank signature raises `Zazu::ArgumentError` locally —
|
|
19
|
+
the API would count it as a failed attempt.
|
|
20
|
+
- `TransferDrafts#create` documents the new optional `client_reference`;
|
|
21
|
+
responses carry `client_reference` and `authorization`.
|
|
22
|
+
- `Zazu::TransferAuthorization` — `signature_input`, `sign` and
|
|
23
|
+
`payee_for`, the HMAC-SHA256 signer for authorization challenges, with
|
|
24
|
+
a fixed test vector shared across SDKs
|
|
25
|
+
(`spec/zazu/transfer_authorization_spec.rb`).
|
|
26
|
+
- `Beneficiaries#create`, `#list_external_accounts`,
|
|
27
|
+
`#get_external_account` and `#create_external_account`.
|
|
28
|
+
- `Zazu::Resources::PayeeTrustRequests` (`zazu.payee_trust_requests`) —
|
|
29
|
+
`create(external_account_ids:)` and `get(id)`.
|
|
30
|
+
- Docs for new pass-through fields: checkout session `customer_name`,
|
|
31
|
+
`collect_billing_address`, `billing_address`, `settled_at`,
|
|
32
|
+
`transaction` and the `clearing` status; payment link billing fields;
|
|
33
|
+
customer `registration_number` / `vat_number` (and MA-only `tax_id` /
|
|
34
|
+
`ice_number`).
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Default base URL is now `https://ma.manza.finance` (Morocco production;
|
|
39
|
+
South Africa is `https://za.manza.finance`). Cassettes are recorded
|
|
40
|
+
against staging at `https://ma.manza.dev`. The old `zazu.ma` hosts are
|
|
41
|
+
still served.
|
|
42
|
+
- Cassettes scrub `Manza-Version`, `account_number`, `bank_identifier`
|
|
43
|
+
and the authorize request `signature`. The three authorize cassettes
|
|
44
|
+
match the request body minus `signature` (`body_without_signature`),
|
|
45
|
+
which replay cannot reproduce; other SDKs should do the same.
|
|
46
|
+
- `rake fixtures:seed` needs a second, authorizer API key, the
|
|
47
|
+
authorizer endpoint's signing secret and a tunnel to a local webhook
|
|
48
|
+
receiver (see the one-time setup in `lib/tasks/fixtures.rake`).
|
|
49
|
+
Re-recording now executes a real 10.00 MAD transfer (the API minimum).
|
|
50
|
+
- Every cassette is re-recorded against `ma.manza.dev`, including the
|
|
51
|
+
full machine-authorization path (authorize 200, decline 200, bad
|
|
52
|
+
signature 422, same key 403, duplicate client_reference 409).
|
|
53
|
+
|
|
10
54
|
## [0.2.1]
|
|
11
55
|
|
|
12
56
|
### Added
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Zazu Ruby SDK
|
|
2
2
|
|
|
3
|
-
Ruby SDK for the [
|
|
3
|
+
Ruby SDK for the [Manza API](https://ma.manza.finance). Faraday + HTTPX adapter for HTTP/2 + persistent connections.
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
6
|
gem "zazu-ruby"
|
|
@@ -14,8 +14,8 @@ The gem is published as `zazu-ruby` on RubyGems but loaded as `zazu` in code (th
|
|
|
14
14
|
require "zazu"
|
|
15
15
|
|
|
16
16
|
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])
|
|
17
|
-
# Or with explicit base URL (defaults to https://
|
|
18
|
-
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"], base_url: "https://
|
|
17
|
+
# Or with explicit base URL (defaults to https://ma.manza.finance, Morocco):
|
|
18
|
+
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"], base_url: "https://za.manza.finance")
|
|
19
19
|
|
|
20
20
|
entity = zazu.entity.get
|
|
21
21
|
# => #<Zazu::Response status=200 ...>
|
|
@@ -79,6 +79,24 @@ zazu.checkout_sessions.create(
|
|
|
79
79
|
)
|
|
80
80
|
zazu.checkout_sessions.get("cs_...")
|
|
81
81
|
|
|
82
|
+
zazu.beneficiaries.list
|
|
83
|
+
zazu.beneficiaries.create(beneficiary_type: "business", company_name: "Acme Supplies", email: "ap@acme.com")
|
|
84
|
+
zazu.beneficiaries.list_external_accounts("01a0...")
|
|
85
|
+
zazu.beneficiaries.get_external_account("01a0...", "01a1...")
|
|
86
|
+
zazu.beneficiaries.create_external_account("01a0...", account_number: "007780...", name: "Main account")
|
|
87
|
+
|
|
88
|
+
zazu.payee_trust_requests.create(external_account_ids: ["01a1..."])
|
|
89
|
+
zazu.payee_trust_requests.get("01a2...")
|
|
90
|
+
|
|
91
|
+
zazu.transfer_drafts.create(
|
|
92
|
+
account_id: "019dde7d-...",
|
|
93
|
+
beneficiary_id: "01a0...",
|
|
94
|
+
amount: "2500.00",
|
|
95
|
+
client_reference: "po_1042" # unique per entity; a duplicate raises Zazu::ConflictError
|
|
96
|
+
)
|
|
97
|
+
zazu.transfer_drafts.get("01a3...")
|
|
98
|
+
zazu.transfer_drafts.decline("01a3...", authorization_id: "01a4...", reason: "Not ours")
|
|
99
|
+
|
|
82
100
|
zazu.webhook_endpoints.list
|
|
83
101
|
zazu.webhook_endpoints.create(
|
|
84
102
|
url: "https://example.com/webhooks/zazu",
|
|
@@ -90,6 +108,28 @@ zazu.webhook_endpoints.enable("01a0...")
|
|
|
90
108
|
zazu.webhook_endpoints.disable("01a0...")
|
|
91
109
|
```
|
|
92
110
|
|
|
111
|
+
## Machine-authorized transfers
|
|
112
|
+
|
|
113
|
+
A draft inside your entity's authorization envelope (trusted payee, within limits) is sent to your enrolled authorizer endpoint as a `payment.authorization_requested` webhook carrying an `authorization.id` and a one-time `nonce`. Sign the draft from **your own record** of it with the endpoint's signing secret, and authorize it with a **different API key** from the one that created it (the creating key gets 403 `same_key_forbidden`):
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
input = Zazu::TransferAuthorization.signature_input(
|
|
117
|
+
payment_id: draft["id"],
|
|
118
|
+
nonce: webhook["data"]["authorization"]["nonce"],
|
|
119
|
+
amount: draft["amount"], # the API's decimal string, e.g. "2500.0"
|
|
120
|
+
currency_code: draft["currency_code"],
|
|
121
|
+
account_id: draft["account_id"],
|
|
122
|
+
payee: Zazu::TransferAuthorization.payee_for(external_account_id: draft["external_account_id"]),
|
|
123
|
+
client_reference: draft["client_reference"]
|
|
124
|
+
)
|
|
125
|
+
signature = Zazu::TransferAuthorization.sign(secret: signing_secret, signature_input: input)
|
|
126
|
+
|
|
127
|
+
authorizer = Zazu.new(api_key: ENV["ZAZU_AUTHORIZER_API_KEY"])
|
|
128
|
+
authorizer.transfer_drafts.authorize(draft["id"], authorization_id: webhook["data"]["authorization"]["id"], signature: signature)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A wrong signature raises `Zazu::ValidationError` (`type` `invalid_signature`). Five on one challenge send the draft to your in-app approvers; five in a row suspend the authorizer.
|
|
132
|
+
|
|
93
133
|
## Pagination
|
|
94
134
|
|
|
95
135
|
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.
|
|
@@ -117,7 +157,9 @@ Every non-2xx response raises a subclass of `Zazu::Error`:
|
|
|
117
157
|
|---|---|
|
|
118
158
|
| 401 | `Zazu::AuthenticationError` |
|
|
119
159
|
| 403 | `Zazu::ForbiddenError` |
|
|
160
|
+
| 400 | `Zazu::ValidationError` (malformed request, e.g. bad `limit`/`cursor`) |
|
|
120
161
|
| 404 | `Zazu::NotFoundError` |
|
|
162
|
+
| 409 | `Zazu::ConflictError` (carries `#payment_id` for a duplicate `client_reference`) |
|
|
121
163
|
| 422 | `Zazu::ValidationError` |
|
|
122
164
|
| 429 | `Zazu::RateLimitError` (carries `#retry_after`) |
|
|
123
165
|
| 5xx | `Zazu::ServerError` |
|
|
@@ -141,7 +183,7 @@ end
|
|
|
141
183
|
zazu = Zazu.new(api_key: "...", api_version: "2026-03-27")
|
|
142
184
|
```
|
|
143
185
|
|
|
144
|
-
Or via env: `ZAZU_API_VERSION=2026-03-27`. The header is sent on every request; the API echoes it back in `Zazu-Version`.
|
|
186
|
+
Or via env: `ZAZU_API_VERSION=2026-03-27`. The header is sent on every request; the API echoes it back in both `Zazu-Version` and `Manza-Version`.
|
|
145
187
|
|
|
146
188
|
## Development
|
|
147
189
|
|
|
@@ -157,10 +199,45 @@ To re-record cassettes against staging:
|
|
|
157
199
|
|
|
158
200
|
```bash
|
|
159
201
|
cp .env.example .env
|
|
160
|
-
# fill in
|
|
202
|
+
# fill in the keys, ZAZU_FIXTURE_ACCOUNT_ID and ZAZU_FIXTURE_BENEFICIARY_ID
|
|
203
|
+
cloudflared tunnel --config ~/.cloudflared/zazu-sdk-authorizer.yml run zazu-sdk-authorizer # separate terminal
|
|
161
204
|
bundle exec rake fixtures:record
|
|
162
205
|
```
|
|
163
206
|
|
|
207
|
+
Recording executes a real 10.00 MAD transfer on staging (the authorize cassette). The one-time staging setup (keys, authorizer enrolment, trusted payee) is listed at the top of `lib/tasks/fixtures.rake`.
|
|
208
|
+
|
|
209
|
+
### The authorizer tunnel
|
|
210
|
+
|
|
211
|
+
The machine-authorization cassettes need the `payment.authorization_requested` webhook, which staging sends to the webhook endpoint enrolled as transfer authorizer. During `rake fixtures:record` the seeder listens for it on `127.0.0.1:${ZAZU_STAGING_AUTHORIZER_PORT:-4599}`, so a tunnel must forward the endpoint's public URL to that port. The endpoint URL cannot change once enrolled, so the tunnel needs a **stable hostname** (a throwaway `trycloudflare.com` URL won't do).
|
|
212
|
+
|
|
213
|
+
The existing setup uses a named Cloudflare tunnel `zazu-sdk-authorizer` → `https://sdk-authorizer.manza.dev/`. To run it on a new machine:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
brew install cloudflared
|
|
217
|
+
cloudflared tunnel login # pick the manza.dev zone
|
|
218
|
+
cloudflared tunnel token --cred-file ~/.cloudflared/zazu-sdk-authorizer.json zazu-sdk-authorizer
|
|
219
|
+
cat > ~/.cloudflared/zazu-sdk-authorizer.yml <<YML
|
|
220
|
+
tunnel: zazu-sdk-authorizer
|
|
221
|
+
credentials-file: $HOME/.cloudflared/zazu-sdk-authorizer.json
|
|
222
|
+
ingress:
|
|
223
|
+
- hostname: sdk-authorizer.manza.dev
|
|
224
|
+
service: http://127.0.0.1:4599
|
|
225
|
+
- service: http_status:404
|
|
226
|
+
YML
|
|
227
|
+
cloudflared tunnel --config ~/.cloudflared/zazu-sdk-authorizer.yml run zazu-sdk-authorizer
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
To create one from scratch instead (then point a new webhook endpoint at it and enrol that one as authorizer):
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
cloudflared tunnel create zazu-sdk-authorizer
|
|
234
|
+
cloudflared tunnel route dns zazu-sdk-authorizer sdk-authorizer.manza.dev
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The tunnel's `service` port must match `ZAZU_STAGING_AUTHORIZER_PORT`: if you change one, change the other, or deliveries never reach the seeder and `fixtures:record` times out waiting for them.
|
|
238
|
+
|
|
239
|
+
Check it end to end: with the tunnel running and nothing on port 4599, `curl -X POST https://sdk-authorizer.manza.dev/` returns 502. During a record run the seeder answers unsigned requests with 401.
|
|
240
|
+
|
|
164
241
|
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.
|
|
165
242
|
|
|
166
243
|
## Cassettes for other-language SDKs
|
data/lib/zazu/client.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Zazu
|
|
|
18
18
|
# uses a connection pool via the HTTPX adapter — multiple threads
|
|
19
19
|
# can share one client.
|
|
20
20
|
class Client
|
|
21
|
-
|
|
21
|
+
# Morocco production. South Africa: https://za.manza.finance.
|
|
22
|
+
DEFAULT_BASE_URL = "https://ma.manza.finance"
|
|
22
23
|
DEFAULT_TIMEOUT = 30
|
|
23
24
|
USER_AGENT = "zazu-ruby/#{VERSION}".freeze
|
|
24
25
|
|
|
@@ -69,6 +70,10 @@ module Zazu
|
|
|
69
70
|
@payment_links ||= Resources::PaymentLinks.new(self)
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
def payee_trust_requests
|
|
74
|
+
@payee_trust_requests ||= Resources::PayeeTrustRequests.new(self)
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
def transfer_drafts
|
|
73
78
|
@transfer_drafts ||= Resources::TransferDrafts.new(self)
|
|
74
79
|
end
|
|
@@ -130,6 +135,7 @@ module Zazu
|
|
|
130
135
|
# is matched separately because Range keys don't work in Hash
|
|
131
136
|
# lookup the way exact integers do.
|
|
132
137
|
ERROR_BY_STATUS = {
|
|
138
|
+
400 => [ValidationError, "Bad request"],
|
|
133
139
|
401 => [AuthenticationError, "Authentication failed"],
|
|
134
140
|
403 => [ForbiddenError, "Forbidden"],
|
|
135
141
|
404 => [NotFoundError, "Not found"],
|
|
@@ -147,7 +153,7 @@ module Zazu
|
|
|
147
153
|
return klass.new(message || default_message, **kwargs)
|
|
148
154
|
end
|
|
149
155
|
|
|
150
|
-
build_special_error(response, message, kwargs)
|
|
156
|
+
build_special_error(response, payload, message, kwargs)
|
|
151
157
|
end
|
|
152
158
|
|
|
153
159
|
def error_payload(body)
|
|
@@ -166,8 +172,10 @@ module Zazu
|
|
|
166
172
|
}
|
|
167
173
|
end
|
|
168
174
|
|
|
169
|
-
def build_special_error(response, message, kwargs)
|
|
175
|
+
def build_special_error(response, payload, message, kwargs)
|
|
170
176
|
case response.status
|
|
177
|
+
when 409
|
|
178
|
+
ConflictError.new(message || "Conflict", payment_id: payload["payment_id"], **kwargs)
|
|
171
179
|
when 429
|
|
172
180
|
retry_after = response.headers["retry-after"]&.to_i
|
|
173
181
|
RateLimitError.new(message || "Rate limited", retry_after: retry_after, **kwargs)
|
data/lib/zazu/errors.rb
CHANGED
|
@@ -42,10 +42,29 @@ module Zazu
|
|
|
42
42
|
# see it).
|
|
43
43
|
class NotFoundError < Error; end
|
|
44
44
|
|
|
45
|
-
# 422 — request body or query params failed validation
|
|
46
|
-
#
|
|
45
|
+
# 400 / 422 — request body or query params failed validation (400 for
|
|
46
|
+
# a malformed request such as a bad `limit`/`cursor`, 422 for a
|
|
47
|
+
# well-formed one the API rejects). `#param` carries the offending
|
|
48
|
+
# field name when the API supplies it.
|
|
47
49
|
class ValidationError < Error; end
|
|
48
50
|
|
|
51
|
+
# 409 — the request conflicts with an existing resource. For a
|
|
52
|
+
# duplicate `client_reference` on a transfer draft (`type`
|
|
53
|
+
# "duplicate_client_reference"), `#payment_id` names the draft that
|
|
54
|
+
# already holds it.
|
|
55
|
+
class ConflictError < Error
|
|
56
|
+
attr_reader :payment_id
|
|
57
|
+
|
|
58
|
+
def initialize(message = nil, payment_id: nil, **)
|
|
59
|
+
super(message, **)
|
|
60
|
+
@payment_id = payment_id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_h
|
|
64
|
+
super.merge(payment_id:).compact
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
49
68
|
# 429 — rate limited. Retry after the `Retry-After` header (seconds).
|
|
50
69
|
class RateLimitError < Error
|
|
51
70
|
attr_reader :retry_after
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Zazu
|
|
4
4
|
module Resources
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# is
|
|
8
|
-
# Beneficiaries are created and managed in the Zazu dashboard.
|
|
5
|
+
# Saved transfer recipients. Each beneficiary embeds its bank
|
|
6
|
+
# accounts; the one flagged `default` is used when a transfer names
|
|
7
|
+
# only the beneficiary_id. There is no update or delete via the API.
|
|
9
8
|
class Beneficiaries < Base
|
|
10
9
|
# GET /api/beneficiaries
|
|
11
10
|
def list(limit: MAX_PER_PAGE, cursor: nil)
|
|
@@ -16,6 +15,35 @@ module Zazu
|
|
|
16
15
|
def get(id)
|
|
17
16
|
http_get(encode_path("api/beneficiaries", id))
|
|
18
17
|
end
|
|
18
|
+
|
|
19
|
+
# POST /api/beneficiaries
|
|
20
|
+
#
|
|
21
|
+
# Keys: beneficiary_type ("individual" | "business"; inferred from
|
|
22
|
+
# person_name / company_name when omitted), person_name, company_name, email,
|
|
23
|
+
# phone_number. Values must be strings. Shares a 10/minute limit
|
|
24
|
+
# with {#create_external_account}.
|
|
25
|
+
def create(**attributes)
|
|
26
|
+
http_post("api/beneficiaries", body: attributes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# GET /api/beneficiaries/:beneficiary_id/external_accounts
|
|
30
|
+
def list_external_accounts(beneficiary_id, limit: MAX_PER_PAGE, cursor: nil)
|
|
31
|
+
list_page(encode_path("api/beneficiaries", beneficiary_id, "external_accounts"), limit: limit, cursor: cursor)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# GET /api/beneficiaries/:beneficiary_id/external_accounts/:id
|
|
35
|
+
def get_external_account(beneficiary_id, id)
|
|
36
|
+
http_get(encode_path("api/beneficiaries", beneficiary_id, "external_accounts", id))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# POST /api/beneficiaries/:beneficiary_id/external_accounts
|
|
40
|
+
#
|
|
41
|
+
# Required: account_number. Optional: name, country_code,
|
|
42
|
+
# currency_code, account_type ("bank" only), bank_identifier
|
|
43
|
+
# (required in ZA, rejected in MA, where it is derived from the RIB).
|
|
44
|
+
def create_external_account(beneficiary_id, **attributes)
|
|
45
|
+
http_post(encode_path("api/beneficiaries", beneficiary_id, "external_accounts"), body: attributes)
|
|
46
|
+
end
|
|
19
47
|
end
|
|
20
48
|
end
|
|
21
49
|
end
|
|
@@ -4,8 +4,9 @@ module Zazu
|
|
|
4
4
|
module Resources
|
|
5
5
|
# One-off hosted checkout sessions. Pre-API there's no list,
|
|
6
6
|
# update, or delete — sessions are created and inspected by id.
|
|
7
|
-
# State (`open`, `processing`, `complete`, `expired`)
|
|
8
|
-
# are read-only from the SDK's perspective.
|
|
7
|
+
# State (`open`, `processing`, `clearing`, `complete`, `expired`)
|
|
8
|
+
# transitions are read-only from the SDK's perspective. Responses
|
|
9
|
+
# carry `settled_at` and the paying `transaction`.
|
|
9
10
|
class CheckoutSessions < Base
|
|
10
11
|
# GET /api/checkout_sessions/:id
|
|
11
12
|
def get(id)
|
|
@@ -16,7 +17,8 @@ module Zazu
|
|
|
16
17
|
#
|
|
17
18
|
# @param attributes [Hash] checkout-session attributes — see API docs.
|
|
18
19
|
# Required: account_id, amount, success_url.
|
|
19
|
-
# Optional: metadata, customer_email,
|
|
20
|
+
# Optional: metadata, customer_email, customer_name, cancel_url,
|
|
21
|
+
# description, expires_at, collect_billing_address, billing_address.
|
|
20
22
|
def create(**attributes)
|
|
21
23
|
http_post("api/checkout_sessions", body: attributes)
|
|
22
24
|
end
|
|
@@ -20,8 +20,10 @@ module Zazu
|
|
|
20
20
|
#
|
|
21
21
|
# @param attributes [Hash] customer attributes — see API docs.
|
|
22
22
|
# Common keys: customer_type ("individual"|"business"),
|
|
23
|
-
# person_name, company_name, email, phone,
|
|
24
|
-
# billing_address (Hash with street/city/postal_code/
|
|
23
|
+
# person_name, company_name, email, phone, registration_number,
|
|
24
|
+
# vat_number, billing_address (Hash with street/city/postal_code/
|
|
25
|
+
# country/country_code). Morocco only: tax_id, ice_number — these
|
|
26
|
+
# keys are absent from responses in other markets.
|
|
25
27
|
def create(**attributes)
|
|
26
28
|
http_post("api/customers", body: attributes)
|
|
27
29
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zazu
|
|
4
|
+
module Resources
|
|
5
|
+
# Requests to trust payees for machine-authorized transfers. The API
|
|
6
|
+
# key can only ask: a member holding payment-authorize permission
|
|
7
|
+
# approves the request in the Zazu app. Status: pending → approved /
|
|
8
|
+
# declined / cancelled. There is no list, update, or delete.
|
|
9
|
+
class PayeeTrustRequests < Base
|
|
10
|
+
# POST /api/payee_trust_requests
|
|
11
|
+
#
|
|
12
|
+
# @param external_account_ids [Array<String>] at most 100 bank accounts.
|
|
13
|
+
def create(external_account_ids:)
|
|
14
|
+
http_post("api/payee_trust_requests", body: { external_account_ids: external_account_ids })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# GET /api/payee_trust_requests/:id
|
|
18
|
+
def get(id)
|
|
19
|
+
http_get(encode_path("api/payee_trust_requests", id))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -21,6 +21,9 @@ module Zazu
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# POST /api/payment_links
|
|
24
|
+
#
|
|
25
|
+
# Optional billing keys: collect_billing_address, billing_address.
|
|
26
|
+
# Responses carry `settled_at`; status includes `clearing`.
|
|
24
27
|
def create(**attributes)
|
|
25
28
|
http_post("api/payment_links", body: attributes)
|
|
26
29
|
end
|
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Zazu
|
|
4
4
|
module Resources
|
|
5
|
-
# API-initiated transfers. Creating a draft
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# the
|
|
9
|
-
#
|
|
10
|
-
#
|
|
5
|
+
# API-initiated transfers. Creating a draft never executes a
|
|
6
|
+
# transfer by itself. A draft inside the entity's machine-
|
|
7
|
+
# authorization envelope (trusted payee, within limits) is sent to
|
|
8
|
+
# the enrolled transfer authorizer as a `payment.authorization_requested`
|
|
9
|
+
# webhook; answer it with {#authorize} or {#decline}, using an API
|
|
10
|
+
# key other than the one that created the draft. Every other draft
|
|
11
|
+
# goes to the in-app approval flow, where a manager or legal
|
|
12
|
+
# representative approves it. Poll {#get} (status: requested →
|
|
13
|
+
# processing → completed / failed) or subscribe to the
|
|
14
|
+
# `transfer.executed` webhook to follow execution.
|
|
11
15
|
class TransferDrafts < Base
|
|
12
16
|
# POST /api/transfer_drafts
|
|
13
17
|
#
|
|
14
18
|
# Required: account_id, amount, and exactly one of beneficiary_id
|
|
15
19
|
# (external transfer) or destination_account_id (own-account move).
|
|
16
20
|
# Optional: external_account_id, currency_code, payment_reference,
|
|
17
|
-
# internal_notes
|
|
21
|
+
# internal_notes, client_reference (unique per entity, at most 128
|
|
22
|
+
# characters; a duplicate raises {Zazu::ConflictError} whose
|
|
23
|
+
# `payment_id` names the existing draft).
|
|
18
24
|
def create(**attributes)
|
|
19
25
|
http_post("api/transfer_drafts", body: attributes)
|
|
20
26
|
end
|
|
@@ -23,6 +29,34 @@ module Zazu
|
|
|
23
29
|
def get(id)
|
|
24
30
|
http_get(encode_path("api/transfer_drafts", id))
|
|
25
31
|
end
|
|
32
|
+
|
|
33
|
+
# POST /api/transfer_drafts/:id/authorize
|
|
34
|
+
#
|
|
35
|
+
# Executes the draft. `authorization_id` comes from the
|
|
36
|
+
# `payment.authorization_requested` webhook; build `signature` with
|
|
37
|
+
# {Zazu::TransferAuthorization}. Requires the `transfers:authorize`
|
|
38
|
+
# scope on a key other than the draft's creator (otherwise 403
|
|
39
|
+
# `same_key_forbidden`). A blank signature is refused locally: the
|
|
40
|
+
# API counts it as a failed attempt, and five fail the challenge.
|
|
41
|
+
def authorize(id, authorization_id:, signature:)
|
|
42
|
+
raise Zazu::ArgumentError, "signature cannot be blank" if signature.to_s.strip.empty?
|
|
43
|
+
|
|
44
|
+
http_post(
|
|
45
|
+
encode_path("api/transfer_drafts", id, "authorize"),
|
|
46
|
+
body: { authorization_id: authorization_id, signature: signature }
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# POST /api/transfer_drafts/:id/decline
|
|
51
|
+
#
|
|
52
|
+
# Declines the challenge and deletes the draft. Returns the
|
|
53
|
+
# authorization (`status: "declined"`).
|
|
54
|
+
def decline(id, authorization_id:, reason: nil)
|
|
55
|
+
http_post(
|
|
56
|
+
encode_path("api/transfer_drafts", id, "decline"),
|
|
57
|
+
body: { authorization_id: authorization_id, reason: reason }.compact
|
|
58
|
+
)
|
|
59
|
+
end
|
|
26
60
|
end
|
|
27
61
|
end
|
|
28
62
|
end
|
|
@@ -27,11 +27,16 @@ module Zazu
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# PATCH /api/webhook_endpoints/:id
|
|
30
|
+
#
|
|
31
|
+
# Changing `url` on the endpoint enrolled as transfer authorizer
|
|
32
|
+
# raises {Zazu::ValidationError}.
|
|
30
33
|
def update(id, **attributes)
|
|
31
34
|
http_patch(encode_path("api/webhook_endpoints", id), body: attributes)
|
|
32
35
|
end
|
|
33
36
|
|
|
34
37
|
# DELETE /api/webhook_endpoints/:id
|
|
38
|
+
#
|
|
39
|
+
# A soft delete (still 204).
|
|
35
40
|
def delete(id)
|
|
36
41
|
http_delete(encode_path("api/webhook_endpoints", id))
|
|
37
42
|
end
|
|
@@ -42,6 +47,8 @@ module Zazu
|
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
# POST /api/webhook_endpoints/:id/regenerate_secret
|
|
50
|
+
#
|
|
51
|
+
# Raises {Zazu::ValidationError} on the enrolled transfer authorizer.
|
|
45
52
|
def regenerate_secret(id)
|
|
46
53
|
http_post(encode_path("api/webhook_endpoints", id, "regenerate_secret"))
|
|
47
54
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Zazu
|
|
6
|
+
# Signs a machine-authorization challenge for an API-created transfer
|
|
7
|
+
# draft. Pure functions — no HTTP.
|
|
8
|
+
#
|
|
9
|
+
# The `payment.authorization_requested` webhook delivers the
|
|
10
|
+
# authorization id and a one-time nonce. Build the signature input
|
|
11
|
+
# from your *own* record of the transfer (not the webhook's
|
|
12
|
+
# `signature_input`, which is there only to compare against), sign
|
|
13
|
+
# it with the authorizer endpoint's signing secret, and pass the
|
|
14
|
+
# result to {Resources::TransferDrafts#authorize}:
|
|
15
|
+
#
|
|
16
|
+
# input = Zazu::TransferAuthorization.signature_input(
|
|
17
|
+
# payment_id: draft["id"], nonce: nonce, amount: draft["amount"],
|
|
18
|
+
# currency_code: draft["currency_code"], account_id: draft["account_id"],
|
|
19
|
+
# payee: Zazu::TransferAuthorization.payee_for(external_account_id: draft["external_account_id"]),
|
|
20
|
+
# client_reference: draft["client_reference"]
|
|
21
|
+
# )
|
|
22
|
+
# signature = Zazu::TransferAuthorization.sign(secret: signing_secret, signature_input: input)
|
|
23
|
+
# zazu.transfer_drafts.authorize(draft["id"], authorization_id: authorization_id, signature: signature)
|
|
24
|
+
module TransferAuthorization
|
|
25
|
+
SIGNATURE_VERSION = "manza.transfer-authorization.v1"
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# `amount` must be the API's decimal string verbatim (e.g. "2500.0").
|
|
30
|
+
# `client_reference` is empty when the transfer has none.
|
|
31
|
+
def signature_input(payment_id:, nonce:, amount:, currency_code:, account_id:, payee:, client_reference: nil)
|
|
32
|
+
raise Zazu::ArgumentError, "amount must be the API's decimal string (got #{amount.inspect})" unless amount.is_a?(String)
|
|
33
|
+
|
|
34
|
+
[
|
|
35
|
+
SIGNATURE_VERSION, payment_id, nonce, amount, currency_code, account_id, payee, client_reference.to_s
|
|
36
|
+
].join("|")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Hex HMAC-SHA256 of the signature input under the authorizer
|
|
40
|
+
# endpoint's signing secret.
|
|
41
|
+
def sign(secret:, signature_input:)
|
|
42
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret, signature_input)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The payee token: `ext:<id>` for a beneficiary's bank account,
|
|
46
|
+
# `own:<id>` for one of the entity's own accounts. Pass exactly one.
|
|
47
|
+
def payee_for(external_account_id: nil, destination_account_id: nil)
|
|
48
|
+
unless external_account_id.nil? ^ destination_account_id.nil?
|
|
49
|
+
raise Zazu::ArgumentError, "pass exactly one of external_account_id or destination_account_id"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
destination_account_id ? "own:#{destination_account_id}" : "ext:#{external_account_id}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/zazu/version.rb
CHANGED
data/lib/zazu.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "zazu/version"
|
|
|
20
20
|
require_relative "zazu/errors"
|
|
21
21
|
require_relative "zazu/response"
|
|
22
22
|
require_relative "zazu/page"
|
|
23
|
+
require_relative "zazu/transfer_authorization"
|
|
23
24
|
require_relative "zazu/resources/base"
|
|
24
25
|
require_relative "zazu/resources/accounts"
|
|
25
26
|
require_relative "zazu/resources/beneficiaries"
|
|
@@ -27,6 +28,7 @@ require_relative "zazu/resources/checkout_sessions"
|
|
|
27
28
|
require_relative "zazu/resources/customers"
|
|
28
29
|
require_relative "zazu/resources/entity"
|
|
29
30
|
require_relative "zazu/resources/invoices"
|
|
31
|
+
require_relative "zazu/resources/payee_trust_requests"
|
|
30
32
|
require_relative "zazu/resources/payment_links"
|
|
31
33
|
require_relative "zazu/resources/transfer_drafts"
|
|
32
34
|
require_relative "zazu/resources/webhook_endpoints"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zazu-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zazu
|
|
@@ -74,10 +74,12 @@ files:
|
|
|
74
74
|
- lib/zazu/resources/customers.rb
|
|
75
75
|
- lib/zazu/resources/entity.rb
|
|
76
76
|
- lib/zazu/resources/invoices.rb
|
|
77
|
+
- lib/zazu/resources/payee_trust_requests.rb
|
|
77
78
|
- lib/zazu/resources/payment_links.rb
|
|
78
79
|
- lib/zazu/resources/transfer_drafts.rb
|
|
79
80
|
- lib/zazu/resources/webhook_endpoints.rb
|
|
80
81
|
- lib/zazu/response.rb
|
|
82
|
+
- lib/zazu/transfer_authorization.rb
|
|
81
83
|
- lib/zazu/version.rb
|
|
82
84
|
homepage: https://github.com/getzazu/zazu-ruby
|
|
83
85
|
licenses:
|