sunny-payments 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +165 -0
- data/lib/sunny/client.rb +103 -0
- data/lib/sunny/errors.rb +55 -0
- data/lib/sunny/resources/bills.rb +63 -0
- data/lib/sunny/resources/bnpl.rb +50 -0
- data/lib/sunny/resources/crypto.rb +47 -0
- data/lib/sunny/resources/customers.rb +37 -0
- data/lib/sunny/resources/invoices.rb +54 -0
- data/lib/sunny/resources/mobile_money.rb +94 -0
- data/lib/sunny/resources/payments.rb +69 -0
- data/lib/sunny/resources/qr_codes.rb +41 -0
- data/lib/sunny/resources/refunds.rb +30 -0
- data/lib/sunny/resources/virtual_accounts.rb +35 -0
- data/lib/sunny/resources/webhooks.rb +63 -0
- data/lib/sunny/version.rb +5 -0
- data/lib/sunny.rb +28 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 487454dfe36081782721d77218b1a9e9e1538cbbcef624b57d01a2ea68d70c7e
|
|
4
|
+
data.tar.gz: ca5c9b4f2292e796b7e14a11e381526c351d05c8db5fda9cf06fc64ac6976db3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 72ca1ceda8b61446b111c8fc51e8a1f62d176fbc311d290c972525d13dc43389c771acccf53f007c2010934b9ed33793caac3b73a52687983f70fa517f6757a4
|
|
7
|
+
data.tar.gz: f8309afac75958f824151c70a250d5735f387c06a5f8ac219b27de559b84e23f1b8a9cfe99e67f066d14e61eda630bfaa5a4307e11ff8d826081c7b7a406e2bf
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Sunny Pay Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/sunny-logo.svg" alt="Sunny Payments" width="60" height="60">
|
|
3
|
+
<img src="assets/ruby.svg" alt="Ruby" width="60" height="60">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<h1 align="center">Sunny Payments Ruby SDK</h1>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
The official Ruby SDK for <a href="https://sunnypay.co.ke">Sunny Payments</a> - Payment processing made simple.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://rubygems.org/gems/sunny-payments"><img src="https://img.shields.io/gem/v/sunny-payments.svg" alt="Gem Version"></a>
|
|
14
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gem install sunny-payments
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or add to your Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'sunny-payments', '~> 1.0'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'sunny'
|
|
33
|
+
|
|
34
|
+
# Initialize the client
|
|
35
|
+
sunny = Sunny::Client.new('sk_live_your_api_key')
|
|
36
|
+
|
|
37
|
+
# Create a payment (currency is REQUIRED)
|
|
38
|
+
payment = sunny.payments.create(
|
|
39
|
+
amount: 1000,
|
|
40
|
+
currency: 'KES', # Required - no default!
|
|
41
|
+
source: 'mpesa',
|
|
42
|
+
description: 'Order #12345'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
puts payment['id'] # pay_xxxxx
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- ✅ **Payments** - Create, capture, refund payments
|
|
51
|
+
- ✅ **Mobile Money** - M-Pesa STK Push, B2C, MTN MoMo, Airtel Money
|
|
52
|
+
- ✅ **Invoices** - Create and send invoices
|
|
53
|
+
- ✅ **Bills** - Airtime, data, electricity payments
|
|
54
|
+
- ✅ **QR Codes** - Static and dynamic QR payments
|
|
55
|
+
- ✅ **Crypto** - Accept BTC, ETH, USDT, USDC
|
|
56
|
+
- ✅ **Webhooks** - Register endpoints & verify signatures
|
|
57
|
+
- ✅ **BNPL** - Buy Now Pay Later
|
|
58
|
+
|
|
59
|
+
## Usage Examples
|
|
60
|
+
|
|
61
|
+
### M-Pesa STK Push
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
result = sunny.mobile_money.mpesa_stk_push(
|
|
65
|
+
phone_number: '254712345678',
|
|
66
|
+
amount: 1000,
|
|
67
|
+
currency: 'KES', # Required
|
|
68
|
+
account_reference: 'ORDER-001'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check status
|
|
72
|
+
status = sunny.mobile_money.mpesa_status(result['checkout_request_id'])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Bills
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# Purchase airtime
|
|
79
|
+
sunny.bills.purchase_airtime(
|
|
80
|
+
phone_number: '+254712345678',
|
|
81
|
+
amount: 100,
|
|
82
|
+
network: 'safaricom'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Buy electricity tokens
|
|
86
|
+
electricity = sunny.bills.purchase_electricity(
|
|
87
|
+
meter_number: '12345678',
|
|
88
|
+
amount: 500,
|
|
89
|
+
phone_number: '+254712345678'
|
|
90
|
+
)
|
|
91
|
+
puts electricity['token'] # KPLC token
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Crypto Payments
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
address = sunny.crypto.create_address(
|
|
98
|
+
cryptocurrency: 'USDT',
|
|
99
|
+
amount: 10000,
|
|
100
|
+
currency: 'KES' # Required
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Webhooks
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Register webhook
|
|
108
|
+
webhook = sunny.webhooks.create(
|
|
109
|
+
url: 'https://example.com/webhooks/sunny',
|
|
110
|
+
events: ['payment.succeeded', 'payment.failed']
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# In your Sinatra/Rails controller:
|
|
114
|
+
post '/webhooks/sunny' do
|
|
115
|
+
payload = request.body.read
|
|
116
|
+
signature = request.env['HTTP_X_SUNNY_SIGNATURE']
|
|
117
|
+
|
|
118
|
+
unless Sunny::Resources::Webhooks.verify_signature(payload, signature, WEBHOOK_SECRET)
|
|
119
|
+
halt 400, 'Invalid signature'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
event = JSON.parse(payload)
|
|
123
|
+
|
|
124
|
+
case event['type']
|
|
125
|
+
when 'payment.succeeded'
|
|
126
|
+
# Handle successful payment
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
status 200
|
|
130
|
+
'OK'
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Error Handling
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
begin
|
|
138
|
+
payment = sunny.payments.create(amount: 1000, currency: 'KES', source: 'mpesa')
|
|
139
|
+
rescue Sunny::AuthenticationError
|
|
140
|
+
puts "Invalid API key"
|
|
141
|
+
rescue Sunny::ValidationError => e
|
|
142
|
+
puts "Validation error: #{e.message}, field: #{e.field}"
|
|
143
|
+
rescue Sunny::RateLimitError => e
|
|
144
|
+
puts "Rate limited. Retry after #{e.retry_after} seconds"
|
|
145
|
+
rescue Sunny::APIError => e
|
|
146
|
+
puts "API error: #{e.message}"
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# Via environment variable
|
|
154
|
+
ENV['SUNNY_API_KEY'] = 'sk_live_xxx'
|
|
155
|
+
|
|
156
|
+
# Or configure directly
|
|
157
|
+
sunny = Sunny::Client.new('sk_live_xxx', {
|
|
158
|
+
base_url: 'https://api.sunnypay.co.ke/v1',
|
|
159
|
+
timeout: 30
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
data/lib/sunny/client.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Sunny
|
|
7
|
+
# Main client for interacting with Sunny Payments API
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :payments, :customers, :mobile_money, :invoices, :bills,
|
|
10
|
+
:qr_codes, :crypto, :webhooks, :refunds, :virtual_accounts, :bnpl
|
|
11
|
+
|
|
12
|
+
# Initialize a new Sunny client
|
|
13
|
+
#
|
|
14
|
+
# @param api_key [String] Your Sunny API key (starts with sk_live_ or sk_test_)
|
|
15
|
+
# @param options [Hash] Optional configuration
|
|
16
|
+
# @option options [String] :base_url Custom API base URL
|
|
17
|
+
# @option options [Integer] :timeout Request timeout in seconds (default: 30)
|
|
18
|
+
def initialize(api_key, options = {})
|
|
19
|
+
raise AuthenticationError, "API key is required" if api_key.nil? || api_key.empty?
|
|
20
|
+
raise AuthenticationError, "Invalid API key format" unless api_key.start_with?("sk_")
|
|
21
|
+
|
|
22
|
+
@api_key = api_key
|
|
23
|
+
@base_url = options[:base_url] || Sunny.api_base
|
|
24
|
+
@timeout = options[:timeout] || 30
|
|
25
|
+
|
|
26
|
+
@connection = build_connection
|
|
27
|
+
|
|
28
|
+
# Initialize resources
|
|
29
|
+
@payments = Resources::Payments.new(self)
|
|
30
|
+
@customers = Resources::Customers.new(self)
|
|
31
|
+
@mobile_money = Resources::MobileMoney.new(self)
|
|
32
|
+
@invoices = Resources::Invoices.new(self)
|
|
33
|
+
@bills = Resources::Bills.new(self)
|
|
34
|
+
@qr_codes = Resources::QRCodes.new(self)
|
|
35
|
+
@crypto = Resources::Crypto.new(self)
|
|
36
|
+
@webhooks = Resources::Webhooks.new(self)
|
|
37
|
+
@refunds = Resources::Refunds.new(self)
|
|
38
|
+
@virtual_accounts = Resources::VirtualAccounts.new(self)
|
|
39
|
+
@bnpl = Resources::BNPL.new(self)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Make a GET request
|
|
43
|
+
def get(path, params = {})
|
|
44
|
+
request(:get, path, params)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Make a POST request
|
|
48
|
+
def post(path, body = {})
|
|
49
|
+
request(:post, path, body)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Make a PUT request
|
|
53
|
+
def put(path, body = {})
|
|
54
|
+
request(:put, path, body)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Make a DELETE request
|
|
58
|
+
def delete(path)
|
|
59
|
+
request(:delete, path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def build_connection
|
|
65
|
+
Faraday.new(url: @base_url) do |conn|
|
|
66
|
+
conn.request :json
|
|
67
|
+
conn.response :json, content_type: /\bjson$/
|
|
68
|
+
conn.headers["Authorization"] = "Bearer #{@api_key}"
|
|
69
|
+
conn.headers["Content-Type"] = "application/json"
|
|
70
|
+
conn.headers["User-Agent"] = "sunny-ruby/#{VERSION}"
|
|
71
|
+
conn.options.timeout = @timeout
|
|
72
|
+
conn.adapter Faraday.default_adapter
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def request(method, path, data = nil)
|
|
77
|
+
response = @connection.send(method, path, data)
|
|
78
|
+
handle_response(response)
|
|
79
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
80
|
+
raise NetworkError, e.message
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def handle_response(response)
|
|
84
|
+
case response.status
|
|
85
|
+
when 200..299
|
|
86
|
+
response.body
|
|
87
|
+
when 401
|
|
88
|
+
raise AuthenticationError, error_message(response)
|
|
89
|
+
when 400
|
|
90
|
+
raise ValidationError.new(error_message(response), field: response.body&.dig("field"))
|
|
91
|
+
when 429
|
|
92
|
+
retry_after = response.headers["Retry-After"]&.to_i || 60
|
|
93
|
+
raise RateLimitError, retry_after
|
|
94
|
+
else
|
|
95
|
+
raise APIError.new(error_message(response), status: response.status, code: response.body&.dig("code"))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def error_message(response)
|
|
100
|
+
response.body&.dig("message") || response.body&.dig("error") || "An error occurred"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/sunny/errors.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
# Base error class for all Sunny errors
|
|
5
|
+
class SunnyError < StandardError
|
|
6
|
+
attr_reader :code, :status
|
|
7
|
+
|
|
8
|
+
def initialize(message = nil, code: nil, status: nil)
|
|
9
|
+
@code = code
|
|
10
|
+
@status = status
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raised when API key is invalid or missing
|
|
16
|
+
class AuthenticationError < SunnyError
|
|
17
|
+
def initialize(message = "Invalid API key")
|
|
18
|
+
super(message, status: 401)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Raised when request parameters are invalid
|
|
23
|
+
class ValidationError < SunnyError
|
|
24
|
+
attr_reader :field
|
|
25
|
+
|
|
26
|
+
def initialize(message, field: nil)
|
|
27
|
+
@field = field
|
|
28
|
+
super(message, status: 400)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Raised when rate limit is exceeded
|
|
33
|
+
class RateLimitError < SunnyError
|
|
34
|
+
attr_reader :retry_after
|
|
35
|
+
|
|
36
|
+
def initialize(retry_after = 60)
|
|
37
|
+
@retry_after = retry_after
|
|
38
|
+
super("Rate limit exceeded. Retry after #{retry_after} seconds", status: 429)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Raised for general API errors
|
|
43
|
+
class APIError < SunnyError
|
|
44
|
+
def initialize(message, status: nil, code: nil)
|
|
45
|
+
super(message, status: status, code: code)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Raised for network/connection errors
|
|
50
|
+
class NetworkError < SunnyError
|
|
51
|
+
def initialize(message = "Unable to connect to Sunny API")
|
|
52
|
+
super(message)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Bills resource for airtime, data, electricity
|
|
6
|
+
class Bills
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Purchase airtime
|
|
12
|
+
#
|
|
13
|
+
# @param phone_number [String] Phone number to top up
|
|
14
|
+
# @param amount [Integer] Amount to purchase
|
|
15
|
+
# @param network [String] Network provider (safaricom, airtel, telkom)
|
|
16
|
+
def purchase_airtime(phone_number:, amount:, network:)
|
|
17
|
+
@client.post("/bills/airtime", {
|
|
18
|
+
phone_number: phone_number,
|
|
19
|
+
amount: amount,
|
|
20
|
+
network: network
|
|
21
|
+
})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get available data bundles
|
|
25
|
+
def get_data_bundles(network)
|
|
26
|
+
@client.get("/bills/data/bundles", { network: network })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Purchase data bundle
|
|
30
|
+
def purchase_data(phone_number:, bundle_id:, network:)
|
|
31
|
+
@client.post("/bills/data", {
|
|
32
|
+
phone_number: phone_number,
|
|
33
|
+
bundle_id: bundle_id,
|
|
34
|
+
network: network
|
|
35
|
+
})
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Purchase electricity tokens (KPLC)
|
|
39
|
+
def purchase_electricity(meter_number:, amount:, phone_number:)
|
|
40
|
+
@client.post("/bills/electricity", {
|
|
41
|
+
meter_number: meter_number,
|
|
42
|
+
amount: amount,
|
|
43
|
+
phone_number: phone_number
|
|
44
|
+
})
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validate electricity meter
|
|
48
|
+
def validate_meter(meter_number)
|
|
49
|
+
@client.get("/bills/electricity/validate", { meter_number: meter_number })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Pay TV subscription
|
|
53
|
+
def pay_tv(provider:, account_number:, amount:, phone_number:)
|
|
54
|
+
@client.post("/bills/tv", {
|
|
55
|
+
provider: provider,
|
|
56
|
+
account_number: account_number,
|
|
57
|
+
amount: amount,
|
|
58
|
+
phone_number: phone_number
|
|
59
|
+
})
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# BNPL (Buy Now Pay Later) resource
|
|
6
|
+
class BNPL
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Check customer eligibility
|
|
12
|
+
#
|
|
13
|
+
# @param customer_id [String] Customer ID
|
|
14
|
+
# @param amount [Integer] Amount to check
|
|
15
|
+
# @param currency [String] Currency code - REQUIRED
|
|
16
|
+
def check_eligibility(customer_id:, amount:, currency:)
|
|
17
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
18
|
+
|
|
19
|
+
@client.post("/bnpl/eligibility", {
|
|
20
|
+
customer_id: customer_id,
|
|
21
|
+
amount: amount,
|
|
22
|
+
currency: currency
|
|
23
|
+
})
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Create a BNPL payment plan
|
|
27
|
+
def create_plan(customer_id:, amount:, currency:, installments:, **options)
|
|
28
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
29
|
+
|
|
30
|
+
@client.post("/bnpl/plans", {
|
|
31
|
+
customer_id: customer_id,
|
|
32
|
+
amount: amount,
|
|
33
|
+
currency: currency,
|
|
34
|
+
installments: installments,
|
|
35
|
+
**options
|
|
36
|
+
})
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get a payment plan
|
|
40
|
+
def retrieve_plan(plan_id)
|
|
41
|
+
@client.get("/bnpl/plans/#{plan_id}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# List payment plans
|
|
45
|
+
def list_plans(**params)
|
|
46
|
+
@client.get("/bnpl/plans", params)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Crypto resource for crypto payments
|
|
6
|
+
class Crypto
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a crypto payment address
|
|
12
|
+
#
|
|
13
|
+
# @param cryptocurrency [String] Crypto type (BTC, ETH, USDT, USDC)
|
|
14
|
+
# @param amount [Integer] Expected amount in fiat
|
|
15
|
+
# @param currency [String] Fiat currency code - REQUIRED
|
|
16
|
+
# @param options [Hash] Additional options
|
|
17
|
+
def create_address(cryptocurrency:, amount:, currency:, **options)
|
|
18
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
19
|
+
|
|
20
|
+
@client.post("/crypto/addresses", {
|
|
21
|
+
cryptocurrency: cryptocurrency,
|
|
22
|
+
amount: amount,
|
|
23
|
+
currency: currency,
|
|
24
|
+
**options
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get crypto payment status
|
|
29
|
+
def get_payment(payment_id)
|
|
30
|
+
@client.get("/crypto/payments/#{payment_id}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# List crypto payments
|
|
34
|
+
def list_payments(**params)
|
|
35
|
+
@client.get("/crypto/payments", params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get exchange rates
|
|
39
|
+
def get_rates(cryptocurrency:, currency:)
|
|
40
|
+
@client.get("/crypto/rates", {
|
|
41
|
+
cryptocurrency: cryptocurrency,
|
|
42
|
+
currency: currency
|
|
43
|
+
})
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Customers resource
|
|
6
|
+
class Customers
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a new customer
|
|
12
|
+
def create(email:, **options)
|
|
13
|
+
@client.post("/customers", { email: email, **options })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Retrieve a customer
|
|
17
|
+
def retrieve(customer_id)
|
|
18
|
+
@client.get("/customers/#{customer_id}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Update a customer
|
|
22
|
+
def update(customer_id, **attributes)
|
|
23
|
+
@client.put("/customers/#{customer_id}", attributes)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# List customers
|
|
27
|
+
def list(**params)
|
|
28
|
+
@client.get("/customers", params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Delete a customer
|
|
32
|
+
def delete(customer_id)
|
|
33
|
+
@client.delete("/customers/#{customer_id}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Invoices resource
|
|
6
|
+
class Invoices
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a new invoice
|
|
12
|
+
#
|
|
13
|
+
# @param customer_email [String] Customer email
|
|
14
|
+
# @param amount [Integer] Invoice amount
|
|
15
|
+
# @param currency [String] Currency code - REQUIRED
|
|
16
|
+
# @param options [Hash] Additional options
|
|
17
|
+
def create(customer_email:, amount:, currency:, **options)
|
|
18
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
19
|
+
|
|
20
|
+
@client.post("/invoices", {
|
|
21
|
+
customer_email: customer_email,
|
|
22
|
+
amount: amount,
|
|
23
|
+
currency: currency,
|
|
24
|
+
**options
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Retrieve an invoice
|
|
29
|
+
def retrieve(invoice_id)
|
|
30
|
+
@client.get("/invoices/#{invoice_id}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# List invoices
|
|
34
|
+
def list(**params)
|
|
35
|
+
@client.get("/invoices", params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Send an invoice
|
|
39
|
+
def send_invoice(invoice_id)
|
|
40
|
+
@client.post("/invoices/#{invoice_id}/send", {})
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Mark invoice as paid
|
|
44
|
+
def mark_paid(invoice_id)
|
|
45
|
+
@client.post("/invoices/#{invoice_id}/mark-paid", {})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Void an invoice
|
|
49
|
+
def void(invoice_id)
|
|
50
|
+
@client.post("/invoices/#{invoice_id}/void", {})
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Mobile Money resource for M-Pesa, MTN MoMo, Airtel Money
|
|
6
|
+
class MobileMoney
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Initiate M-Pesa STK Push
|
|
12
|
+
#
|
|
13
|
+
# @param phone_number [String] Customer phone number (254...)
|
|
14
|
+
# @param amount [Integer] Amount in smallest currency unit
|
|
15
|
+
# @param currency [String] Currency code (e.g., 'KES') - REQUIRED
|
|
16
|
+
# @param account_reference [String] Account reference for the transaction
|
|
17
|
+
# @param options [Hash] Additional options (description, metadata, etc.)
|
|
18
|
+
# @return [Hash] STK push response with checkout_request_id
|
|
19
|
+
def mpesa_stk_push(phone_number:, amount:, currency:, account_reference:, **options)
|
|
20
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
21
|
+
|
|
22
|
+
@client.post("/mobile-money/mpesa/stk-push", {
|
|
23
|
+
phone_number: phone_number,
|
|
24
|
+
amount: amount,
|
|
25
|
+
currency: currency,
|
|
26
|
+
account_reference: account_reference,
|
|
27
|
+
**options
|
|
28
|
+
})
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check M-Pesa STK Push status
|
|
32
|
+
#
|
|
33
|
+
# @param checkout_request_id [String] The checkout request ID from stk_push
|
|
34
|
+
# @return [Hash] Transaction status
|
|
35
|
+
def mpesa_status(checkout_request_id)
|
|
36
|
+
@client.get("/mobile-money/mpesa/status/#{checkout_request_id}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# M-Pesa B2C (payouts)
|
|
40
|
+
#
|
|
41
|
+
# @param phone_number [String] Recipient phone number
|
|
42
|
+
# @param amount [Integer] Amount to send
|
|
43
|
+
# @param currency [String] Currency code - REQUIRED
|
|
44
|
+
# @param options [Hash] Additional options
|
|
45
|
+
# @return [Hash] B2C response
|
|
46
|
+
def mpesa_b2c(phone_number:, amount:, currency:, **options)
|
|
47
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
48
|
+
|
|
49
|
+
@client.post("/mobile-money/mpesa/b2c", {
|
|
50
|
+
phone_number: phone_number,
|
|
51
|
+
amount: amount,
|
|
52
|
+
currency: currency,
|
|
53
|
+
**options
|
|
54
|
+
})
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# MTN Mobile Money collection
|
|
58
|
+
#
|
|
59
|
+
# @param phone_number [String] Customer phone number
|
|
60
|
+
# @param amount [Integer] Amount to collect
|
|
61
|
+
# @param currency [String] Currency code (e.g., 'UGX') - REQUIRED
|
|
62
|
+
# @param options [Hash] Additional options
|
|
63
|
+
# @return [Hash] Collection response
|
|
64
|
+
def mtn_collect(phone_number:, amount:, currency:, **options)
|
|
65
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
66
|
+
|
|
67
|
+
@client.post("/mobile-money/mtn/collect", {
|
|
68
|
+
phone_number: phone_number,
|
|
69
|
+
amount: amount,
|
|
70
|
+
currency: currency,
|
|
71
|
+
**options
|
|
72
|
+
})
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Airtel Money collection
|
|
76
|
+
#
|
|
77
|
+
# @param phone_number [String] Customer phone number
|
|
78
|
+
# @param amount [Integer] Amount to collect
|
|
79
|
+
# @param currency [String] Currency code - REQUIRED
|
|
80
|
+
# @param options [Hash] Additional options
|
|
81
|
+
# @return [Hash] Collection response
|
|
82
|
+
def airtel_collect(phone_number:, amount:, currency:, **options)
|
|
83
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
84
|
+
|
|
85
|
+
@client.post("/mobile-money/airtel/collect", {
|
|
86
|
+
phone_number: phone_number,
|
|
87
|
+
amount: amount,
|
|
88
|
+
currency: currency,
|
|
89
|
+
**options
|
|
90
|
+
})
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Payments resource for creating and managing payments
|
|
6
|
+
class Payments
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a new payment
|
|
12
|
+
#
|
|
13
|
+
# @param amount [Integer] Amount in smallest currency unit
|
|
14
|
+
# @param currency [String] Currency code (e.g., 'KES', 'USD', 'UGX') - REQUIRED
|
|
15
|
+
# @param source [String] Payment source (e.g., 'mpesa', 'card')
|
|
16
|
+
# @param options [Hash] Additional options
|
|
17
|
+
# @return [Hash] Created payment object
|
|
18
|
+
def create(amount:, currency:, source:, **options)
|
|
19
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
20
|
+
|
|
21
|
+
@client.post("/payments", {
|
|
22
|
+
amount: amount,
|
|
23
|
+
currency: currency,
|
|
24
|
+
source: source,
|
|
25
|
+
**options
|
|
26
|
+
})
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Retrieve a payment by ID
|
|
30
|
+
#
|
|
31
|
+
# @param payment_id [String] The payment ID
|
|
32
|
+
# @return [Hash] Payment object
|
|
33
|
+
def retrieve(payment_id)
|
|
34
|
+
@client.get("/payments/#{payment_id}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# List payments
|
|
38
|
+
#
|
|
39
|
+
# @param params [Hash] Query parameters (limit, starting_after, etc.)
|
|
40
|
+
# @return [Hash] List of payments with pagination info
|
|
41
|
+
def list(**params)
|
|
42
|
+
@client.get("/payments", params)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Capture a payment
|
|
46
|
+
#
|
|
47
|
+
# @param payment_id [String] The payment ID
|
|
48
|
+
# @param amount [Integer, nil] Amount to capture (optional, captures full amount if nil)
|
|
49
|
+
# @return [Hash] Captured payment object
|
|
50
|
+
def capture(payment_id, amount: nil)
|
|
51
|
+
body = amount ? { amount: amount } : {}
|
|
52
|
+
@client.post("/payments/#{payment_id}/capture", body)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Refund a payment
|
|
56
|
+
#
|
|
57
|
+
# @param payment_id [String] The payment ID
|
|
58
|
+
# @param amount [Integer, nil] Amount to refund (optional, full refund if nil)
|
|
59
|
+
# @param reason [String, nil] Reason for refund
|
|
60
|
+
# @return [Hash] Refund object
|
|
61
|
+
def refund(payment_id, amount: nil, reason: nil)
|
|
62
|
+
body = {}
|
|
63
|
+
body[:amount] = amount if amount
|
|
64
|
+
body[:reason] = reason if reason
|
|
65
|
+
@client.post("/payments/#{payment_id}/refund", body)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# QR Codes resource
|
|
6
|
+
class QRCodes
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a QR code
|
|
12
|
+
#
|
|
13
|
+
# @param type [String] QR type ('static' or 'dynamic')
|
|
14
|
+
# @param amount [Integer, nil] Amount (required for dynamic)
|
|
15
|
+
# @param currency [String] Currency code - REQUIRED
|
|
16
|
+
# @param options [Hash] Additional options
|
|
17
|
+
def create(type:, currency:, amount: nil, **options)
|
|
18
|
+
raise ValidationError.new("currency is required", field: "currency") if currency.nil? || currency.empty?
|
|
19
|
+
|
|
20
|
+
body = { type: type, currency: currency, **options }
|
|
21
|
+
body[:amount] = amount if amount
|
|
22
|
+
@client.post("/qr-codes", body)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Retrieve a QR code
|
|
26
|
+
def retrieve(qr_id)
|
|
27
|
+
@client.get("/qr-codes/#{qr_id}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# List QR codes
|
|
31
|
+
def list(**params)
|
|
32
|
+
@client.get("/qr-codes", params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Deactivate a QR code
|
|
36
|
+
def deactivate(qr_id)
|
|
37
|
+
@client.post("/qr-codes/#{qr_id}/deactivate", {})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Refunds resource
|
|
6
|
+
class Refunds
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a refund
|
|
12
|
+
def create(payment_id:, amount: nil, reason: nil)
|
|
13
|
+
body = { payment_id: payment_id }
|
|
14
|
+
body[:amount] = amount if amount
|
|
15
|
+
body[:reason] = reason if reason
|
|
16
|
+
@client.post("/refunds", body)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Retrieve a refund
|
|
20
|
+
def retrieve(refund_id)
|
|
21
|
+
@client.get("/refunds/#{refund_id}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# List refunds
|
|
25
|
+
def list(**params)
|
|
26
|
+
@client.get("/refunds", params)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sunny
|
|
4
|
+
module Resources
|
|
5
|
+
# Virtual Accounts resource
|
|
6
|
+
class VirtualAccounts
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Create a virtual account
|
|
12
|
+
def create(customer_email:, **options)
|
|
13
|
+
@client.post("/virtual-accounts", {
|
|
14
|
+
customer_email: customer_email,
|
|
15
|
+
**options
|
|
16
|
+
})
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Retrieve a virtual account
|
|
20
|
+
def retrieve(account_id)
|
|
21
|
+
@client.get("/virtual-accounts/#{account_id}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# List virtual accounts
|
|
25
|
+
def list(**params)
|
|
26
|
+
@client.get("/virtual-accounts", params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Close a virtual account
|
|
30
|
+
def close(account_id)
|
|
31
|
+
@client.post("/virtual-accounts/#{account_id}/close", {})
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Sunny
|
|
6
|
+
module Resources
|
|
7
|
+
# Webhooks resource
|
|
8
|
+
class Webhooks
|
|
9
|
+
def initialize(client)
|
|
10
|
+
@client = client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Register a webhook endpoint
|
|
14
|
+
def create(url:, events:, **options)
|
|
15
|
+
@client.post("/webhooks", {
|
|
16
|
+
url: url,
|
|
17
|
+
events: events,
|
|
18
|
+
**options
|
|
19
|
+
})
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Retrieve a webhook
|
|
23
|
+
def retrieve(webhook_id)
|
|
24
|
+
@client.get("/webhooks/#{webhook_id}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# List webhooks
|
|
28
|
+
def list(**params)
|
|
29
|
+
@client.get("/webhooks", params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Update a webhook
|
|
33
|
+
def update(webhook_id, **attributes)
|
|
34
|
+
@client.put("/webhooks/#{webhook_id}", attributes)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Delete a webhook
|
|
38
|
+
def delete(webhook_id)
|
|
39
|
+
@client.delete("/webhooks/#{webhook_id}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Verify webhook signature
|
|
43
|
+
#
|
|
44
|
+
# @param payload [String] Raw request body
|
|
45
|
+
# @param signature [String] X-Sunny-Signature header value
|
|
46
|
+
# @param secret [String] Your webhook secret
|
|
47
|
+
# @return [Boolean] Whether signature is valid
|
|
48
|
+
def self.verify_signature(payload, signature, secret)
|
|
49
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
|
|
50
|
+
secure_compare(expected, signature)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Constant-time string comparison
|
|
54
|
+
def self.secure_compare(a, b)
|
|
55
|
+
return false unless a.bytesize == b.bytesize
|
|
56
|
+
|
|
57
|
+
result = 0
|
|
58
|
+
a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
|
|
59
|
+
result.zero?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/sunny.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sunny/version"
|
|
4
|
+
require_relative "sunny/errors"
|
|
5
|
+
require_relative "sunny/client"
|
|
6
|
+
require_relative "sunny/resources/payments"
|
|
7
|
+
require_relative "sunny/resources/customers"
|
|
8
|
+
require_relative "sunny/resources/mobile_money"
|
|
9
|
+
require_relative "sunny/resources/invoices"
|
|
10
|
+
require_relative "sunny/resources/bills"
|
|
11
|
+
require_relative "sunny/resources/qr_codes"
|
|
12
|
+
require_relative "sunny/resources/crypto"
|
|
13
|
+
require_relative "sunny/resources/webhooks"
|
|
14
|
+
require_relative "sunny/resources/refunds"
|
|
15
|
+
require_relative "sunny/resources/virtual_accounts"
|
|
16
|
+
require_relative "sunny/resources/bnpl"
|
|
17
|
+
|
|
18
|
+
module Sunny
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :api_key, :api_base
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield self
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
self.api_base = ENV["SUNNY_API_URL"] || "https://api.sunnypay.co.ke/v1"
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sunny-payments
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sunny Pay Team
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: faraday-retry
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '13.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '13.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rspec
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rubocop
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '1.0'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '1.0'
|
|
88
|
+
description: Accept Mobile money, cards, crypto, invoices, QR codes, and more with
|
|
89
|
+
Sunny Payments.
|
|
90
|
+
email:
|
|
91
|
+
- dev@sunnypay.co.ke
|
|
92
|
+
executables: []
|
|
93
|
+
extensions: []
|
|
94
|
+
extra_rdoc_files: []
|
|
95
|
+
files:
|
|
96
|
+
- LICENSE
|
|
97
|
+
- README.md
|
|
98
|
+
- lib/sunny.rb
|
|
99
|
+
- lib/sunny/client.rb
|
|
100
|
+
- lib/sunny/errors.rb
|
|
101
|
+
- lib/sunny/resources/bills.rb
|
|
102
|
+
- lib/sunny/resources/bnpl.rb
|
|
103
|
+
- lib/sunny/resources/crypto.rb
|
|
104
|
+
- lib/sunny/resources/customers.rb
|
|
105
|
+
- lib/sunny/resources/invoices.rb
|
|
106
|
+
- lib/sunny/resources/mobile_money.rb
|
|
107
|
+
- lib/sunny/resources/payments.rb
|
|
108
|
+
- lib/sunny/resources/qr_codes.rb
|
|
109
|
+
- lib/sunny/resources/refunds.rb
|
|
110
|
+
- lib/sunny/resources/virtual_accounts.rb
|
|
111
|
+
- lib/sunny/resources/webhooks.rb
|
|
112
|
+
- lib/sunny/version.rb
|
|
113
|
+
homepage: https://sunnypay.co.ke
|
|
114
|
+
licenses:
|
|
115
|
+
- MIT
|
|
116
|
+
metadata:
|
|
117
|
+
homepage_uri: https://sunnypay.co.ke
|
|
118
|
+
source_code_uri: https://github.com/SUNNYPAY-LIMITED/Sunny-Ruby-SDK
|
|
119
|
+
changelog_uri: https://github.com/SUNNYPAY-LIMITED/Sunny-Ruby-SDK/blob/main/CHANGELOG.md
|
|
120
|
+
rdoc_options: []
|
|
121
|
+
require_paths:
|
|
122
|
+
- lib
|
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: 2.7.0
|
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
requirements: []
|
|
134
|
+
rubygems_version: 3.6.7
|
|
135
|
+
specification_version: 4
|
|
136
|
+
summary: Official Ruby SDK for Sunny Payments
|
|
137
|
+
test_files: []
|