Skip to content

Add Mobile Money channel support via Charges resource#1

Merged
nanafox merged 3 commits intonanafox:mainfrom
smorttey:main
Sep 22, 2025
Merged

Add Mobile Money channel support via Charges resource#1
nanafox merged 3 commits intonanafox:mainfrom
smorttey:main

Conversation

@smorttey
Copy link
Copy Markdown
Contributor

@smorttey smorttey commented Sep 21, 2025

Summary

  • Introduces a first-class Charges resource to support Mobile Money payments using Paystack’s Charges API.
  • Covers initiation (/charge) and OTP submission (/charge/submit_otp) for provider voucher flows.

Details

New Resource

  • File: lib/paystack_sdk/resources/charges.rb
  • Methods:
    • mobile_money(payload): posts to /charge with email, amount, currency, and mobile_money (phone, provider).
    • submit_otp(payload): posts to /charge/submit_otp with reference and otp.

Validations

  • Requires: email, amount, mobile_money.
  • Optional: currency, reference.
  • mobile_money hash requires phone and a supported provider.
  • Supported providers: mtn, atl, vod, mpesa, orange, wave (case-insensitive).

Client Wiring

client.charges accessor in lib/paystack_sdk/client.rb.

Specs

  • spec/resources/charges_spec.rb → initiation, provider validation, OTP.
  • spec/client_spec.rb → updated to cover client.charges.

Usage

Initiate

client.charges.mobile_money(
  email: "customer@email.com",
  amount: 10000,
  currency: "GHS",
  mobile_money: { phone: "0551234987", provider: "mtn" }
)

OTP (e.g., Vodafone voucher)

client.charges.submit_otp(
  otp: "123456",
  reference: "r13havfcdt7btcm"
)

Verify Final Status

client.transactions.verify(reference: "r13havfcdt7btcm")

Tests

  • Run all tests:
bundle exec rspec
  • Targeted (charges only):
bundle exec rspec spec/resources/charges_spec.rb

Non-goals

  • Webhook handling remains application responsibility.
  • Other Charges submit steps (submit_pin, submit_phone, etc.) not included here; can be added if needed.

Notes

  • No breaking changes.
  • Docs: consider updating README with Mobile Money examples and supported providers.

Refactor

  • Code was refactored with Rubocop to standardize string delimiters and improve readability.
  • No functional changes, purely formatting improvements.

Summary by CodeRabbit

  • New Features

    • Added Charges API access via the client.
    • Introduced Mobile Money charges with provider validation and OTP submission support; provider codes accepted case-insensitively.
  • Improvements

    • More consistent and early-return input validations with clearer error handling.
    • Minor console and style cleanups.
  • Tests

    • Added comprehensive tests for Charges and updated/standardized test styles and numeric formatting across suites.

Add PaystackSdk::Resources::Charges with:
POST /charge for Mobile Money initiation (mobile_money.phone, mobile_money.provider)
POST /charge/submit_otp for OTP/voucher flows (e.g., Vodafone)
Validate payloads (email, amount, currency, reference) and provider codes
(mtn, atl, vod, mpesa, orange, wave)
Expose client.charges
Add specs for Charges resource and client accessor
No breaking changes. Mobile Money status verification continues via
/transaction/verify/{reference} and webhook handling remains app-level.

Also, Refactor code to standardize string delimiters and improve readability
- No functional changes were made; this is purely a formatting improvement.
Copy link
Copy Markdown
Owner

@nanafox nanafox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your submission, @smorttey. Your contribution will help improve the project. To ensure consistency, I use Standard Ruby for linting and formatting. Since the current submission uses Rubocop, it introduces formatting differences with the established codebase. For this project, let's stick to Standard Ruby and avoid Rubocop to maintain a uniform style.

If you'd prefer to use Rubocop, I suggest updating your .rubocop.yml file to include and use StandardRB's rules.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 22, 2025

Walkthrough

Adds a new Charges resource (mobile money and OTP flows), wires it into the client, adds related specs, refactors response iteration and several validators for early returns, applies minor stylistic/test changes, and adjusts development dependencies in the gemspec.

Changes

