manza 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 +7 -0
- data/CHANGELOG.md +129 -0
- data/LICENSE +21 -0
- data/README.md +265 -0
- data/lib/manza/client.rb +206 -0
- data/lib/manza/errors.rb +93 -0
- data/lib/manza/page.rb +67 -0
- data/lib/manza/resources/accounts.rb +61 -0
- data/lib/manza/resources/base.rb +136 -0
- data/lib/manza/resources/beneficiaries.rb +49 -0
- data/lib/manza/resources/checkout_sessions.rb +27 -0
- data/lib/manza/resources/customers.rb +42 -0
- data/lib/manza/resources/entity.rb +14 -0
- data/lib/manza/resources/invoices.rb +69 -0
- data/lib/manza/resources/payee_trust_requests.rb +23 -0
- data/lib/manza/resources/payment_links.rb +37 -0
- data/lib/manza/resources/transfer_drafts.rb +62 -0
- data/lib/manza/resources/webhook_endpoints.rb +67 -0
- data/lib/manza/response.rb +64 -0
- data/lib/manza/transfer_authorization.rb +55 -0
- data/lib/manza/version.rb +5 -0
- data/lib/manza.rb +35 -0
- metadata +110 -0
data/lib/manza/errors.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
4
|
+
# Base class for every Manza 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
|
+
# 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.
|
|
49
|
+
class ValidationError < Error; end
|
|
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
|
+
|
|
68
|
+
# 429 — rate limited. Retry after the `Retry-After` header (seconds).
|
|
69
|
+
class RateLimitError < Error
|
|
70
|
+
attr_reader :retry_after
|
|
71
|
+
|
|
72
|
+
def initialize(message = nil, retry_after: nil, **)
|
|
73
|
+
super(message, **)
|
|
74
|
+
@retry_after = retry_after
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 5xx — server error. Worth retrying once with backoff.
|
|
79
|
+
class ServerError < Error; end
|
|
80
|
+
|
|
81
|
+
# Network timeout, connection refused, DNS failure — anything that
|
|
82
|
+
# prevents the SDK from hearing back from the API.
|
|
83
|
+
class ConnectionError < Error; end
|
|
84
|
+
|
|
85
|
+
# The SDK was misconfigured (no API key, invalid base URL, etc.).
|
|
86
|
+
# Raised before any HTTP request is attempted.
|
|
87
|
+
class ConfigurationError < Error; end
|
|
88
|
+
|
|
89
|
+
# Caller passed a value the SDK refuses to send (e.g. `limit > 100`).
|
|
90
|
+
# Distinct from `ValidationError`, which represents server-side
|
|
91
|
+
# validation rejection.
|
|
92
|
+
class ArgumentError < Error; end
|
|
93
|
+
end
|
data/lib/manza/page.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
4
|
+
# A single page of a list endpoint's response.
|
|
5
|
+
#
|
|
6
|
+
# The Manza 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 Manza::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 Manza
|
|
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 [Manza::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 Manza
|
|
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 {Manza::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 {Manza::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 Manza::ArgumentError, "limit must be a positive integer (got #{limit.inspect})" unless limit.is_a?(Integer) && limit.positive?
|
|
103
|
+
|
|
104
|
+
raise Manza::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 Manza::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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
4
|
+
module Resources
|
|
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.
|
|
8
|
+
class Beneficiaries < Base
|
|
9
|
+
# GET /api/beneficiaries
|
|
10
|
+
def list(limit: MAX_PER_PAGE, cursor: nil)
|
|
11
|
+
list_page("api/beneficiaries", limit: limit, cursor: cursor)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /api/beneficiaries/:id
|
|
15
|
+
def get(id)
|
|
16
|
+
http_get(encode_path("api/beneficiaries", id))
|
|
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
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
4
|
+
module Resources
|
|
5
|
+
# One-off hosted checkout sessions. Pre-API there's no list,
|
|
6
|
+
# update, or delete — sessions are created and inspected by id.
|
|
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`.
|
|
10
|
+
class CheckoutSessions < Base
|
|
11
|
+
# GET /api/checkout_sessions/:id
|
|
12
|
+
def get(id)
|
|
13
|
+
http_get(encode_path("api/checkout_sessions", id))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# POST /api/checkout_sessions
|
|
17
|
+
#
|
|
18
|
+
# @param attributes [Hash] checkout-session attributes — see API docs.
|
|
19
|
+
# Required: account_id, amount, success_url.
|
|
20
|
+
# Optional: metadata, customer_email, customer_name, cancel_url,
|
|
21
|
+
# description, expires_at, collect_billing_address, billing_address.
|
|
22
|
+
def create(**attributes)
|
|
23
|
+
http_post("api/checkout_sessions", body: attributes)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
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, 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.
|
|
27
|
+
def create(**attributes)
|
|
28
|
+
http_post("api/customers", body: attributes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# PATCH /api/customers/:id
|
|
32
|
+
def update(id, **attributes)
|
|
33
|
+
http_patch(encode_path("api/customers", id), body: attributes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# DELETE /api/customers/:id
|
|
37
|
+
def delete(id)
|
|
38
|
+
http_delete(encode_path("api/customers", id))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
4
|
+
module Resources
|
|
5
|
+
# The current entity (the tenant the API key belongs to).
|
|
6
|
+
#
|
|
7
|
+
# client.entity.get # => Manza::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 Manza
|
|
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,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
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 Manza 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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
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
|
+
#
|
|
25
|
+
# Optional billing keys: collect_billing_address, billing_address.
|
|
26
|
+
# Responses carry `settled_at`; status includes `clearing`.
|
|
27
|
+
def create(**attributes)
|
|
28
|
+
http_post("api/payment_links", body: attributes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# POST /api/payment_links/:id/cancel
|
|
32
|
+
def cancel(id)
|
|
33
|
+
http_post(encode_path("api/payment_links", id, "cancel"))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Manza
|
|
4
|
+
module Resources
|
|
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.
|
|
15
|
+
class TransferDrafts < Base
|
|
16
|
+
# POST /api/transfer_drafts
|
|
17
|
+
#
|
|
18
|
+
# Required: account_id, amount, and exactly one of beneficiary_id
|
|
19
|
+
# (external transfer) or destination_account_id (own-account move).
|
|
20
|
+
# Optional: external_account_id, currency_code, payment_reference,
|
|
21
|
+
# internal_notes, client_reference (unique per entity, at most 128
|
|
22
|
+
# characters; a duplicate raises {Manza::ConflictError} whose
|
|
23
|
+
# `payment_id` names the existing draft).
|
|
24
|
+
def create(**attributes)
|
|
25
|
+
http_post("api/transfer_drafts", body: attributes)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# GET /api/transfer_drafts/:id
|
|
29
|
+
def get(id)
|
|
30
|
+
http_get(encode_path("api/transfer_drafts", id))
|
|
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
|
+
# {Manza::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 Manza::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
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|