nombaone 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 +36 -0
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/lib/nombaone/client.rb +158 -0
- data/lib/nombaone/errors.rb +286 -0
- data/lib/nombaone/internal/http_client.rb +230 -0
- data/lib/nombaone/internal/util.rb +155 -0
- data/lib/nombaone/object.rb +152 -0
- data/lib/nombaone/pagination.rb +116 -0
- data/lib/nombaone/resources/base_resource.rb +40 -0
- data/lib/nombaone/resources/coupons.rb +84 -0
- data/lib/nombaone/resources/customers.rb +160 -0
- data/lib/nombaone/resources/events.rb +46 -0
- data/lib/nombaone/resources/invoices.rb +61 -0
- data/lib/nombaone/resources/mandates.rb +71 -0
- data/lib/nombaone/resources/metrics.rb +24 -0
- data/lib/nombaone/resources/organization.rb +91 -0
- data/lib/nombaone/resources/payment_methods.rb +102 -0
- data/lib/nombaone/resources/plans.rb +129 -0
- data/lib/nombaone/resources/prices.rb +53 -0
- data/lib/nombaone/resources/sandbox.rb +81 -0
- data/lib/nombaone/resources/settlements.rb +91 -0
- data/lib/nombaone/resources/subscriptions.rb +310 -0
- data/lib/nombaone/resources/webhook_endpoints.rb +140 -0
- data/lib/nombaone/version.rb +7 -0
- data/lib/nombaone/webhook_event.rb +64 -0
- data/lib/nombaone/webhooks.rb +167 -0
- data/lib/nombaone.rb +58 -0
- data/sig/nombaone/client.rbs +36 -0
- data/sig/nombaone/errors.rbs +68 -0
- data/sig/nombaone/internal.rbs +51 -0
- data/sig/nombaone/object.rbs +22 -0
- data/sig/nombaone/pagination.rbs +23 -0
- data/sig/nombaone/resources.rbs +243 -0
- data/sig/nombaone/webhooks.rbs +18 -0
- data/sig/nombaone.rbs +24 -0
- metadata +87 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
# A response object from the API. Every field is readable two ways:
|
|
5
|
+
#
|
|
6
|
+
# customer.amount_in_kobo # snake_case reader, derived from "amountInKobo"
|
|
7
|
+
# customer[:amount_in_kobo] # or bracket access (String/Symbol, snake or wire)
|
|
8
|
+
#
|
|
9
|
+
# Nested objects and arrays of objects are wrapped recursively, so
|
|
10
|
+
# `subscription.items.first.price_id` just works. `metadata` (and any
|
|
11
|
+
# free-form JSON) keeps its keys verbatim. Reach the raw wire Hash any time
|
|
12
|
+
# with {#to_h}.
|
|
13
|
+
#
|
|
14
|
+
# The object also carries the {#request_id} for the call that produced it and
|
|
15
|
+
# the raw {#last_response} (status + headers), for support and rate-limit
|
|
16
|
+
# introspection.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# sub = nombaone.subscriptions.retrieve("nbo000000000001sub")
|
|
20
|
+
# sub.status # => "active"
|
|
21
|
+
# sub.current_period_end # => "2026-08-05T00:00:00.000Z"
|
|
22
|
+
# sub.items.first.price_id # => "nbo000000000001prc"
|
|
23
|
+
# sub[:latestInvoiceId] # => "nbo000000000001inv"
|
|
24
|
+
# sub.request_id # => "req_…"
|
|
25
|
+
class NombaObject
|
|
26
|
+
# @return [String, nil] the `meta.requestId` of the call that produced this.
|
|
27
|
+
attr_reader :request_id
|
|
28
|
+
# @return [Nombaone::Internal::Response, nil] the raw HTTP response.
|
|
29
|
+
attr_reader :last_response
|
|
30
|
+
|
|
31
|
+
# @param values [Hash] the parsed wire object (camelCase string keys).
|
|
32
|
+
# @param request_id [String, nil]
|
|
33
|
+
# @param response [Nombaone::Internal::Response, nil]
|
|
34
|
+
# @api private
|
|
35
|
+
def initialize(values, request_id: nil, response: nil)
|
|
36
|
+
@values = values.is_a?(Hash) ? values : {}
|
|
37
|
+
@converted = {}
|
|
38
|
+
@request_id = request_id
|
|
39
|
+
@last_response = response
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Read a field by snake_case or wire name (String or Symbol). Returns nil
|
|
43
|
+
# when the field is absent — the lenient counterpart to a reader method
|
|
44
|
+
# (which raises `NoMethodError` on an unknown field).
|
|
45
|
+
#
|
|
46
|
+
# @param key [String, Symbol]
|
|
47
|
+
# @return [Object, nil]
|
|
48
|
+
def [](key)
|
|
49
|
+
wire = resolve_key(key)
|
|
50
|
+
wire.nil? ? nil : converted(wire)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Walk nested objects safely: `invoice.dig(:line_items, 0, :amount_in_kobo)`.
|
|
54
|
+
#
|
|
55
|
+
# @param keys [Array<String, Symbol, Integer>]
|
|
56
|
+
# @return [Object, nil]
|
|
57
|
+
def dig(*keys)
|
|
58
|
+
keys.reduce(self) do |node, key|
|
|
59
|
+
break nil if node.nil?
|
|
60
|
+
break nil unless node.is_a?(NombaObject) || node.is_a?(Array) || node.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
node[key]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param key [String, Symbol]
|
|
67
|
+
# @return [Boolean] whether the field is present.
|
|
68
|
+
def key?(key)
|
|
69
|
+
!resolve_key(key).nil?
|
|
70
|
+
end
|
|
71
|
+
alias has_key? key?
|
|
72
|
+
alias member? key?
|
|
73
|
+
|
|
74
|
+
# @return [Array<String>] the wire field names present on this object.
|
|
75
|
+
def keys
|
|
76
|
+
@values.keys
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The raw wire Hash, exactly as received (camelCase string keys, nested
|
|
80
|
+
# Hashes/Arrays). Use this to serialize the object back or inspect the
|
|
81
|
+
# untouched payload.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def to_h
|
|
85
|
+
@values
|
|
86
|
+
end
|
|
87
|
+
alias to_hash to_h
|
|
88
|
+
|
|
89
|
+
# Two objects are equal when their underlying wire Hashes are equal.
|
|
90
|
+
# `request_id`/response metadata do not participate.
|
|
91
|
+
#
|
|
92
|
+
# @param other [Object]
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def ==(other)
|
|
95
|
+
other.is_a?(NombaObject) && other.to_h == @values
|
|
96
|
+
end
|
|
97
|
+
alias eql? ==
|
|
98
|
+
|
|
99
|
+
# @return [Integer]
|
|
100
|
+
def hash
|
|
101
|
+
@values.hash
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [String]
|
|
105
|
+
def inspect
|
|
106
|
+
"#<#{self.class.name} #{@values.inspect}>"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @api private
|
|
110
|
+
def respond_to_missing?(name, include_private = false)
|
|
111
|
+
name = name.to_s
|
|
112
|
+
return false if name.end_with?("=", "?", "!")
|
|
113
|
+
|
|
114
|
+
!resolve_key(name).nil? || super
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @api private
|
|
118
|
+
def method_missing(name, *args)
|
|
119
|
+
wire = args.empty? ? resolve_key(name) : nil
|
|
120
|
+
return converted(wire) unless wire.nil?
|
|
121
|
+
|
|
122
|
+
super
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Resolve a caller-supplied key to the underlying wire key, matching either
|
|
128
|
+
# the exact wire name (a camelCase field, or a verbatim `metadata` key) or
|
|
129
|
+
# its snake_case form.
|
|
130
|
+
def resolve_key(key)
|
|
131
|
+
name = key.to_s
|
|
132
|
+
return name if @values.key?(name)
|
|
133
|
+
|
|
134
|
+
camel = Internal::Util.camelize(name)
|
|
135
|
+
@values.key?(camel) ? camel : nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def converted(wire_key)
|
|
139
|
+
return @converted[wire_key] if @converted.key?(wire_key)
|
|
140
|
+
|
|
141
|
+
@converted[wire_key] = wrap(@values[wire_key])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def wrap(value)
|
|
145
|
+
case value
|
|
146
|
+
when Hash then NombaObject.new(value)
|
|
147
|
+
when Array then value.map { |item| wrap(item) }
|
|
148
|
+
else value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
# One page of a list result, plus everything needed to keep going. A `Page`
|
|
5
|
+
# is {Enumerable}: iterating it (`each`, `map`, `select`, `first(n)`, …)
|
|
6
|
+
# walks **every item across every following page**, fetching each next page
|
|
7
|
+
# lazily as you go — so `list.first(3)` stops after one page.
|
|
8
|
+
#
|
|
9
|
+
# For a single page, read {#data}; for page-by-page control, use {#next_page}
|
|
10
|
+
# / {#next_page?} or {#each_page}.
|
|
11
|
+
#
|
|
12
|
+
# @example Every item, cursors handled for you
|
|
13
|
+
# nombaone.customers.list.each { |customer| puts customer.email }
|
|
14
|
+
#
|
|
15
|
+
# @example Just this page
|
|
16
|
+
# page = nombaone.invoices.list(status: "open", limit: 50)
|
|
17
|
+
# page.data # => [<Invoice>, …]
|
|
18
|
+
# page.next_cursor # => "…" or nil
|
|
19
|
+
#
|
|
20
|
+
# @example Manual paging
|
|
21
|
+
# page = page.next_page if page.next_page?
|
|
22
|
+
class Page
|
|
23
|
+
include Enumerable
|
|
24
|
+
|
|
25
|
+
# @return [Array<NombaObject>] the items on this page.
|
|
26
|
+
attr_reader :data
|
|
27
|
+
# @return [String, nil] the request id of the fetch that produced this page.
|
|
28
|
+
attr_reader :request_id
|
|
29
|
+
|
|
30
|
+
# @api private
|
|
31
|
+
def initialize(fetcher:, query:, result:)
|
|
32
|
+
@fetcher = fetcher
|
|
33
|
+
@query = query || {}
|
|
34
|
+
@data = Array(result.data).map { |item| item.is_a?(Hash) ? NombaObject.new(item) : item }
|
|
35
|
+
@page = result.pagination || { limit: @data.length, has_more: false, next_cursor: nil }
|
|
36
|
+
@request_id = result.request_id
|
|
37
|
+
@response = result.response
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The applied page size (1–100; the API default is 20).
|
|
41
|
+
# @return [Integer]
|
|
42
|
+
def limit = @page[:limit]
|
|
43
|
+
|
|
44
|
+
# Whether more items exist beyond this page.
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def has_more? = @page[:has_more]
|
|
47
|
+
|
|
48
|
+
# The opaque cursor for the next page, or nil when there is none.
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
def next_cursor = @page[:next_cursor]
|
|
51
|
+
|
|
52
|
+
# @return [Boolean] whether {#next_page} will return another page.
|
|
53
|
+
def next_page?
|
|
54
|
+
has_more? && !next_cursor.nil?
|
|
55
|
+
end
|
|
56
|
+
alias has_next_page? next_page?
|
|
57
|
+
|
|
58
|
+
# The cursor block as an object: `pagination.limit`, `pagination.has_more`,
|
|
59
|
+
# `pagination.next_cursor`.
|
|
60
|
+
# @return [NombaObject]
|
|
61
|
+
def pagination
|
|
62
|
+
@pagination ||= NombaObject.new(
|
|
63
|
+
{ "limit" => limit, "hasMore" => has_more?, "nextCursor" => next_cursor },
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Fetch the next page (same filters, next cursor).
|
|
68
|
+
# @return [Page]
|
|
69
|
+
# @raise [Nombaone::Error] if there is no next page — check {#next_page?}.
|
|
70
|
+
def next_page
|
|
71
|
+
unless next_page?
|
|
72
|
+
raise Error, "No next page available — check next_page? before calling next_page."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
paged_query = @query.merge(cursor: next_cursor)
|
|
76
|
+
Page.new(fetcher: @fetcher, query: paged_query, result: @fetcher.call(paged_query))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Iterate every item across this and all following pages. Without a block,
|
|
80
|
+
# returns an {Enumerator} (so the whole {Enumerable} toolbox works and stays
|
|
81
|
+
# lazy — `each` only fetches the next page when the current one is drained).
|
|
82
|
+
#
|
|
83
|
+
# @yieldparam item [NombaObject]
|
|
84
|
+
# @return [Enumerator, self]
|
|
85
|
+
def each(&block)
|
|
86
|
+
return to_enum(:each) unless block
|
|
87
|
+
|
|
88
|
+
page = self
|
|
89
|
+
loop do
|
|
90
|
+
page.data.each(&block)
|
|
91
|
+
break unless page.next_page?
|
|
92
|
+
|
|
93
|
+
page = page.next_page
|
|
94
|
+
end
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
alias auto_paging_each each
|
|
98
|
+
|
|
99
|
+
# Iterate page-by-page (this page first), fetching each next page lazily.
|
|
100
|
+
#
|
|
101
|
+
# @yieldparam page [Page]
|
|
102
|
+
# @return [Enumerator, self]
|
|
103
|
+
def each_page
|
|
104
|
+
return to_enum(:each_page) unless block_given?
|
|
105
|
+
|
|
106
|
+
page = self
|
|
107
|
+
loop do
|
|
108
|
+
yield page
|
|
109
|
+
break unless page.next_page?
|
|
110
|
+
|
|
111
|
+
page = page.next_page
|
|
112
|
+
end
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
# The API resource namespaces (`nombaone.customers`, `nombaone.subscriptions`,
|
|
5
|
+
# …). Each is a thin, fully-documented transcription of one slice of the wire
|
|
6
|
+
# contract.
|
|
7
|
+
module Resources
|
|
8
|
+
# Re-exported so resource method signatures can default optional keyword
|
|
9
|
+
# arguments to `OMIT` (dropped from the body) rather than `nil` (sent as
|
|
10
|
+
# JSON `null` to clear a nullable field). See {Nombaone::Internal::OMIT}.
|
|
11
|
+
OMIT = Internal::OMIT
|
|
12
|
+
|
|
13
|
+
# Base class every resource namespace extends. Holds the client and the
|
|
14
|
+
# small request helpers each method builds on.
|
|
15
|
+
class BaseResource
|
|
16
|
+
# @api private
|
|
17
|
+
def initialize(client)
|
|
18
|
+
@client = client
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Issue a single-object request; returns a {NombaObject}.
|
|
24
|
+
def request(method, path, body: nil, query: nil, options: {})
|
|
25
|
+
@client.request(method: method, path: path, body: body, query: query, options: options)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Issue a list request; returns a {Page}.
|
|
29
|
+
def request_page(path, query: nil, options: {})
|
|
30
|
+
@client.request_page(method: :get, path: path, query: query, options: options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Percent-encode one id before splicing it into a path — ids come from
|
|
34
|
+
# user input and are never trusted raw.
|
|
35
|
+
def encode(value)
|
|
36
|
+
Internal::Util.encode_path_segment(value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
module Resources
|
|
5
|
+
# Coupons — reusable discount rules you apply via
|
|
6
|
+
# `customers.apply_discount` / `subscriptions.apply_discount`. The coupon is
|
|
7
|
+
# the rule; applying it creates a discount (one application of it).
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# coupon = nombaone.coupons.create(
|
|
11
|
+
# code: "LAUNCH20", percent_off: 20, duration: "repeating", duration_in_cycles: 3
|
|
12
|
+
# )
|
|
13
|
+
class Coupons < BaseResource
|
|
14
|
+
# Create a coupon. Set **exactly one** of `amount_off_in_kobo` / `percent_off`.
|
|
15
|
+
#
|
|
16
|
+
# @param code [String] the tenant-facing redemption code, e.g. `"LAUNCH20"`.
|
|
17
|
+
# @param duration [String] `"once"`, `"repeating"`, or `"forever"`.
|
|
18
|
+
# @param amount_off_in_kobo [Integer] fixed discount, **integer kobo** (₦1.00 = 100).
|
|
19
|
+
# @param percent_off [Integer] percentage discount, 1–100.
|
|
20
|
+
# @param duration_in_cycles [Integer] required when `duration` is `"repeating"`.
|
|
21
|
+
# @param redeem_by [String] ISO-8601 date-time after which it can't be applied.
|
|
22
|
+
# @param max_redemptions [Integer]
|
|
23
|
+
# @param metadata [Hash]
|
|
24
|
+
# @param request_options [Hash]
|
|
25
|
+
# @return [NombaObject]
|
|
26
|
+
# @raise [Nombaone::ValidationError] 422 `COUPON_INVALID_DEFINITION` — set
|
|
27
|
+
# exactly one of `amount_off_in_kobo` / `percent_off`.
|
|
28
|
+
def create(code:, duration:, amount_off_in_kobo: OMIT, percent_off: OMIT,
|
|
29
|
+
duration_in_cycles: OMIT, redeem_by: OMIT, max_redemptions: OMIT,
|
|
30
|
+
metadata: OMIT, request_options: {})
|
|
31
|
+
request(:post, "/coupons",
|
|
32
|
+
body: {
|
|
33
|
+
code: code,
|
|
34
|
+
duration: duration,
|
|
35
|
+
amount_off_in_kobo: amount_off_in_kobo,
|
|
36
|
+
percent_off: percent_off,
|
|
37
|
+
duration_in_cycles: duration_in_cycles,
|
|
38
|
+
redeem_by: redeem_by,
|
|
39
|
+
max_redemptions: max_redemptions,
|
|
40
|
+
metadata: metadata,
|
|
41
|
+
},
|
|
42
|
+
options: request_options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Retrieve a coupon by id.
|
|
46
|
+
#
|
|
47
|
+
# @param id [String] `nbo…cpn`
|
|
48
|
+
# @param request_options [Hash]
|
|
49
|
+
# @return [NombaObject]
|
|
50
|
+
# @raise [Nombaone::NotFoundError] 404 `COUPON_NOT_FOUND`
|
|
51
|
+
def retrieve(id, request_options: {})
|
|
52
|
+
request(:get, "/coupons/#{encode(id)}", options: request_options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Update a coupon's redeem-by, max redemptions, or metadata.
|
|
56
|
+
#
|
|
57
|
+
# @param id [String] `nbo…cpn`
|
|
58
|
+
# @param redeem_by [String]
|
|
59
|
+
# @param max_redemptions [Integer]
|
|
60
|
+
# @param metadata [Hash]
|
|
61
|
+
# @param request_options [Hash]
|
|
62
|
+
# @return [NombaObject]
|
|
63
|
+
def update(id, redeem_by: OMIT, max_redemptions: OMIT, metadata: OMIT, request_options: {})
|
|
64
|
+
request(:patch, "/coupons/#{encode(id)}",
|
|
65
|
+
body: {
|
|
66
|
+
redeem_by: redeem_by,
|
|
67
|
+
max_redemptions: max_redemptions,
|
|
68
|
+
metadata: metadata,
|
|
69
|
+
},
|
|
70
|
+
options: request_options)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# List coupons, newest first.
|
|
74
|
+
#
|
|
75
|
+
# @param limit [Integer] page size, 1–100 (API default 20).
|
|
76
|
+
# @param cursor [String] opaque cursor from a previous page.
|
|
77
|
+
# @param request_options [Hash]
|
|
78
|
+
# @return [Page<NombaObject>]
|
|
79
|
+
def list(limit: OMIT, cursor: OMIT, request_options: {})
|
|
80
|
+
request_page("/coupons", query: { limit: limit, cursor: cursor }, options: request_options)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
module Resources
|
|
5
|
+
# Customers — the people and businesses you bill.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# customer = nombaone.customers.create(email: "ada@example.com", name: "Ada Lovelace")
|
|
9
|
+
# customer.id # => "nbo…cus"
|
|
10
|
+
class Customers < BaseResource
|
|
11
|
+
# Create a customer.
|
|
12
|
+
#
|
|
13
|
+
# @param email [String] unique per organization + environment
|
|
14
|
+
# (`CUSTOMER_EMAIL_TAKEN` on reuse).
|
|
15
|
+
# @param name [String]
|
|
16
|
+
# @param phone [String, nil]
|
|
17
|
+
# @param metadata [Hash] free-form annotations (keys are stored verbatim).
|
|
18
|
+
# @param request_options [Hash] per-call overrides (`:idempotency_key`,
|
|
19
|
+
# `:headers`, `:timeout`, `:max_retries`, `:cancel_when`).
|
|
20
|
+
# @return [NombaObject] the created customer.
|
|
21
|
+
# @raise [Nombaone::ValidationError] 422 `CLIENT_VALIDATION_FAILED` — see `error.fields`.
|
|
22
|
+
# @raise [Nombaone::ConflictError] 409 `CUSTOMER_EMAIL_TAKEN` — reuse the existing customer.
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# customer = nombaone.customers.create(
|
|
26
|
+
# email: "ada@example.com",
|
|
27
|
+
# name: "Ada Lovelace",
|
|
28
|
+
# metadata: { crm_id: "crm_812" },
|
|
29
|
+
# )
|
|
30
|
+
def create(email:, name:, phone: OMIT, metadata: OMIT, request_options: {})
|
|
31
|
+
request(:post, "/customers",
|
|
32
|
+
body: { email: email, name: name, phone: phone, metadata: metadata },
|
|
33
|
+
options: request_options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Retrieve a customer by id.
|
|
37
|
+
#
|
|
38
|
+
# @param id [String] `nbo…cus`
|
|
39
|
+
# @param request_options [Hash]
|
|
40
|
+
# @return [NombaObject]
|
|
41
|
+
# @raise [Nombaone::NotFoundError] 404 `CUSTOMER_NOT_FOUND` — check the id
|
|
42
|
+
# and that your key matches the environment the customer was created in.
|
|
43
|
+
def retrieve(id, request_options: {})
|
|
44
|
+
request(:get, "/customers/#{encode(id)}", options: request_options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Update a customer's mutable fields. At least one field is required.
|
|
48
|
+
#
|
|
49
|
+
# @param id [String] `nbo…cus`
|
|
50
|
+
# @param name [String]
|
|
51
|
+
# @param phone [String, nil] pass `nil` to clear the phone number.
|
|
52
|
+
# @param metadata [Hash]
|
|
53
|
+
# @param request_options [Hash]
|
|
54
|
+
# @return [NombaObject]
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# nombaone.customers.update(customer.id, phone: "+2348012345678")
|
|
58
|
+
def update(id, name: OMIT, phone: OMIT, metadata: OMIT, request_options: {})
|
|
59
|
+
request(:patch, "/customers/#{encode(id)}",
|
|
60
|
+
body: { name: name, phone: phone, metadata: metadata },
|
|
61
|
+
options: request_options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# List customers, newest first.
|
|
65
|
+
#
|
|
66
|
+
# @param email [String] exact-match filter on email.
|
|
67
|
+
# @param limit [Integer] page size, 1–100 (API default 20).
|
|
68
|
+
# @param cursor [String] opaque cursor from a previous page's `next_cursor`.
|
|
69
|
+
# @param request_options [Hash]
|
|
70
|
+
# @return [Page<NombaObject>]
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# nombaone.customers.list.each { |customer| puts customer.email }
|
|
74
|
+
def list(email: OMIT, limit: OMIT, cursor: OMIT, request_options: {})
|
|
75
|
+
request_page("/customers",
|
|
76
|
+
query: { email: email, limit: limit, cursor: cursor },
|
|
77
|
+
options: request_options)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Apply a coupon to a customer. The resulting discount shapes every future
|
|
81
|
+
# invoice for the customer until it ends or is removed.
|
|
82
|
+
#
|
|
83
|
+
# @param id [String] `nbo…cus`
|
|
84
|
+
# @param coupon [String] a coupon id (`nbo…cpn`) or its code (e.g. `"LAUNCH20"`).
|
|
85
|
+
# @param request_options [Hash]
|
|
86
|
+
# @return [NombaObject] the created discount.
|
|
87
|
+
# @raise [Nombaone::NotFoundError] 404 `COUPON_NOT_FOUND`
|
|
88
|
+
# @raise [Nombaone::ConflictError] 409 `COUPON_ALREADY_APPLIED`
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# nombaone.customers.apply_discount(customer.id, coupon: "LAUNCH20")
|
|
92
|
+
def apply_discount(id, coupon:, request_options: {})
|
|
93
|
+
request(:post, "/customers/#{encode(id)}/discount",
|
|
94
|
+
body: { coupon: coupon }, options: request_options)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Remove the customer's active discount. Returns the ended discount.
|
|
98
|
+
#
|
|
99
|
+
# @param id [String] `nbo…cus`
|
|
100
|
+
# @param request_options [Hash]
|
|
101
|
+
# @return [NombaObject]
|
|
102
|
+
def remove_discount(id, request_options: {})
|
|
103
|
+
request(:delete, "/customers/#{encode(id)}/discount", options: request_options)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Grant credit to a customer. Credit is drawn down oldest-grant-first by
|
|
107
|
+
# future invoices **before** any payment rail is charged.
|
|
108
|
+
#
|
|
109
|
+
# This moves money-shaped state, so the API requires an `Idempotency-Key`;
|
|
110
|
+
# the SDK sends one automatically. Pass `request_options[:idempotency_key]`
|
|
111
|
+
# to keep the grant idempotent across process restarts.
|
|
112
|
+
#
|
|
113
|
+
# @param id [String] `nbo…cus`
|
|
114
|
+
# @param amount_in_kobo [Integer] amount to grant, **integer kobo**
|
|
115
|
+
# (₦1.00 = 100). `250_000` is ₦2,500 — not ₦250,000. Multiply naira by
|
|
116
|
+
# 100 exactly once, at the edge of your system.
|
|
117
|
+
# @param source [String] `"manual"` or `"goodwill"` (defaults to `manual`).
|
|
118
|
+
# @param source_reference [String] your own reference (ticket, promo id, …).
|
|
119
|
+
# @param metadata [Hash]
|
|
120
|
+
# @param request_options [Hash]
|
|
121
|
+
# @return [NombaObject] the created credit grant.
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# nombaone.customers.grant_credit(customer.id, amount_in_kobo: 250_000, source: "goodwill")
|
|
125
|
+
def grant_credit(id, amount_in_kobo:, source: OMIT, source_reference: OMIT,
|
|
126
|
+
metadata: OMIT, request_options: {})
|
|
127
|
+
request(:post, "/customers/#{encode(id)}/credit",
|
|
128
|
+
body: {
|
|
129
|
+
amount_in_kobo: amount_in_kobo,
|
|
130
|
+
source: source,
|
|
131
|
+
source_reference: source_reference,
|
|
132
|
+
metadata: metadata,
|
|
133
|
+
},
|
|
134
|
+
options: request_options)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Retrieve the customer's credit balance and the grants behind it.
|
|
138
|
+
#
|
|
139
|
+
# @param id [String] `nbo…cus`
|
|
140
|
+
# @param request_options [Hash]
|
|
141
|
+
# @return [NombaObject] with `balance_in_kobo` and `grants`.
|
|
142
|
+
def retrieve_credit_balance(id, request_options: {})
|
|
143
|
+
request(:get, "/customers/#{encode(id)}/credit", options: request_options)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Void a credit grant — its remaining balance becomes unusable. Consumed
|
|
147
|
+
# credit is untouched.
|
|
148
|
+
#
|
|
149
|
+
# @param id [String] `nbo…cus`
|
|
150
|
+
# @param grant_id [String] `nbo…crg`
|
|
151
|
+
# @param request_options [Hash]
|
|
152
|
+
# @return [NombaObject]
|
|
153
|
+
# @raise [Nombaone::ConflictError] 409 `CREDIT_GRANT_ALREADY_VOIDED`
|
|
154
|
+
def void_credit(id, grant_id, request_options: {})
|
|
155
|
+
request(:delete, "/customers/#{encode(id)}/credit/#{encode(grant_id)}",
|
|
156
|
+
options: request_options)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
module Resources
|
|
5
|
+
# Events — the append-only log behind every webhook. Webhook delivery is
|
|
6
|
+
# at-least-once; this log is your reconciliation backstop when a delivery
|
|
7
|
+
# was missed or you need to backfill.
|
|
8
|
+
class Events < BaseResource
|
|
9
|
+
# List events, newest first.
|
|
10
|
+
#
|
|
11
|
+
# @param type [String] filter to one catalog type, e.g. `"invoice.paid"`.
|
|
12
|
+
# @param limit [Integer] page size, 1–100 (API default 20).
|
|
13
|
+
# @param cursor [String] opaque cursor from a previous page.
|
|
14
|
+
# @param request_options [Hash]
|
|
15
|
+
# @return [Page<NombaObject>]
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# nombaone.events.list(type: "invoice.paid").each { |event| puts event.id }
|
|
19
|
+
def list(type: OMIT, limit: OMIT, cursor: OMIT, request_options: {})
|
|
20
|
+
request_page("/events",
|
|
21
|
+
query: { type: type, limit: limit, cursor: cursor },
|
|
22
|
+
options: request_options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Retrieve one event by id.
|
|
26
|
+
#
|
|
27
|
+
# @param id [String] `nbo…evt`
|
|
28
|
+
# @param request_options [Hash]
|
|
29
|
+
# @return [NombaObject]
|
|
30
|
+
def retrieve(id, request_options: {})
|
|
31
|
+
request(:get, "/events/#{encode(id)}", options: request_options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The machine-readable event catalog — every event type the platform can
|
|
35
|
+
# emit, with a description and its `data` keys. Useful for building
|
|
36
|
+
# subscription pickers or codegen.
|
|
37
|
+
#
|
|
38
|
+
# @param request_options [Hash]
|
|
39
|
+
# @return [NombaObject] a map of `type => { when:, payload: }`; read entries
|
|
40
|
+
# with `catalog["invoice.paid"]`.
|
|
41
|
+
def catalog(request_options: {})
|
|
42
|
+
request(:get, "/events/catalog", options: request_options)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nombaone
|
|
4
|
+
module Resources
|
|
5
|
+
# Invoices — read what the billing engine produced; void what should never
|
|
6
|
+
# be collected. You never create invoices — subscription cycles do; amounts
|
|
7
|
+
# are locked at finalization. All amounts are integer kobo.
|
|
8
|
+
class Invoices < BaseResource
|
|
9
|
+
# Retrieve an invoice by id.
|
|
10
|
+
#
|
|
11
|
+
# @param id [String] `nbo…inv`
|
|
12
|
+
# @param request_options [Hash]
|
|
13
|
+
# @return [NombaObject]
|
|
14
|
+
# @raise [Nombaone::NotFoundError] 404 `INVOICE_NOT_FOUND`
|
|
15
|
+
def retrieve(id, request_options: {})
|
|
16
|
+
request(:get, "/invoices/#{encode(id)}", options: request_options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# List invoices, newest first.
|
|
20
|
+
#
|
|
21
|
+
# @param customer_id [String] filter to one customer (`nbo…cus`).
|
|
22
|
+
# @param subscription_id [String] filter to one subscription (`nbo…sub`).
|
|
23
|
+
# @param status [String] the list filter accepts `"draft"`, `"open"`,
|
|
24
|
+
# `"paid"`, `"void"`, `"uncollectible"` (note: no `"partially_paid"`,
|
|
25
|
+
# though invoice objects can carry that status).
|
|
26
|
+
# @param limit [Integer] page size, 1–100 (API default 20).
|
|
27
|
+
# @param cursor [String] opaque cursor from a previous page.
|
|
28
|
+
# @param request_options [Hash]
|
|
29
|
+
# @return [Page<NombaObject>]
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# nombaone.invoices.list(status: "open").each { |inv| puts inv.amount_due_in_kobo }
|
|
33
|
+
def list(customer_id: OMIT, subscription_id: OMIT, status: OMIT, limit: OMIT, cursor: OMIT,
|
|
34
|
+
request_options: {})
|
|
35
|
+
request_page("/invoices",
|
|
36
|
+
query: {
|
|
37
|
+
customer_id: customer_id,
|
|
38
|
+
subscription_id: subscription_id,
|
|
39
|
+
status: status,
|
|
40
|
+
limit: limit,
|
|
41
|
+
cursor: cursor,
|
|
42
|
+
},
|
|
43
|
+
options: request_options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Void an open, unpaid invoice. Paid invoices can't be voided — refund the
|
|
47
|
+
# settlement instead.
|
|
48
|
+
#
|
|
49
|
+
# @param id [String] `nbo…inv`
|
|
50
|
+
# @param comment [String]
|
|
51
|
+
# @param request_options [Hash]
|
|
52
|
+
# @return [NombaObject]
|
|
53
|
+
# @raise [Nombaone::ConflictError] 409 `INVOICE_NOT_VOIDABLE`
|
|
54
|
+
# @raise [Nombaone::ConflictError] 409 `INVOICE_ALREADY_PAID`
|
|
55
|
+
def void(id, comment: OMIT, request_options: {})
|
|
56
|
+
request(:post, "/invoices/#{encode(id)}/void",
|
|
57
|
+
body: { comment: comment }, options: request_options)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|