Cohort / File(s) Summary of Changes
Charges resource (new)
lib/paystack_sdk/resources/charges.rb
Adds PaystackSdk::Resources::Charges with MOBILE_MONEY_PROVIDERS, mobile_money(payload), and submit_otp(payload); includes payload/provider validation, posts to /charge and /charge/submit_otp, and wraps responses.
Client wiring
lib/paystack_sdk/client.rb
Requires the charges resource and adds charges accessor returning Resources::Charges.new(@connection).
Response iteration & helpers
lib/paystack_sdk/response.rb
Changes each(&block)each, refactors some error handling to single-line conditionals, uses %i[...] for method arrays, and replaces $2 with Regexp.last_match(2).
Validations refactor
lib/paystack_sdk/validations.rb
Rewrites validators to use early returns (reduced nesting) while preserving validation semantics and nil-allowance behavior.
Customers list minor tweak
lib/paystack_sdk/resources/customers.rb
Inlines conditional date validation calls (validate_date_format!) with equivalent behavior.
Executable style
bin/console
Replaces double-quoted require strings with single-quoted ones; no behavior change.
Gemspec dev deps
paystack_sdk.gemspec
Development dependency edits producing final state with rspec (~> 3.13) and standard (~> 1.49.0) (temporary swap with debug occurred in the diff).
Specs: Charges (new)
spec/resources/charges_spec.rb
Adds tests covering mobile_money payload posting, provider case-insensitivity and validation errors, and submit_otp behavior and errors.
Specs: Client accessor
spec/client_spec.rb
Adds test verifying client.charges returns a Resources::Charges instance (via a stubbed double).
Specs: Formatting/style updates
spec/resources/banks_spec.rb, spec/resources/transfer_recipients_spec.rb, spec/resources/transfers_spec.rb, spec/resources/verification_spec.rb, spec/resources/transactions_spec.rb
Switches some expect { ... } to expect do ... end for raise expectations; adds digit separators to numeric literals in tests; no behavior changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Client as PaystackSdk::Client
  participant Charges as Resources::Charges
  participant API as Paystack API

  App->>Client: client.charges
  Client->>Charges: instantiate with @connection
  App->>Charges: mobile_money(payload)
  Note right of Charges: validate payload (email, amount, currency)\nvalidate mobile_money (phone, provider)
  Charges->>API: POST /charge (payload)
  API-->>Charges: HTTP response
  Charges-->>App: PaystackSdk::Response

  opt OTP required
    App->>Charges: submit_otp({reference, otp})
    Charges->>API: POST /charge/submit_otp
    API-->>Charges: HTTP response
    Charges-->>App: PaystackSdk::Response
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

I thump my paw — new charges hop the line,
Mobile-money carrots, providers align.
OTP nibble, a tiny twitchy test,
Validators tidy the burrowed nest.
Specs clap like paws, the release smells fine. 🐇💳✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Add Mobile Money channel support via Charges resource" succinctly and accurately summarizes the primary change in the PR — introducing a Charges resource to add Mobile Money support — and is specific, focused, and readable for teammates scanning history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cee5ee and 8055695.

📒 Files selected for processing (3)
  • lib/paystack_sdk/resources/charges.rb (1 hunks)
  • lib/paystack_sdk/validations.rb (7 hunks)
  • spec/resources/charges_spec.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • spec/resources/charges_spec.rb
  • lib/paystack_sdk/resources/charges.rb
  • lib/paystack_sdk/validations.rb

Comment @coderabbitai help to get the list of available commands and usage tips.

@smorttey
Copy link
Copy Markdown
Contributor Author

Bro, missed your comment before reverting to standard rb. Need to change my github to receive emails.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
paystack_sdk.gemspec (1)

42-46: Dev deps: confirm intent to keep debug and finalize versions.

AI summary says debug was removed, but gemspec still includes it. If unused, remove to slim dev surface.

Apply if you want to drop debug:

 spec.add_dependency "faraday", "~> 2.13.1"
-spec.add_development_dependency "debug", "~> 1.9.0"
 spec.add_development_dependency "irb", "~> 1.15.1"
 spec.add_development_dependency "rake", "~> 13.2.1"
 spec.add_development_dependency "rspec", "~> 3.13"
 spec.add_development_dependency "standard", "~> 1.49.0"
