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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Direct-debit mandates (NIBSS). Creation is **asynchronous**: the mandate
6
+ # starts `consent_pending` and activates only after the customer authorizes
7
+ # it with their bank — the engine sweeps for activation and fires
8
+ # `payment_method.attached` / `payment_method.updated`. Don't poll in a tight
9
+ # loop; listen for the webhook, and don't charge before it's active
10
+ # (`MANDATE_NOT_ACTIVE` / `MANDATE_CONSENT_PENDING`).
11
+ class Mandates < BaseResource
12
+ # Create a mandate. Requires an `Idempotency-Key` (sent automatically).
13
+ #
14
+ # @param customer_ref [String] the customer this mandate belongs to (`nbo…cus`).
15
+ # @param customer_account_number [String]
16
+ # @param bank_code [String] CBN 3-digit bank code (058 GTB · 044 Access · 033 UBA · …).
17
+ # @param customer_name [String]
18
+ # @param customer_account_name [String]
19
+ # @param customer_phone_number [String]
20
+ # @param customer_address [String]
21
+ # @param narration [String] shown on the customer's statement.
22
+ # @param max_amount_in_kobo [Integer] hard per-debit ceiling, **integer
23
+ # kobo** (₦1.00 = 100). Charges above it fail with `MANDATE_MAX_AMOUNT_EXCEEDED`.
24
+ # @param frequency [String] defaults to `"monthly"` server-side.
25
+ # @param start_date [String] local date-time (no zone); defaults to tomorrow.
26
+ # @param end_date [String] local date-time (no zone); defaults to one year out.
27
+ # @param request_options [Hash]
28
+ # @return [NombaObject] the pending mandate setup; relay its
29
+ # `consent_instruction` to the customer, then wait for the webhook.
30
+ #
31
+ # @example
32
+ # mandate = nombaone.mandates.create(
33
+ # customer_ref: customer.id, customer_account_number: "0123456789", bank_code: "058",
34
+ # customer_name: "Ada Lovelace", customer_account_name: "Ada Lovelace",
35
+ # customer_phone_number: "+2348012345678", customer_address: "1 Marina, Lagos",
36
+ # narration: "Acme Pro subscription", max_amount_in_kobo: 500_000
37
+ # )
38
+ def create(customer_ref:, customer_account_number:, bank_code:, customer_name:,
39
+ customer_account_name:, customer_phone_number:, customer_address:, narration:,
40
+ max_amount_in_kobo:, frequency: OMIT, start_date: OMIT, end_date: OMIT,
41
+ request_options: {})
42
+ request(:post, "/mandates",
43
+ body: {
44
+ customer_ref: customer_ref,
45
+ customer_account_number: customer_account_number,
46
+ bank_code: bank_code,
47
+ customer_name: customer_name,
48
+ customer_account_name: customer_account_name,
49
+ customer_phone_number: customer_phone_number,
50
+ customer_address: customer_address,
51
+ narration: narration,
52
+ max_amount_in_kobo: max_amount_in_kobo,
53
+ frequency: frequency,
54
+ start_date: start_date,
55
+ end_date: end_date,
56
+ },
57
+ options: request_options)
58
+ end
59
+
60
+ # Check a mandate's current standing. Returns the underlying
61
+ # **payment-method** row (its `status` moves `consent_pending` → `active`).
62
+ #
63
+ # @param id [String] `nbo…pmt`
64
+ # @param request_options [Hash]
65
+ # @return [NombaObject] a payment method.
66
+ def retrieve(id, request_options: {})
67
+ request(:get, "/mandates/#{encode(id)}", options: request_options)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Metrics — MRR, churn, and the dunning funnel, computed from the ledger on
6
+ # read (never stored, never stale).
7
+ class Metrics < BaseResource
8
+ # Billing KPIs over a window (defaults to a recent window server-side).
9
+ #
10
+ # @param from [String] ISO-8601 date-time, start of the window.
11
+ # @param to [String] ISO-8601 date-time, end of the window.
12
+ # @param request_options [Hash]
13
+ # @return [NombaObject] with `mrr_in_kobo`, `active_count`, churn counts,
14
+ # and the `dunning_funnel`.
15
+ #
16
+ # @example
17
+ # metrics = nombaone.metrics.billing
18
+ # puts "MRR ₦#{metrics.mrr_in_kobo / 100}"
19
+ def billing(from: OMIT, to: OMIT, request_options: {})
20
+ request(:get, "/metrics/billing", query: { from: from, to: to }, options: request_options)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Billing + dunning policy under `nombaone.organization.billing`.
6
+ class OrganizationBilling < BaseResource
7
+ # Read the org's billing + dunning policy.
8
+ #
9
+ # @param request_options [Hash]
10
+ # @return [NombaObject]
11
+ def retrieve(request_options: {})
12
+ request(:get, "/organization/billing", options: request_options)
13
+ end
14
+
15
+ # Update the billing policy. PUT semantics, but only supplied keys change.
16
+ #
17
+ # @param partial_collection_enabled [Boolean]
18
+ # @param proration_credit_policy [String] `"credit_next_cycle"` or `"none"`.
19
+ # @param dunning_max_attempts [Integer] 1–10.
20
+ # @param dunning_intervals_hours [Array<Integer>]
21
+ # @param dunning_max_window_hours [Integer] must be ≥ the largest interval.
22
+ # @param grace_period_hours [Integer]
23
+ # @param payday_days [Array<Integer>] days of month, 1–31.
24
+ # @param payday_pull_forward_days [Integer] 0–28.
25
+ # @param payday_bias_enabled [Boolean]
26
+ # @param default_collection_method [String] `"charge_automatically"` or `"send_invoice"`.
27
+ # @param comms_enabled [Boolean]
28
+ # @param request_options [Hash]
29
+ # @return [NombaObject]
30
+ #
31
+ # @example
32
+ # nombaone.organization.billing.update(payday_bias_enabled: true, payday_days: [25, 28, 30])
33
+ def update(partial_collection_enabled: OMIT, proration_credit_policy: OMIT,
34
+ dunning_max_attempts: OMIT, dunning_intervals_hours: OMIT,
35
+ dunning_max_window_hours: OMIT, grace_period_hours: OMIT, payday_days: OMIT,
36
+ payday_pull_forward_days: OMIT, payday_bias_enabled: OMIT,
37
+ default_collection_method: OMIT, comms_enabled: OMIT, request_options: {})
38
+ request(:put, "/organization/billing",
39
+ body: {
40
+ partial_collection_enabled: partial_collection_enabled,
41
+ proration_credit_policy: proration_credit_policy,
42
+ dunning_max_attempts: dunning_max_attempts,
43
+ dunning_intervals_hours: dunning_intervals_hours,
44
+ dunning_max_window_hours: dunning_max_window_hours,
45
+ grace_period_hours: grace_period_hours,
46
+ payday_days: payday_days,
47
+ payday_pull_forward_days: payday_pull_forward_days,
48
+ payday_bias_enabled: payday_bias_enabled,
49
+ default_collection_method: default_collection_method,
50
+ comms_enabled: comms_enabled,
51
+ },
52
+ options: request_options)
53
+ end
54
+ end
55
+
56
+ # Organization settings — configuration, not a billing object.
57
+ class Organization < BaseResource
58
+ # Billing + dunning policy.
59
+ # @return [OrganizationBilling]
60
+ def billing
61
+ @billing ||= OrganizationBilling.new(@client)
62
+ end
63
+
64
+ # Read org-level settings (limits, settlement mode, branding, statuses).
65
+ #
66
+ # @param request_options [Hash]
67
+ # @return [NombaObject]
68
+ def retrieve(request_options: {})
69
+ request(:get, "/organization", options: request_options)
70
+ end
71
+
72
+ # Update tenant-editable settings. At least one field is required.
73
+ #
74
+ # @param monthly_request_quota [Integer]
75
+ # @param settlement_mode [String] `"split_at_collection"` or `"collect_then_payout"`.
76
+ # @param branding [Hash] `display_name`, `support_email`, `logo_url`, `primary_color_hex`.
77
+ # @param request_options [Hash]
78
+ # @return [NombaObject]
79
+ def update(monthly_request_quota: OMIT, settlement_mode: OMIT, branding: OMIT,
80
+ request_options: {})
81
+ request(:put, "/organization",
82
+ body: {
83
+ monthly_request_quota: monthly_request_quota,
84
+ settlement_mode: settlement_mode,
85
+ branding: branding,
86
+ },
87
+ options: request_options)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Payment methods — cards (via hosted checkout), direct-debit mandates (see
6
+ # `nombaone.mandates`), and virtual accounts for the transfer rail.
7
+ #
8
+ # Card and mandate are **pull** rails (the engine initiates the debit); a
9
+ # virtual account is the **push** rail (the customer sends a transfer and
10
+ # the engine matches it — never treat a collect as instantly final).
11
+ class PaymentMethods < BaseResource
12
+ # Start a hosted-checkout card capture. Card entry happens on the PCI
13
+ # hosted page — no card data ever touches your servers. The method appears
14
+ # as `setup_pending` until the customer completes checkout.
15
+ #
16
+ # @param customer_ref [String] the customer this card will belong to (`nbo…cus`).
17
+ # @param amount_in_kobo [Integer] the validation charge, **integer kobo** (₦1.00 = 100).
18
+ # @param callback_url [String] where the hosted checkout returns the customer.
19
+ # @param request_options [Hash]
20
+ # @return [NombaObject] with a `checkout_link` to redirect the customer to.
21
+ #
22
+ # @example
23
+ # setup = nombaone.payment_methods.setup(
24
+ # customer_ref: customer.id, amount_in_kobo: 5_000, callback_url: "https://example.com/return"
25
+ # )
26
+ # # redirect the customer to setup.checkout_link
27
+ def setup(customer_ref:, amount_in_kobo:, callback_url:, request_options: {})
28
+ request(:post, "/payment-methods/setup",
29
+ body: {
30
+ customer_ref: customer_ref,
31
+ amount_in_kobo: amount_in_kobo,
32
+ callback_url: callback_url,
33
+ },
34
+ options: request_options)
35
+ end
36
+
37
+ # Issue a dedicated virtual account (NUBAN) so the customer can pay by
38
+ # bank transfer. The engine matches inbound transfers to invoices by
39
+ # reference and exact integer-kobo amount.
40
+ #
41
+ # @param customer_ref [String] the customer to issue the account for (`nbo…cus`).
42
+ # @param expected_amount [Integer] optional expected amount hint, integer kobo.
43
+ # @param expiry_date [String] optional ISO date the account should expire.
44
+ # @param request_options [Hash]
45
+ # @return [NombaObject]
46
+ def create_virtual_account(customer_ref:, expected_amount: OMIT, expiry_date: OMIT,
47
+ request_options: {})
48
+ request(:post, "/payment-methods/virtual-account",
49
+ body: {
50
+ customer_ref: customer_ref,
51
+ expected_amount: expected_amount,
52
+ expiry_date: expiry_date,
53
+ },
54
+ options: request_options)
55
+ end
56
+
57
+ # Retrieve a payment method by id.
58
+ #
59
+ # @param id [String] `nbo…pmt`
60
+ # @param request_options [Hash]
61
+ # @return [NombaObject]
62
+ # @raise [Nombaone::NotFoundError] 404 `PAYMENT_METHOD_NOT_FOUND`
63
+ def retrieve(id, request_options: {})
64
+ request(:get, "/payment-methods/#{encode(id)}", options: request_options)
65
+ end
66
+
67
+ # List payment methods, newest first.
68
+ #
69
+ # @param customer_ref [String] filter to one customer (`nbo…cus`). Note
70
+ # the wire filter name is `customerRef`.
71
+ # @param limit [Integer] page size, 1–100 (API default 20).
72
+ # @param cursor [String] opaque cursor from a previous page.
73
+ # @param request_options [Hash]
74
+ # @return [Page<NombaObject>]
75
+ def list(customer_ref: OMIT, limit: OMIT, cursor: OMIT, request_options: {})
76
+ request_page("/payment-methods",
77
+ query: { customer_ref: customer_ref, limit: limit, cursor: cursor },
78
+ options: request_options)
79
+ end
80
+
81
+ # Make this the customer's default payment method.
82
+ #
83
+ # @param id [String] `nbo…pmt`
84
+ # @param request_options [Hash]
85
+ # @return [NombaObject]
86
+ def set_default(id, request_options: {})
87
+ request(:post, "/payment-methods/#{encode(id)}/default", body: {}, options: request_options)
88
+ end
89
+
90
+ # Detach a payment method. Subscriptions still billing against it will
91
+ # need a replacement (`SUBSCRIPTION_PAYMENT_METHOD_REQUIRED` at next charge
92
+ # otherwise).
93
+ #
94
+ # @param id [String] `nbo…pmt`
95
+ # @param request_options [Hash]
96
+ # @return [NombaObject]
97
+ def remove(id, request_options: {})
98
+ request(:delete, "/payment-methods/#{encode(id)}", options: request_options)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Prices nested under a plan (create/list); reach `nombaone.prices` for
6
+ # reads and deactivation.
7
+ class PlanPrices < BaseResource
8
+ # Create a price under a plan. Prices are immutable once created.
9
+ #
10
+ # @param plan_id [String] `nbo…pln`
11
+ # @param unit_amount_in_kobo [Integer] amount per unit per interval,
12
+ # **integer kobo** (₦1.00 = 100). `250_000` is ₦2,500 — not ₦250,000.
13
+ # @param interval [String] `"day"` `"week"` `"month"` `"year"`.
14
+ # @param interval_count [Integer] bill every N intervals (default 1).
15
+ # @param usage_type [String] `"licensed"` (default) or `"metered"`.
16
+ # @param billing_scheme [String] `"per_unit"` (default) or `"tiered"`.
17
+ # @param trial_period_days [Integer] free-trial days at subscribe time.
18
+ # @param metadata [Hash]
19
+ # @param request_options [Hash]
20
+ # @return [NombaObject]
21
+ #
22
+ # @example
23
+ # price = nombaone.plans.prices.create(plan.id, unit_amount_in_kobo: 250_000, interval: "month")
24
+ def create(plan_id, unit_amount_in_kobo:, interval:, interval_count: OMIT, usage_type: OMIT,
25
+ billing_scheme: OMIT, trial_period_days: OMIT, metadata: OMIT, request_options: {})
26
+ request(:post, "/plans/#{encode(plan_id)}/prices",
27
+ body: {
28
+ unit_amount_in_kobo: unit_amount_in_kobo,
29
+ interval: interval,
30
+ interval_count: interval_count,
31
+ usage_type: usage_type,
32
+ billing_scheme: billing_scheme,
33
+ trial_period_days: trial_period_days,
34
+ metadata: metadata,
35
+ },
36
+ options: request_options)
37
+ end
38
+
39
+ # List a plan's prices, newest first.
40
+ #
41
+ # @param plan_id [String] `nbo…pln`
42
+ # @param limit [Integer] page size, 1–100 (API default 20).
43
+ # @param cursor [String] opaque cursor from a previous page.
44
+ # @param request_options [Hash]
45
+ # @return [Page<NombaObject>]
46
+ def list(plan_id, limit: OMIT, cursor: OMIT, request_options: {})
47
+ request_page("/plans/#{encode(plan_id)}/prices",
48
+ query: { limit: limit, cursor: cursor }, options: request_options)
49
+ end
50
+ end
51
+
52
+ # Plans — your catalog. A plan holds the name and description; its prices
53
+ # (amount + cadence) live underneath it (`nombaone.plans.prices`).
54
+ #
55
+ # @example
56
+ # plan = nombaone.plans.create(name: "Pro")
57
+ # price = nombaone.plans.prices.create(plan.id, unit_amount_in_kobo: 250_000, interval: "month")
58
+ class Plans < BaseResource
59
+ # Prices nested under a plan.
60
+ # @return [PlanPrices]
61
+ def prices
62
+ @prices ||= PlanPrices.new(@client)
63
+ end
64
+
65
+ # Create a plan.
66
+ #
67
+ # @param name [String] unique within your organization (`PLAN_NAME_TAKEN` on reuse).
68
+ # @param description [String]
69
+ # @param metadata [Hash]
70
+ # @param request_options [Hash]
71
+ # @return [NombaObject]
72
+ # @raise [Nombaone::ConflictError] 409 `PLAN_NAME_TAKEN`
73
+ def create(name:, description: OMIT, metadata: OMIT, request_options: {})
74
+ request(:post, "/plans",
75
+ body: { name: name, description: description, metadata: metadata },
76
+ options: request_options)
77
+ end
78
+
79
+ # Retrieve a plan by id.
80
+ #
81
+ # @param id [String] `nbo…pln`
82
+ # @param request_options [Hash]
83
+ # @return [NombaObject]
84
+ # @raise [Nombaone::NotFoundError] 404 `PLAN_NOT_FOUND`
85
+ def retrieve(id, request_options: {})
86
+ request(:get, "/plans/#{encode(id)}", options: request_options)
87
+ end
88
+
89
+ # Update a plan's mutable fields. At least one field is required.
90
+ #
91
+ # @param id [String] `nbo…pln`
92
+ # @param name [String]
93
+ # @param description [String, nil] pass `nil` to clear the description.
94
+ # @param metadata [Hash]
95
+ # @param request_options [Hash]
96
+ # @return [NombaObject]
97
+ def update(id, name: OMIT, description: OMIT, metadata: OMIT, request_options: {})
98
+ request(:patch, "/plans/#{encode(id)}",
99
+ body: { name: name, description: description, metadata: metadata },
100
+ options: request_options)
101
+ end
102
+
103
+ # List plans, newest first.
104
+ #
105
+ # @param status [String] `"active"` or `"archived"`.
106
+ # @param limit [Integer] page size, 1–100 (API default 20).
107
+ # @param cursor [String] opaque cursor from a previous page.
108
+ # @param request_options [Hash]
109
+ # @return [Page<NombaObject>]
110
+ def list(status: OMIT, limit: OMIT, cursor: OMIT, request_options: {})
111
+ request_page("/plans",
112
+ query: { status: status, limit: limit, cursor: cursor },
113
+ options: request_options)
114
+ end
115
+
116
+ # Archive a plan — it stops being subscribable but its history stays.
117
+ #
118
+ # @param id [String] `nbo…pln`
119
+ # @param request_options [Hash]
120
+ # @return [NombaObject]
121
+ # @raise [Nombaone::ConflictError] 409 `PLAN_ALREADY_ARCHIVED`
122
+ # @raise [Nombaone::ConflictError] 409 `PLAN_HAS_ACTIVE_SUBSCRIBERS` —
123
+ # migrate or cancel those subscriptions first.
124
+ def archive(id, request_options: {})
125
+ request(:post, "/plans/#{encode(id)}/archive", body: {}, options: request_options)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Prices — the amounts and cadences plans are sold at. Create and list them
6
+ # under a plan via `nombaone.plans.prices`; this namespace reads and
7
+ # deactivates them directly.
8
+ #
9
+ # Prices are **immutable** once created — to change pricing, create a new
10
+ # price and deactivate the old one. Existing subscriptions keep the price
11
+ # they were sold at.
12
+ class Prices < BaseResource
13
+ # Retrieve a price by id.
14
+ #
15
+ # @param id [String] `nbo…prc`
16
+ # @param request_options [Hash]
17
+ # @return [NombaObject]
18
+ # @raise [Nombaone::NotFoundError] 404 `PRICE_NOT_FOUND`
19
+ def retrieve(id, request_options: {})
20
+ request(:get, "/prices/#{encode(id)}", options: request_options)
21
+ end
22
+
23
+ # List prices across all plans, newest first.
24
+ #
25
+ # @param plan_ref [String] filter to one plan's prices (`nbo…pln`). Note
26
+ # the wire filter name is `planRef`.
27
+ # @param active [Boolean] filter by the active flag.
28
+ # @param limit [Integer] page size, 1–100 (API default 20).
29
+ # @param cursor [String] opaque cursor from a previous page.
30
+ # @param request_options [Hash]
31
+ # @return [Page<NombaObject>]
32
+ #
33
+ # @example
34
+ # page = nombaone.prices.list(plan_ref: plan.id, active: true)
35
+ def list(plan_ref: OMIT, active: OMIT, limit: OMIT, cursor: OMIT, request_options: {})
36
+ request_page("/prices",
37
+ query: { plan_ref: plan_ref, active: active, limit: limit, cursor: cursor },
38
+ options: request_options)
39
+ end
40
+
41
+ # Deactivate a price so no new subscriptions can be created against it.
42
+ # Existing subscriptions are unaffected — prices are immutable history.
43
+ #
44
+ # @param id [String] `nbo…prc`
45
+ # @param request_options [Hash]
46
+ # @return [NombaObject]
47
+ # @raise [Nombaone::ConflictError] 409 `PRICE_ALREADY_INACTIVE`
48
+ def deactivate(id, request_options: {})
49
+ request(:post, "/prices/#{encode(id)}/deactivate", body: {}, options: request_options)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # **Sandbox only.** Simulation instruments that make billing outcomes happen
6
+ # on demand — no cron waits, no real cards. These endpoints exist only on
7
+ # the sandbox deployment; calling any of them with a live key raises
8
+ # locally, **before any network request**.
9
+ class Sandbox < BaseResource
10
+ # **Sandbox only.** Mint a ready, chargeable test payment method whose
11
+ # `behavior` decides every future charge outcome deterministically.
12
+ #
13
+ # @param customer_id [String] `nbo…cus`
14
+ # @param behavior [String] `"success"` (default), `"decline_insufficient_funds"`,
15
+ # `"decline_expired_card"`, `"decline_do_not_honor"`, or `"requires_otp"`.
16
+ # @param kind [String] `"card"` (default) or `"mandate"`.
17
+ # @param request_options [Hash]
18
+ # @return [NombaObject] a payment method.
19
+ #
20
+ # @example
21
+ # method = nombaone.sandbox.create_payment_method(
22
+ # customer_id: customer.id, behavior: "decline_insufficient_funds"
23
+ # )
24
+ def create_payment_method(customer_id:, behavior: OMIT, kind: OMIT, request_options: {})
25
+ assert_sandbox!
26
+ request(:post, "/sandbox/payment-methods",
27
+ body: { customer_id: customer_id, behavior: behavior, kind: kind },
28
+ options: request_options)
29
+ end
30
+
31
+ # **Sandbox only.** The test clock: run the subscription's next billing
32
+ # cycle right now, through the real engine — invoice, charge, ledger,
33
+ # webhooks and all.
34
+ #
35
+ # @param subscription_id [String] `nbo…sub`
36
+ # @param request_options [Hash]
37
+ # @return [NombaObject] the cycle `outcome` and the `invoice` it produced.
38
+ #
39
+ # @example
40
+ # result = nombaone.sandbox.advance_cycle(subscription.id)
41
+ # result.outcome # => "paid"
42
+ def advance_cycle(subscription_id, request_options: {})
43
+ assert_sandbox!
44
+ request(:post, "/sandbox/subscriptions/#{encode(subscription_id)}/advance-cycle",
45
+ body: {}, options: request_options)
46
+ end
47
+
48
+ # **Sandbox only.** Emit a real, signed catalog event to your registered
49
+ # endpoints — the genuine pipeline (real secret, real signature, real
50
+ # retries), not a mock. This is how you rehearse your handler.
51
+ #
52
+ # @param type [String] any catalog event type, e.g. `"invoice.payment_failed"`.
53
+ # @param payload [Hash] shapes the delivery's `data` object.
54
+ # @param request_options [Hash]
55
+ # @return [NombaObject]
56
+ #
57
+ # @example
58
+ # nombaone.sandbox.simulate_webhook(
59
+ # type: "invoice.payment_failed",
60
+ # payload: { reference: invoice.id, reason: "insufficient_funds" }
61
+ # )
62
+ def simulate_webhook(type:, payload: OMIT, request_options: {})
63
+ assert_sandbox!
64
+ request(:post, "/sandbox/webhooks/simulate",
65
+ body: { type: type, payload: payload }, options: request_options)
66
+ end
67
+
68
+ private
69
+
70
+ # Fail locally, before any network call, when constructed with a live key.
71
+ def assert_sandbox!
72
+ return unless @client.mode == "live"
73
+
74
+ raise Error,
75
+ "nombaone.sandbox.* only works with a sandbox key (nbo_sandbox_…) — the " \
76
+ "/v1/sandbox endpoints do not exist on the live API. Use your sandbox key to " \
77
+ "rehearse, then go live without the sandbox calls."
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nombaone
4
+ module Resources
5
+ # Settlements — where collected money lands, and how it leaves (refunds,
6
+ # payouts) under the escrow lock. Collections split into a non-refundable
7
+ # platform fee plus the net to your tenant sub-account.
8
+ class Settlements < BaseResource
9
+ # Retrieve a settlement by id.
10
+ #
11
+ # @param id [String] `nbo…stl`
12
+ # @param request_options [Hash]
13
+ # @return [NombaObject]
14
+ # @raise [Nombaone::NotFoundError] 404 `SETTLEMENT_NOT_FOUND`
15
+ def retrieve(id, request_options: {})
16
+ request(:get, "/settlements/#{encode(id)}", options: request_options)
17
+ end
18
+
19
+ # List settlements, newest first.
20
+ #
21
+ # @param status [String] filter by settlement status.
22
+ # @param limit [Integer] page size, 1–100 (API default 20).
23
+ # @param cursor [String] opaque cursor from a previous page.
24
+ # @param request_options [Hash]
25
+ # @return [Page<NombaObject>]
26
+ def list(status: OMIT, limit: OMIT, cursor: OMIT, request_options: {})
27
+ request_page("/settlements",
28
+ query: { status: status, limit: limit, cursor: cursor },
29
+ options: request_options)
30
+ end
31
+
32
+ # Your escrow lock and available-to-withdraw balance.
33
+ #
34
+ # @param request_options [Hash]
35
+ # @return [NombaObject]
36
+ def retrieve_escrow(request_options: {})
37
+ request(:get, "/settlements/escrow", options: request_options)
38
+ end
39
+
40
+ # Refund a settlement's tenant share. The platform fee is never refunded.
41
+ #
42
+ # **Money moves here.** The API requires an `Idempotency-Key`; the SDK
43
+ # sends one automatically, but pass your own stable
44
+ # `request_options[:idempotency_key]` so a retry from a *new process*
45
+ # cannot refund twice.
46
+ #
47
+ # @param id [String] `nbo…stl`
48
+ # @param amount_in_kobo [Integer] **integer kobo**; defaults server-side to
49
+ # the full remaining refundable amount.
50
+ # @param request_options [Hash]
51
+ # @return [NombaObject]
52
+ # @raise [Nombaone::ConflictError] 409 `REFUND_ALREADY_REFUNDED`
53
+ # @raise [Nombaone::ValidationError] 422 `REFUND_AMOUNT_EXCEEDS_NET`
54
+ def refund(id, amount_in_kobo: OMIT, request_options: {})
55
+ request(:post, "/settlements/#{encode(id)}/refund",
56
+ body: { amount_in_kobo: amount_in_kobo }, options: request_options)
57
+ end
58
+
59
+ # Withdraw settled funds to your bank account.
60
+ #
61
+ # **Money moves here, and the `Idempotency-Key` doubles as the payout's
62
+ # durable `merchantTxRef`.** Always pass an explicit, stable
63
+ # `request_options[:idempotency_key]` (e.g. your own payout id) — an
64
+ # auto-generated key protects SDK-level retries, but a brand-new process
65
+ # retrying with a fresh key would create a **second** payout.
66
+ #
67
+ # @param amount_in_kobo [Integer] **integer kobo** (₦1.00 = 100).
68
+ # @param bank_code [String] CBN 3-digit bank code.
69
+ # @param account_number [String]
70
+ # @param request_options [Hash]
71
+ # @return [NombaObject]
72
+ # @raise [Nombaone::ConflictError] 409 `ESCROW_LOCKED`
73
+ # @raise [Nombaone::ValidationError] 422 `PAYOUT_EXCEEDS_AVAILABLE`
74
+ #
75
+ # @example
76
+ # payout = nombaone.settlements.create_payout(
77
+ # amount_in_kobo: 5_000_000, bank_code: "058", account_number: "0123456789",
78
+ # request_options: { idempotency_key: "payout-#{my_payout_row.id}" }
79
+ # )
80
+ def create_payout(amount_in_kobo:, bank_code:, account_number:, request_options: {})
81
+ request(:post, "/settlements/payout",
82
+ body: {
83
+ amount_in_kobo: amount_in_kobo,
84
+ bank_code: bank_code,
85
+ account_number: account_number,
86
+ },
87
+ options: request_options)
88
+ end
89
+ end
90
+ end
91
+ end