paystack_sdk 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d2f7a168f41486d2df726ca96f7f6ff1864e4f66b46a019a3894385c5a74a16
4
- data.tar.gz: d2f63d9282c6fd568c8e9c16d7b91bbb65fe874fea489420af92f0336f9431e2
3
+ metadata.gz: 02712537eff419059ff52364f7f6077b8b40fd171a2f29326d74e2475aa28482
4
+ data.tar.gz: cf8537555639790e816ade12badb9e0ba177c5c76f9bb2142d70d81c3dfb8540
5
5
  SHA512:
6
- metadata.gz: 9470f85d1e68ec70caa4b50e02c3ab001eef9490d2f562294c1067316ca12b43c6f6f74e876750b1ef8da6d2f88c88b56f0d2eeb234e95d92162fd4e09afd6ec
7
- data.tar.gz: 92d8ddbc6768188517ad0dc39b36b64adb0faac6c13fa4a3c1b8bd29f6c8977f028b18c3ef892bd7e483c801eed68783a4e35607acf2cbd51fb3ac349cb1a70f
6
+ metadata.gz: 7d833404efed23211c319b6b629c3158e6b1013538fb32648d76fe1fef445c79291b8c01c4291054b98e39cc71c0364060b2a0fcbd10b2673cc58b22a7bc5fd0
7
+ data.tar.gz: fc869da180ae8333a892b46e72768de8f85daa3b9b53bef1ad9a2893dc2d8fe453ffe227a4acb8e14afcda6ba750a59d24049cef99bb9630b1199d8933f8e916
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.0] - 2026-10-10
6
+
7
+ ### Upgrading from 0.4
8
+
9
+ Two behaviours changed in small ways. Check them if you use these calls:
10
+
11
+ - `transactions.initiate` sends `metadata` stringified, as Paystack documents it, so numbers in it keep their type when you read them back (`{count: 42}` stays `42`; it used to come back as `"42"`).
12
+ - Filters that take a numeric ID (`customer_id:`, `plan_id:`, `transaction_id:` and `authorization_id:`) raise `InvalidValueError` for anything but a numeric ID. Paystack used to answer a code such as `CUS_abc123` there with an empty list and no error, so if you passed one you were silently getting nothing.
13
+
14
+ If you are on 0.4.0, take 0.4.1 first if you only need the security fix.
15
+
16
+ ### Added
17
+
18
+ - `Response#dig(*keys)` reads a nested value and returns `nil` as soon as a key is missing, for fields Paystack only sometimes sends (`response.dig(:authorization, :authorization_code)`). Dot access still raises `NoMethodError` for a key that is not there, which catches typos.
19
+ - The `paystack-sdk-overview` skill now has a "Topic skills" table that routes an agent to the right skill for the task, and the skills link to each other with `[[name]]`. A spec fails if a shipped skill is not reachable from the overview.
20
+ - `client.webhook_events` (Paystack's Webhook Events API: `list`, `lookup`, `fetch`, `resend`, `resend_matching`), the log of the webhooks Paystack sent you with their delivery status, the payload and what your endpoint answered. It is documented by Paystack but not in its OpenAPI spec, so it is hand-written and recorded in `spec/fixtures/docs_only_operations.yml`; `bin/paystack-spec audit` and the contract checker read that file. `list`, `lookup` and `fetch` were checked against the test API; `resend` and `resend_matching` were not (they deliver webhooks to your endpoint), and `resend_matching` requires `preview:`.
21
+ - `Webhook::EVENTS` also lists `refund.needs-attention` (named in Paystack's Refunds guide) and `paymentrequest.draft`, `product.create`, `product.update` and `product.delete`, which Paystack was seen sending but its Webhooks page does not list.
22
+ - Skills `paystack-sdk-charge-statuses` (what each charge `status` means, what to show the payer, which `charges.*` call comes next, and why `success?` is not "the charge worked"; the flows were reproduced on the test API with Paystack's test cards) and `paystack-sdk-mobile-money` (Ghana providers and codes, the phone number form, the 180-second approval window, how the result arrives, and what test mode does not prove). Specs run what they say.
23
+ - Skill `paystack-sdk-webhooks`: receiving Paystack webhooks in Rails or Rack with `PaystackSdk::Webhook` (signature over the raw body, `verify!` vs `valid_signature?` vs `construct_event` and their errors, CSRF, the optional IP allow-list, answering 200 fast, the documented retry policy, duplicate and out-of-order deliveries, verifying by reference before granting value, the documented events, and testing with a signed request). Its Ruby is run by `spec/skills_webhooks_spec.rb`; payload fields beyond Paystack's one documented sample are marked unverified.
24
+ - Skill `paystack-sdk-refunds`: full and partial refunds, the pending-until-processed lifecycle, what to record, refund webhooks, and what to do after a timeout. Includes what the test API showed (a GHS 1.00 minimum per refund, an omitted `amount` asks for the whole original amount and fails after a partial refund, a refunded transaction verifies as `reversal-pending`) and marks the rest unverified.
25
+ - Skill `paystack-sdk-money-safety`: the checklist for any code that moves or records money (integer minor-unit amounts, verifying by reference with `paid?(amount:, currency:)` and the customer email before granting value, never resending a write after a timeout, reference discipline, storing only the encrypted authorization code, keeping keys out of logs, `sandbox_only` limits, webhook and reconciliation rules) with a pre-merge table. Its claims are run by `spec/skills_money_safety_spec.rb` or were observed on the Paystack test API.
26
+ - Skill `paystack-sdk-testing`: keys for test, staging and CI (`sandbox_only`, `live?` and what they do not check), a copyable WebMock helper with success, 400, 401, 429, 500 and timeout stubs (each run through the SDK in the gem's specs), an opt-in test-API suite pattern, Paystack's published test cards and the MTN test number, testing a webhook endpoint with `Webhook.sign`, and what test mode cannot prove.
27
+ - Skill `paystack-sdk-saved-card-renewals`: charging a returning customer's saved card on your own schedule with `transactions.charge_authorization`. Covers saving only `reusable` authorizations keyed to their email, the responses observed on the Paystack test API (invalid, inactive, other-email and mobile money codes, duplicate references), lookup-first renewal references and the never-retry-after-timeout rule, partial debit (refused for GHS on the test API), the reported confirmation race, mobile money having no reusable token, and dunning advice. Its helper code is run by `spec/skills_saved_card_renewals_spec.rb`.
28
+ - Skill `paystack-sdk-payments`: the hosted checkout flow with `client.transactions` (`initiate` keywords, redirecting to `authorization_url`, why the callback proves nothing, `verify` and `paid?`, what each verify status means, idempotent confirmation from the callback and the webhook as labelled design advice, which fields to store, `fetch` and `list`). Its claims are run by `spec/skills_payments_spec.rb` or were observed on Paystack's test API; the checkout page itself could not be driven and is marked unverified.
29
+ - AI skills for Claude Code, shipped in the gem and installed with `paystack_sdk skills install` (any Ruby project) or `rails generate paystack_sdk:skills` (Rails) into `.claude/skills/`. The first skill is `paystack-sdk-overview`. Installing is idempotent, writes only `paystack-sdk-<topic>` folders marked with `.paystack_sdk.json`, never overwrites a folder it did not install (`--force` to replace), removes only its own stale folders, stamps each `SKILL.md` with the gem version, and supports `--dir`, `--global` and `--dry-run`. Also `skills list`, `skills path` and `skills uninstall`.
30
+ - The `paystack_sdk` executable (`exe/paystack_sdk`); it handles the skills and `version` and does not load Faraday.
31
+
32
+ ### Fixed
33
+
34
+ - `transactions.initiate` now sends `metadata` stringified, as `charge_authorization` already did and as Paystack documents it. Paystack accepts an object too, but stores it differently: a nested number comes back as a string (`{count: 42}` as `{"count" => "42"}`), whereas a stringified object keeps it (`42`). Checked against the test API. If you read numbers back out of `metadata`, they are now numbers.
35
+ - Filters that take a numeric ID (`customer_id:` on `transactions.list`, `transactions.export`, `subscriptions.list` and `payment_requests.list`, `plan_id:` on `subscriptions.list`, `transaction_id:` on `refunds.list`, `authorization_id:` on `customers.direct_debit_activation_charge`) now refuse anything but a numeric ID (an Integer, or a string of digits) with `InvalidValueError`. Paystack answers a code such as `CUS_abc123` there with an empty list and no error, which looks like "no results". If you passed a code to one of these, you were silently getting nothing.
36
+ - Passing a String date or date-time to a `from:` or `to:` filter (`transactions.list(from: "2026-10-01")`, and every other date filter) raised `NoMethodError: undefined method 'iso8601' for class Time` in a plain Ruby process, because the gem never loaded Ruby's `time` library. It only worked where something else had loaded it, such as Rails or RSpec. The gem now requires it. A Date or Time object was never affected.
37
+
5
38
  ## [0.4.1] - 2026-10-10
6
39
 
7
40
  ### Security
data/README.md CHANGED
@@ -4,6 +4,7 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [AI Skills](#ai-skills)
7
8
  - [Installation](#installation)
8
9
  - [Quick Start](#quick-start)
9
10
  - [Usage](#usage)
@@ -100,8 +101,10 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
100
101
  - [List and Fetch Terminals](#list-and-fetch-terminals)
101
102
  - [Send an Event and Check It](#send-an-event-and-check-it)
102
103
  - [Update, Commission and Decommission](#update-commission-and-decommission)
104
+ - [Webhook Events](#webhook-events)
103
105
  - [Response Handling](#response-handling)
104
106
  - [Working with Response Objects](#working-with-response-objects)
107
+ - [Optional Fields: `dig`](#optional-fields-dig)
105
108
  - [Accessing the Original Response](#accessing-the-original-response)
106
109
  - [Error Handling](#error-handling)
107
110
  - [Advanced Usage](#advanced-usage)
@@ -115,6 +118,25 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
115
118
  - [License](#license)
116
119
  - [Code of Conduct](#code-of-conduct)
117
120
 
121
+ ## AI Skills
122
+
123
+ The gem ships skills for AI coding agents (Claude Code `SKILL.md` folders) that teach an agent how to use `paystack_sdk` correctly: the conventions, what raises and what returns, and the checks that keep money safe. They are installed from the gem you have, so they match its version.
124
+
125
+ ```sh
126
+ # Any Ruby project: installs into ./.claude/skills
127
+ bundle exec paystack_sdk skills install
128
+
129
+ # A Rails app
130
+ bin/rails generate paystack_sdk:skills
131
+ ```
132
+
133
+ - **Safe to repeat.** A second run reports `identical`. After a gem upgrade, run it again: it updates only the folders it installed.
134
+ - **Never overwrites your skills.** Every folder it writes is named `paystack-sdk-<topic>` and carries a `.paystack_sdk.json` marker. A folder with the same name that it did not install is left alone and reported as a `conflict` (`--force` replaces it). It removes only its own folders that a newer gem no longer ships.
135
+ - **Version-stamped.** Each installed `SKILL.md` records the gem version in its frontmatter (`metadata.gem_version`).
136
+ - Options: `--dir DIR` (another directory), `--global` (`~/.claude/skills`), `--dry-run` (change nothing; `rails generate ... --pretend` in Rails). Other commands: `paystack_sdk skills list`, `skills path` (where the skills live inside the gem, for agents that read files directly), `skills uninstall`, `paystack_sdk version`.
137
+
138
+ Run `paystack_sdk skills list` for the skills in your installed version and when each one applies. Each is a `paystack-sdk-<topic>` folder; an agent picks them from their descriptions.
139
+
118
140
  ## Installation
119
141
 
120
142
  Add this line to your application's Gemfile:
@@ -1763,6 +1785,37 @@ paystack.terminals.commission(serial_number: "1111150412230003899")
1763
1785
  paystack.terminals.decommission(serial_number: "1111150412230003899")
1764
1786
  ```
1765
1787
 
1788
+ ### Webhook Events
1789
+
1790
+ The Webhook Events API is the log of every webhook Paystack tried to send to your integration's webhook URL: whether it was delivered, what Paystack sent, and what your endpoint answered. It is documented by Paystack but is not in its OpenAPI spec, so `client.webhook_events` is written from the docs (the entry in `spec/fixtures/docs_only_operations.yml` records what was checked). Use it to find out why a webhook never arrived, to see the exact payload, and to replay events your endpoint missed.
1791
+
1792
+ ```ruby
1793
+ # Events Paystack could not deliver (status is Delivered, Pending or Failed)
1794
+ response = paystack.webhook_events.list(status: "Failed", limit: 20)
1795
+
1796
+ response.each do |event|
1797
+ puts "#{event.event_name} #{event.status} (#{event.status_detail}) answered #{event.response_code}"
1798
+ end
1799
+
1800
+ # The next page: pass meta.next back as next_cursor (not together with previous)
1801
+ paystack.webhook_events.list(next_cursor: response.original_response.dig("meta", "next"))
1802
+
1803
+ # Filters: category, event_type, status, category_row_id, from, to
1804
+ paystack.webhook_events.list(event_type: "charge.success", from: Date.new(2025, 6, 1))
1805
+
1806
+ # One event in full: the payload Paystack sent, and your endpoint's answer
1807
+ event = paystack.webhook_events.fetch(id: "6ac94b47fdad55440ef2066a")
1808
+ event.event_payload[:data] # brackets: a key named data is not reachable with dot access
1809
+ event.merchant_response_body
1810
+
1811
+ # Find an event by its own id, or by the id of the resource it is about (a transaction id)
1812
+ paystack.webhook_events.lookup(id: 6_641_907_106)
1813
+ ```
1814
+
1815
+ Checked against Paystack's test API: `list`, `lookup` and `fetch` (read-only). A page holds 50 events by default (the docs say 20) and `limit` is capped at 50. Every event has a `_id`; the payloads Paystack records are `{"event": ..., "data": {...}}` with no event id inside, and `data.id` was present in every payload seen.
1816
+
1817
+ **Resending is unverified and makes Paystack deliver webhooks to your endpoint.** `resend(ids:)` and `resend_matching(preview:, filters:)` were never called: the docs say resending is not idempotent, so your endpoint must dedupe. `resend_matching` makes you choose `preview:` (the docs make it optional) because with no filters it resends everything. Call it with `preview: true` first.
1818
+
1766
1819
  ### Response Handling
1767
1820
 
1768
1821
  #### Working with Response Objects
@@ -1820,6 +1873,21 @@ response.meta.perPage # => 20
1820
1873
 
1821
1874
  `response.meta` is `nil` when the response has no `meta`.
1822
1875
 
1876
+ #### Optional Fields: `dig`
1877
+
1878
+ Dot access raises `NoMethodError` for a key that is not in the response. That is what you want for a typo, but not for a field Paystack only sometimes sends (`paid_at` on an unpaid transaction, `authorization` details on a payment without a card). Read those with `dig`: it returns `nil` as soon as a key along the path is missing, accepts strings or symbols (and an Integer to index an Array), and returns the value as it is, like `Hash#dig`.
1879
+
1880
+ ```ruby
1881
+ response = paystack.transactions.verify(reference: "order-1042")
1882
+
1883
+ response.dig(:paid_at) # => "2025-06-01T10:00:00.000Z" or nil
1884
+ response.dig(:authorization, :authorization_code) # => "AUTH_xxxx" or nil
1885
+ response.dig(:customer, :email) # => "ama@example.com"
1886
+ response.dig(:log, :history, 0, :message) # an Integer indexes an Array
1887
+ ```
1888
+
1889
+ `response[:key]` also gives `nil` for a missing key, but wraps what it finds in a `Response`; `dig` gives you the plain value.
1890
+
1823
1891
  #### Accessing the Original Response
1824
1892
 
1825
1893
  Sometimes you may need access to the original API response:
data/exe/paystack_sdk ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/paystack_sdk/cli"
5
+
6
+ exit PaystackSdk::CLI.run(ARGV)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "paystack_sdk/skills"
5
+
6
+ module PaystackSdk
7
+ module Generators
8
+ # `rails generate paystack_sdk:skills`: installs the gem's AI skills into the app's `.claude/skills/`.
9
+ # It never overwrites a skill folder it did not install, and after a gem upgrade it updates only its
10
+ # own folders. `--pretend` shows what would happen.
11
+ class SkillsGenerator < Rails::Generators::Base
12
+ desc "Install the paystack_sdk AI skills into .claude/skills/ (Claude Code)"
13
+
14
+ class_option :force, type: :boolean, default: false, desc: "Replace a same-named folder paystack_sdk did not install"
15
+
16
+ COLORS = {"create" => :green, "update" => :yellow, "force" => :yellow, "remove" => :red, "conflict" => :red}.freeze
17
+
18
+ def install_skills
19
+ dir = File.join(destination_root, ".claude", "skills")
20
+ results = PaystackSdk::Skills.install(dir, dry_run: options[:pretend], force: options[:force])
21
+
22
+ results.each { |r| say_status(r.status, [r.name, r.note].compact.join(": "), COLORS.fetch(r.status, :blue)) }
23
+ say_status("note", "Some skill folders were left alone; run with --force to replace them.", :red) if results.any? { |r| r.status == "conflict" }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "skills"
5
+ require_relative "version"
6
+
7
+ module PaystackSdk
8
+ # The `paystack_sdk` command. It only handles the AI skills; it does not load Faraday or call Paystack.
9
+ #
10
+ # paystack_sdk skills install [--dir DIR | --global] [--dry-run] [--force]
11
+ # paystack_sdk skills list
12
+ # paystack_sdk skills path
13
+ # paystack_sdk skills uninstall [--dir DIR | --global] [--dry-run]
14
+ # paystack_sdk version
15
+ class CLI
16
+ USAGE = <<~TEXT
17
+ Usage:
18
+ paystack_sdk skills install [--dir DIR | --global] [--dry-run] [--force]
19
+ paystack_sdk skills uninstall [--dir DIR | --global] [--dry-run]
20
+ paystack_sdk skills list
21
+ paystack_sdk skills path
22
+ paystack_sdk version
23
+
24
+ `skills install` copies the AI skills that ship with this gem into .claude/skills/ (Claude Code),
25
+ ready for an agent to load. It is safe to repeat, never overwrites a skill folder it did not
26
+ install, and after a gem upgrade updates only its own folders.
27
+
28
+ Options:
29
+ --dir DIR install into DIR instead of ./.claude/skills
30
+ --global install into ~/.claude/skills
31
+ --dry-run say what would happen and change nothing
32
+ --force replace a same-named folder that paystack_sdk did not install
33
+ TEXT
34
+
35
+ # Runs the command.
36
+ #
37
+ # @param argv [Array<String>]
38
+ # @param out [IO]
39
+ # @param err [IO]
40
+ # @return [Integer] the exit status
41
+ def self.run(argv, out: $stdout, err: $stderr)
42
+ new(out: out, err: err).run(argv.dup)
43
+ end
44
+
45
+ def initialize(out:, err:)
46
+ @out = out
47
+ @err = err
48
+ end
49
+
50
+ def run(argv)
51
+ command = argv.shift
52
+ case command
53
+ when "skills" then skills(argv)
54
+ when "version", "--version", "-v" then @out.puts("paystack_sdk #{VERSION}").then { 0 }
55
+ when nil, "help", "--help", "-h" then @out.puts(USAGE).then { 0 }
56
+ else fail_with("unknown command #{command.inspect}")
57
+ end
58
+ rescue OptionParser::ParseError => e
59
+ fail_with(e.message)
60
+ end
61
+
62
+ private
63
+
64
+ def skills(argv)
65
+ sub = argv.shift
66
+ case sub
67
+ when "install" then install(argv)
68
+ when "uninstall" then uninstall(argv)
69
+ when "list" then list
70
+ when "path" then @out.puts(Skills::SOURCE_DIR).then { 0 }
71
+ else fail_with("unknown skills command #{sub.inspect}")
72
+ end
73
+ end
74
+
75
+ def install(argv)
76
+ options = parse(argv, force: true)
77
+ dir = target(options)
78
+ results = Skills.install(dir, dry_run: options[:dry_run], force: options[:force])
79
+ report(results, dir, options)
80
+ (results.any? { |r| r.status == "conflict" }) ? 1 : 0
81
+ end
82
+
83
+ def uninstall(argv)
84
+ options = parse(argv, force: false)
85
+ dir = target(options)
86
+ results = Skills.uninstall(dir, dry_run: options[:dry_run])
87
+ report(results, dir, options)
88
+ 0
89
+ end
90
+
91
+ def list
92
+ Skills.all.each { |skill| @out.puts("#{skill.name}\n #{skill.description}") }
93
+ 0
94
+ end
95
+
96
+ def parse(argv, force:)
97
+ options = {}
98
+ OptionParser.new do |o|
99
+ o.on("--dir DIR") { |v| options[:dir] = v }
100
+ o.on("--global") { options[:global] = true }
101
+ o.on("--dry-run") { options[:dry_run] = true }
102
+ o.on("--force") { options[:force] = true } if force
103
+ end.parse!(argv)
104
+ raise OptionParser::InvalidOption, argv.first unless argv.empty?
105
+ raise OptionParser::InvalidOption, "--dir and --global together" if options[:dir] && options[:global]
106
+
107
+ options
108
+ end
109
+
110
+ def target(options)
111
+ return File.expand_path(options[:dir]) if options[:dir]
112
+ return File.join(Dir.home, ".claude", "skills") if options[:global]
113
+
114
+ File.join(Dir.pwd, ".claude", "skills")
115
+ end
116
+
117
+ def report(results, dir, options)
118
+ @out.puts("#{"Dry run: " if options[:dry_run]}#{dir}")
119
+ results.each do |r|
120
+ @out.puts("#{r.status.rjust(12)} #{r.name}#{" (#{r.note})" if r.note}")
121
+ end
122
+ @out.puts("\nDry run: nothing was written.") if options[:dry_run]
123
+ @out.puts("\nSome folders were left alone. Re-run with --force to replace them.") if results.any? { |r| r.status == "conflict" }
124
+ end
125
+
126
+ def fail_with(message)
127
+ @err.puts("paystack_sdk: #{message}\n\n#{USAGE}")
128
+ 1
129
+ end
130
+ end
131
+ end
@@ -27,6 +27,7 @@ require_relative "resources/integrations"
27
27
  require_relative "resources/virtual_terminals"
28
28
  require_relative "resources/direct_debits"
29
29
  require_relative "resources/terminals"
30
+ require_relative "resources/webhook_events"
30
31
  require_relative "utils/connection_utils"
31
32
 
32
33
  module PaystackSdk
@@ -344,6 +345,18 @@ module PaystackSdk
344
345
  @terminals ||= Resources::Terminals.new(@connection)
345
346
  end
346
347
 
348
+ # Provides access to the `WebhookEvents` resource: the log of webhooks Paystack sent you, with their
349
+ # delivery status, and a way to send them again. (Not in Paystack's OpenAPI spec; from its docs.)
350
+ #
351
+ # @return [PaystackSdk::Resources::WebhookEvents] An instance of the
352
+ # `WebhookEvents` resource.
353
+ #
354
+ # @example
355
+ # client.webhook_events.list(status: "Failed")
356
+ def webhook_events
357
+ @webhook_events ||= Resources::WebhookEvents.new(@connection)
358
+ end
359
+
347
360
  # A short description that never includes the secret key. Ruby's default `inspect` prints every
348
361
  # instance variable, and the connection holds the key in its `Authorization` header, so logging,
349
362
  # `pp`, an error page or an error tracker would otherwise write the key out.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
+ require "time"
4
5
  require "erb"
5
6
  require "json"
6
7
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
4
4
  # Do not edit by hand: regenerate it with `bin/paystack-scaffold Customer`. Hand-written extras go in lib/paystack_sdk/resources/extensions/customers.rb.
5
- # scaffold-digest: 24269cd1f96877a533fed260bdce25e0dc552ebe7ca1c5636362779e27358054
5
+ # scaffold-digest: f23af37740665b121f36ec65dd45f1275b9b4358e704cf85b43f6cdd3218adae
6
6
 
7
7
  require_relative "base"
8
8
 
@@ -281,6 +281,7 @@ module PaystackSdk
281
281
  def direct_debit_activation_charge(id:, authorization_id:)
282
282
  validate_presence!(value: id, name: "id")
283
283
  validate_presence!(value: authorization_id, name: "authorization_id")
284
+ validate_numeric_id!(value: authorization_id, name: "authorization_id", allow_nil: false)
284
285
 
285
286
  wire_body = to_wire({authorization_id:}, WIRE_NAMES)
286
287
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
4
4
  # Do not edit by hand: regenerate it with `bin/paystack-scaffold Payment Request`. Hand-written extras go in lib/paystack_sdk/resources/extensions/payment_requests.rb.
5
- # scaffold-digest: 884176d0fce2f23befd4655d04efd15b5dccf6808f496f41b09e0afcd43c1e73
5
+ # scaffold-digest: cfe9d7cb1a4ff04446434e6a6d3b3ca134e3b04e3b2a223d3f50e546ef6e9c3e
6
6
 
7
7
  require_relative "base"
8
8
 
@@ -40,6 +40,7 @@ module PaystackSdk
40
40
  )
41
41
  validate_positive_integer!(value: per_page, name: "perPage")
42
42
  validate_positive_integer!(value: page, name: "page")
43
+ validate_numeric_id!(value: customer_id, name: "customer")
43
44
  validate_allowed_values!(
44
45
  value: status,
45
46
  allowed_values: %w[draft pending success failed],
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
4
4
  # Do not edit by hand: regenerate it with `bin/paystack-scaffold Refund`. Hand-written extras go in lib/paystack_sdk/resources/extensions/refunds.rb.
5
- # scaffold-digest: c3b04aa2ea57cc443996cfdf4b3432aa4839b01df431c881aa247d2c97514a1b
5
+ # scaffold-digest: a937a81e9f6e991fee25d3817854912d092e7ebc140f6deffc87a3bef23bd5ba
6
6
 
7
7
  require_relative "base"
8
8
 
@@ -28,6 +28,7 @@ module PaystackSdk
28
28
  def list(per_page: nil, page: nil, from: nil, to: nil, transaction_id: nil)
29
29
  validate_positive_integer!(value: per_page, name: "perPage")
30
30
  validate_positive_integer!(value: page, name: "page")
31
+ validate_numeric_id!(value: transaction_id, name: "transaction")
31
32
 
32
33
  wire_query = to_wire(
33
34
  {
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
4
4
  # Do not edit by hand: regenerate it with `bin/paystack-scaffold Subscription`. Hand-written extras go in lib/paystack_sdk/resources/extensions/subscriptions.rb.
5
- # scaffold-digest: bf9e49fa6de4d23b295572c9c4fc3aa74477986410dd3f858145f2e4239d0444
5
+ # scaffold-digest: afa2b5d66179938b017341e73f1617480718a7b043c2b051c437fad1b209f893
6
6
 
7
7
  require_relative "base"
8
8
 
@@ -29,6 +29,8 @@ module PaystackSdk
29
29
  def list(per_page: nil, page: nil, plan_id: nil, customer_id: nil, from: nil, to: nil)
30
30
  validate_positive_integer!(value: per_page, name: "perPage")
31
31
  validate_positive_integer!(value: page, name: "page")
32
+ validate_numeric_id!(value: plan_id, name: "plan")
33
+ validate_numeric_id!(value: customer_id, name: "customer")
32
34
 
33
35
  wire_query = to_wire(
34
36
  {
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
4
4
  # Do not edit by hand: regenerate it with `bin/paystack-scaffold Transaction`. Hand-written extras go in lib/paystack_sdk/resources/extensions/transactions.rb.
5
- # scaffold-digest: c0b49a40a718c09c83eb9a496eb40d1f7cd4c1387b54efa3cfe360906be89f0e
5
+ # scaffold-digest: e2ec619d787ab4bb8a5c5c09e4c0df028f645a578b41df012aad09174ce3eca2
6
6
 
7
7
  require_relative "base"
8
8
 
@@ -31,7 +31,7 @@ module PaystackSdk
31
31
  # @param transaction_charge [String] A flat fee to charge the subaccount for a transaction.
32
32
  # @param bearer [String] The bearer of the transaction charge One of: account, subaccount.
33
33
  # @param label [String] Used to replace the email address shown on the Checkout
34
- # @param metadata [Hash] JSON object of custom data
34
+ # @param metadata [String] Custom data as a JSON object (a Hash, or a JSON string).
35
35
  # @return [PaystackSdk::Response] The response from the Paystack API.
36
36
  # @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
37
37
  # @see https://paystack.com/docs/api/transaction/#initialize
@@ -84,7 +84,7 @@ module PaystackSdk
84
84
  transaction_charge:,
85
85
  bearer:,
86
86
  label:,
87
- metadata:
87
+ metadata: stringify_json(metadata)
88
88
  },
89
89
  WIRE_NAMES
90
90
  )
@@ -283,6 +283,7 @@ module PaystackSdk
283
283
  allowed_values: %w[merchantApi checkout pos virtualTerminal],
284
284
  name: "source"
285
285
  )
286
+ validate_numeric_id!(value: customer_id, name: "customer")
286
287
  validate_positive_integer!(value: amount, name: "amount")
287
288
  validate_allowed_values!(
288
289
  value: channel,
@@ -372,7 +373,7 @@ module PaystackSdk
372
373
  # @param from [String] The start date
373
374
  # @param to [String] The end date
374
375
  # @param status [String] Filter by the status of the transaction One of: success, failed, abandoned, reversed, all.
375
- # @param customer_id [Numeric] Filter by customer ID
376
+ # @param customer_id [Integer] Filter by customer ID
376
377
  # @param subaccount_code [String] Filter by subaccount code
377
378
  # @param settlement [Integer] Filter by the settlement ID
378
379
  # @param currency [String] Specify the transaction currency to export.
@@ -399,6 +400,7 @@ module PaystackSdk
399
400
  allowed_values: %w[success failed abandoned reversed all],
400
401
  name: "status"
401
402
  )
403
+ validate_numeric_id!(value: customer_id, name: "customer")
402
404
  validate_positive_integer!(value: amount, name: "amount")
403
405
 
404
406
  wire_query = to_wire(