lib/paystack_sdk/response.rb (2)

212-221: Enumerator-friendly each is correct.

enum_for(:each) + wrapped yields are idiomatic.

Optional: include Enumerable for free methods:

 class Response
+  include Enumerable

244-249: Wrapped first/last are correct.

Consider aligning #[] with string/symbol dual‑key access (doc claims both). Proposed outside this hunk:

-      if @raw_data.is_a?(Hash)
-        value = @raw_data[key.is_a?(String) ? key.to_sym : key]
+      if @raw_data.is_a?(Hash)
+        k = key
+        value =
+          if k.is_a?(String)
+            @raw_data[k] || @raw_data[k.to_sym]
+          else
+            @raw_data[k] || @raw_data[k.to_s]
+          end
         wrap_value(value)
lib/paystack_sdk/validations.rb (1)

56-60: Missing params: consider reporting all, not just first.

Improves DX in tests and clients.

Example:

- param = missing_params.first
- raise PaystackSdk::MissingParamError.new(param)
+ raise PaystackSdk::MissingParamError.new(missing_params.join(", "))

If the error class expects a single field, consider a new error type for multiple fields instead.

bin/console (1)

10-11: Optional: load env vars in console for easier local testing.
If you rely on PAYSTACK_SECRET_KEY locally, consider auto-loading dotenv.

 require 'bundler/setup'
+require 'dotenv/load' # optional: auto‑load .env for console sessions
 require 'paystack_sdk'
spec/client_spec.rb (1)

45-52: Also assert memoization of client.charges.
Ensures we only instantiate Charges once.

-  describe "#charges" do
+  describe "#charges" do
     it "returns an instance of Charges resource" do
-      charges_double = instance_double(PaystackSdk::Resources::Charges)
-      allow(PaystackSdk::Resources::Charges).to receive(:new).and_return(charges_double)
+      charges_double = instance_double(PaystackSdk::Resources::Charges)
+      expect(PaystackSdk::Resources::Charges).to receive(:new).once.and_return(charges_double)
 
-      expect(client.charges).to eq(charges_double)
+      expect(client.charges).to eq(charges_double)
+      # Memoized on subsequent calls
+      expect(client.charges).to equal(charges_double)
     end
   end
spec/resources/charges_spec.rb (1)

7-59: Add tests for missing provider and missing reference.
These round out edge cases aligned with validations.

# In describe "#mobile_money"
it "raises an error when mobile_money provider is missing" do
  payload[:mobile_money].delete(:provider)
  expect { charges.mobile_money(payload) }
    .to raise_error(PaystackSdk::MissingParamError, /mobile_money provider/i)
end

# In describe "#submit_otp"
it "raises an error when reference is missing" do
  invalid = otp_payload.except(:reference)
  expect { charges.submit_otp(invalid) }
    .to raise_error(PaystackSdk::MissingParamError, /reference/i)
end

Also applies to: 61-83

lib/paystack_sdk/resources/charges.rb (2)

14-14: Provider list OK; consider future-proofing with alias handling.
If Paystack adds aliases (e.g., vodafone/vod), consider accepting known aliases via a mapping table.


61-83: Optional: default currency per provider group.
If currency is omitted, consider defaulting GHS for mtn/vod/atl, KES for mpesa, etc., or at least warn/log when omitted to reduce integration errors.

Would you like me to add a small mapping with defaults and update docs?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80a1f69 and 0cee5ee.

