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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +36 -0
  3. data/LICENSE +21 -0
  4. data/README.md +230 -0
  5. data/lib/nombaone/client.rb +158 -0
  6. data/lib/nombaone/errors.rb +286 -0
  7. data/lib/nombaone/internal/http_client.rb +230 -0
  8. data/lib/nombaone/internal/util.rb +155 -0
  9. data/lib/nombaone/object.rb +152 -0
  10. data/lib/nombaone/pagination.rb +116 -0
  11. data/lib/nombaone/resources/base_resource.rb +40 -0
  12. data/lib/nombaone/resources/coupons.rb +84 -0
  13. data/lib/nombaone/resources/customers.rb +160 -0
  14. data/lib/nombaone/resources/events.rb +46 -0
  15. data/lib/nombaone/resources/invoices.rb +61 -0
  16. data/lib/nombaone/resources/mandates.rb +71 -0
  17. data/lib/nombaone/resources/metrics.rb +24 -0
  18. data/lib/nombaone/resources/organization.rb +91 -0
  19. data/lib/nombaone/resources/payment_methods.rb +102 -0
  20. data/lib/nombaone/resources/plans.rb +129 -0
  21. data/lib/nombaone/resources/prices.rb +53 -0
  22. data/lib/nombaone/resources/sandbox.rb +81 -0
  23. data/lib/nombaone/resources/settlements.rb +91 -0
  24. data/lib/nombaone/resources/subscriptions.rb +310 -0
  25. data/lib/nombaone/resources/webhook_endpoints.rb +140 -0
  26. data/lib/nombaone/version.rb +7 -0
  27. data/lib/nombaone/webhook_event.rb +64 -0
  28. data/lib/nombaone/webhooks.rb +167 -0
  29. data/lib/nombaone.rb +58 -0
  30. data/sig/nombaone/client.rbs +36 -0
  31. data/sig/nombaone/errors.rbs +68 -0
  32. data/sig/nombaone/internal.rbs +51 -0
  33. data/sig/nombaone/object.rbs +22 -0
  34. data/sig/nombaone/pagination.rbs +23 -0
  35. data/sig/nombaone/resources.rbs +243 -0
  36. data/sig/nombaone/webhooks.rbs +18 -0
  37. data/sig/nombaone.rbs +24 -0
  38. 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