📒 Files selected for processing (14)
  • bin/console (1 hunks)
  • lib/paystack_sdk/client.rb (2 hunks)
  • lib/paystack_sdk/resources/charges.rb (1 hunks)
  • lib/paystack_sdk/resources/customers.rb (1 hunks)
  • lib/paystack_sdk/response.rb (6 hunks)
  • lib/paystack_sdk/validations.rb (8 hunks)
  • paystack_sdk.gemspec (1 hunks)
  • spec/client_spec.rb (1 hunks)
  • spec/resources/banks_spec.rb (1 hunks)
  • spec/resources/charges_spec.rb (1 hunks)
  • spec/resources/transactions_spec.rb (4 hunks)
  • spec/resources/transfer_recipients_spec.rb (1 hunks)
  • spec/resources/transfers_spec.rb (2 hunks)
  • spec/resources/verification_spec.rb (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
spec/resources/verification_spec.rb (2)
lib/paystack_sdk/client.rb (1)
  • verification (122-124)
lib/paystack_sdk/resources/verification.rb (4)
  • resolve_account (6-34)
  • resolve_account (9-13)
  • resolve_card_bin (17-20)
  • validate_account (26-33)
spec/resources/charges_spec.rb (3)
lib/paystack_sdk/client.rb (1)
  • charges (136-138)
lib/paystack_sdk/resources/charges.rb (2)
  • mobile_money (31-36)
  • submit_otp (46-57)
lib/paystack_sdk/response.rb (1)
  • success? (122-124)
spec/resources/transfer_recipients_spec.rb (1)
lib/paystack_sdk/resources/transfer_recipients.rb (2)
  • create (5-45)
  • create (8-16)
spec/resources/transfers_spec.rb (1)
lib/paystack_sdk/resources/transfers.rb (2)
  • create (5-45)
  • create (8-16)
lib/paystack_sdk/resources/customers.rb (1)
lib/paystack_sdk/validations.rb (1)
  • validate_date_format! (106-118)
spec/resources/banks_spec.rb (2)
lib/paystack_sdk/resources/transactions.rb (1)
  • list (105-128)
lib/paystack_sdk/resources/banks.rb (2)
  • list (5-19)
  • list (8-18)
spec/client_spec.rb (1)
lib/paystack_sdk/client.rb (1)
  • charges (136-138)
lib/paystack_sdk/resources/charges.rb (2)
lib/paystack_sdk/resources/base.rb (1)
  • handle_response (43-45)
lib/paystack_sdk/validations.rb (4)
  • validate_fields! (204-240)
  • validate_hash! (39-43)
  • validate_presence! (67-71)
  • validate_allowed_values! (137-148)
🔇 Additional comments (28)
spec/resources/banks_spec.rb (1)

27-30: Style switch to expect do … end looks good.

Functionally identical, aligns suite style.

spec/resources/transfers_spec.rb (2)

9-9: Numeric literal with underscore is fine.

Improves readability without changing value.


26-29: Style switch to expect do … end looks good.

Keeps behavior; consistent with other specs.

spec/resources/verification_spec.rb (4)

18-21: LGTM on style change.


24-27: LGTM on style change.


41-44: LGTM on style change.


72-75: LGTM on style change.

spec/resources/transfer_recipients_spec.rb (1)

25-28: LGTM on style change.

No behavioral impact.

lib/paystack_sdk/response.rb (3)

232-236: Delegation to size/length/count/empty? looks good.


263-263: Good switch to Regexp.last_match.

Avoids globals, thread‑safer.


89-89: Raising AuthenticationError for 401 is intended — callers must handle AuthenticationError.

Search of the repo shows resources call handle_response(...) and README/tests expect or rescue AuthenticationError (and tests mock successful Faraday::Response); no callers were found that rely on receiving an unsuccessful Response object for 401.

lib/paystack_sdk/validations.rb (7)

40-43: Early return in validate_hash! is fine.


68-71: Presence check reads well.


94-97: Reference format guard is good.


110-110: Nil date early return keeps flow clear.


156-166: Email regex baseline is acceptable.


174-184: Currency ISO-3 validator is fine.


144-148: Incorrect — case-insensitive handling already implemented

validate_mobile_money_provider! downcases the provider before calling validate_allowed_values! (lib/paystack_sdk/resources/charges.rb:85–90) and specs cover mixed-case "MTN", so adding a case_insensitive option is unnecessary.

Likely an incorrect or invalid review comment.

lib/paystack_sdk/resources/customers.rb (1)

71-74: Inline date validations are concise and clear.

bin/console (1)

4-5: LGTM — console requires look good.
Single quotes and require order are consistent; no functional concerns.

spec/resources/transactions_spec.rb (1)

11-11: LGTM — numeric literal separators only.
Underscore formatting improves readability without changing values.

Also applies to: 136-136, 144-144, 157-157, 196-196

lib/paystack_sdk/client.rb (2)

9-9: LGTM — charges resource required.
Import placement matches existing pattern.


126-138: LGTM — public charges accessor mirrors other resources.
Docs and memoization are consistent.

spec/resources/charges_spec.rb (5)

20-32: LGTM — happy-path mobile money initiation covered.
Good verification of POST target and response wrapping.


46-51: LGTM — invalid provider path exercised.
Regex expectation is clear.


53-58: LGTM — missing phone validation covered.
Good negative coverage.


61-74: LGTM — OTP submission happy-path covered.
Covers endpoint and response wrapping.


76-81: LGTM — missing OTP validation covered.
Consistent with validations API.

Comment thread lib/paystack_sdk/resources/charges.rb
Comment thread lib/paystack_sdk/resources/charges.rb
Comment thread spec/resources/charges_spec.rb Outdated
Comment on lines +34 to +44
it "accepts provider codes in different cases" do
payload[:mobile_money][:provider] = "MTN"
response_double = double("Response", success?: true)
expect(connection).to receive(:post)
.with("/charge", payload)
.and_return(response_double)
expect(PaystackSdk::Response).to receive(:new).with(response_double)
.and_return(response_double)

expect { charges.mobile_money(payload) }.not_to raise_error
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Normalize provider before POST; adjust expectation accordingly.
Validation accepts case-insensitive providers, but the outgoing payload currently preserves caller case. Paystack typically expects lowercase codes; normalize to lowercase before posting and update the spec to assert the normalized payload.

-  it "accepts provider codes in different cases" do
-    payload[:mobile_money][:provider] = "MTN"
-    response_double = double("Response", success?: true)
-    expect(connection).to receive(:post)
-      .with("/charge", payload)
-      .and_return(response_double)
-    expect(PaystackSdk::Response).to receive(:new).with(response_double)
-      .and_return(response_double)
-
-    expect { charges.mobile_money(payload) }.not_to raise_error
-  end
+  it "accepts provider codes in different cases and normalizes before POST" do
+    payload[:mobile_money][:provider] = "MTN"
+    normalized_payload = payload.merge(
+      mobile_money: payload[:mobile_money].merge(provider: "mtn")
+    )
+    response_double = double("Response", success?: true)
+    expect(connection).to receive(:post)
+      .with("/charge", normalized_payload)
+      .and_return(response_double)
+    expect(PaystackSdk::Response).to receive(:new).with(response_double)
+      .and_return(response_double)
+
+    expect { charges.mobile_money(payload) }.not_to raise_error
+  end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it "accepts provider codes in different cases" do
payload[:mobile_money][:provider] = "MTN"
response_double = double("Response", success?: true)
expect(connection).to receive(:post)
.with("/charge", payload)
.and_return(response_double)
expect(PaystackSdk::Response).to receive(:new).with(response_double)
.and_return(response_double)
expect { charges.mobile_money(payload) }.not_to raise_error
end
it "accepts provider codes in different cases and normalizes before POST" do
payload[:mobile_money][:provider] = "MTN"
normalized_payload = payload.merge(
mobile_money: payload[:mobile_money].merge(provider: "mtn")
)
response_double = double("Response", success?: true)
expect(connection).to receive(:post)
.with("/charge", normalized_payload)
.and_return(response_double)
expect(PaystackSdk::Response).to receive(:new).with(response_double)
.and_return(response_double)
expect { charges.mobile_money(payload) }.not_to raise_error
end
🤖 Prompt for AI Agents
In spec/resources/charges_spec.rb around lines 34–44, the test expects the
outgoing payload to preserve caller case but the code should normalize
mobile_money.provider to lowercase before the POST; update the implementation to
downcase the provider (e.g., use a safe non-destructive copy or set
payload_for_post = payload.deep_dup and
payload_for_post[:mobile_money][:provider] =
payload_for_post[:mobile_money][:provider].to_s.downcase) and change the test
expectation to expect the POST with the payload whose mobile_money.provider is
downcased, keeping the rest of the assertions the same.

@smorttey smorttey requested a review from nanafox September 22, 2025 21:43
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactors, sweet.

Copy link
Copy Markdown
Owner

@nanafox nanafox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the additions, @smorttey!

@nanafox nanafox merged commit f8dd978 into nanafox:main Sep 22, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants