afconwave 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c800c0e3b437a939afb9da71063c62b0ddc93416ad54bf21d7004519c1aa33ee
4
+ data.tar.gz: 9e347465ac42a572ee570df78a3b5a574a41e6033dde417dde769f16ea890893
5
+ SHA512:
6
+ metadata.gz: 10fd15f5d2dfa7e9e92b66ff19a6562af5cebf4e31f353635e0509fee588f831234ad2d3dc2bec33bde58a10dd7966fe357782ab3fb51ff37c987e0262169851
7
+ data.tar.gz: 78c6052b92c89addbf87c4291c8b6ea9a2cbe0ee5495277a813280aa9f923cb8d78b25f967a124bb107b30b1196bb373ee45f46e17acd4fc934c4cbe10dae3b8
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AfconWave
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,144 @@
1
+ # afconwave โ€” Official Ruby SDK
2
+
3
+ > The official Ruby client library for the AfconWave Payments API.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/afconwave.svg)](https://badge.fury.io/rb/afconwave)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ---
9
+
10
+ ## Features
11
+
12
+ - โœ… Simple, clean Ruby API
13
+ - ๐ŸŒ Payments, Payouts, and Refunds
14
+ - ๐Ÿ”’ Secure HMAC-SHA256 signature verification
15
+ - ๐Ÿงช Sandbox-ready with test keys
16
+ - ๐Ÿ“ฆ Lightweight, zero external dependencies (uses `net/http`)
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'afconwave'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ Or install it directly:
35
+
36
+ ```bash
37
+ gem install afconwave
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Quick Start
43
+
44
+ ```ruby
45
+ require 'afconwave'
46
+
47
+ afw = AfconWave::Client.new(secret_key: 'sk_test_your_key_here')
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Usage Guide
53
+
54
+ ### Create a Payment
55
+
56
+ ```ruby
57
+ payment = afw.create_payment(
58
+ amount: 5000, # Amount in minor units (5000 = 50 XAF)
59
+ currency: 'XAF',
60
+ description: 'Order #1234',
61
+ callback_url: 'https://yoursite.com/payment/callback',
62
+ customer: {
63
+ name: 'Jean Dupont',
64
+ email: 'jean@example.com'
65
+ }
66
+ )
67
+
68
+ puts payment['checkout_url'] # Redirect user here
69
+ puts payment['id'] # e.g., pay_507f191e8180f
70
+ ```
71
+
72
+ ### Retrieve a Payment
73
+
74
+ ```ruby
75
+ payment = afw.retrieve_payment('pay_507f191e8180f')
76
+
77
+ puts payment['status'] # "pending" | "success" | "failed"
78
+ puts payment['amount']
79
+ ```
80
+
81
+ ### List Payments
82
+
83
+ ```ruby
84
+ result = afw.list_payments(limit: 20, status: 'success')
85
+
86
+ result['data'].each do |payment|
87
+ puts "#{payment['id']} - #{payment['amount']} #{payment['currency']}"
88
+ end
89
+ ```
90
+
91
+ ### Webhook Verification
92
+
93
+ ```ruby
94
+ # In a Rails controller
95
+ def webhook
96
+ payload = request.raw_post
97
+ signature = request.headers['X-AfconWave-Signature']
98
+ secret = ENV['AFCONWAVE_WEBHOOK_SECRET']
99
+
100
+ is_valid = AfconWave::Client.verify_webhook_signature(
101
+ payload: payload,
102
+ signature: signature,
103
+ secret: secret
104
+ )
105
+
106
+ if is_valid
107
+ event = JSON.parse(payload)
108
+ # Handle event...
109
+ render json: { status: 'ok' }, status: 200
110
+ else
111
+ render json: { error: 'Invalid signature' }, status: 400
112
+ end
113
+ end
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Error Handling
119
+
120
+ ```ruby
121
+ begin
122
+ payment = afw.create_payment(...)
123
+ rescue AfconWave::AuthError => e
124
+ puts "Invalid API Key: #{e.message}"
125
+ rescue AfconWave::Error => e
126
+ puts "API Error #{e.status_code}: #{e.message}"
127
+ end
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Configuration
133
+
134
+ | Parameter | Type | Default | Description |
135
+ |---|---|---|---|
136
+ | `secret_key` | `String` | **required** | Your AfconWave secret API key |
137
+ | `base_url` | `String` | `https://api.afconwave.com/v1` | API base URL |
138
+ | `timeout` | `Integer` | `30` | Request timeout in seconds |
139
+
140
+ ---
141
+
142
+ ## License
143
+
144
+ MIT ยฉ AfconWave
@@ -0,0 +1,3 @@
1
+ module AfconWave
2
+ VERSION = "1.0.0"
3
+ end
data/lib/afconwave.rb ADDED
@@ -0,0 +1,150 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'openssl'
5
+ require_relative 'afconwave/version'
6
+
7
+ module AfconWave
8
+ # Shortcut to create a new client
9
+ def self.new(secret_key:, **options)
10
+ Client.new(secret_key: secret_key, **options)
11
+ end
12
+ # โ”€โ”€โ”€ Exceptions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
13
+
14
+ class Error < StandardError
15
+ attr_reader :status_code, :code
16
+
17
+ def initialize(message, status_code: nil, code: nil)
18
+ @status_code = status_code
19
+ @code = code
20
+ super(message)
21
+ end
22
+ end
23
+
24
+ class AuthError < Error; end
25
+ class PaymentError < Error; end
26
+
27
+ # โ”€โ”€โ”€ Main Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
+
29
+ class Client
30
+ attr_accessor :secret_key, :base_url, :timeout
31
+
32
+ def initialize(secret_key:, base_url: 'https://api.afconwave.com/api/v1', timeout: 30)
33
+ @secret_key = secret_key
34
+ @base_url = base_url
35
+ @timeout = timeout
36
+ end
37
+
38
+ def self.verify_webhook_signature(payload:, signature:, secret:, tolerance: 300)
39
+ # 1. Verify Signature (timing-safe compare via OpenSSL stdlib)
40
+ expected = OpenSSL::HMAC.hexdigest('sha256', secret, payload)
41
+
42
+ # OpenSSL.fixed_length_secure_compare requires equal-length inputs.
43
+ return false unless signature.is_a?(String) && expected.bytesize == signature.bytesize
44
+ return false unless OpenSSL.fixed_length_secure_compare(expected, signature)
45
+
46
+ # 2. Verify Timestamp (Replay Protection)
47
+ begin
48
+ data = JSON.parse(payload)
49
+ if data['timestamp']
50
+ current_time = Time.now.to_i # seconds
51
+ webhook_time = data['timestamp'] / 1000 # convert ms to seconds
52
+ age = (current_time - webhook_time).abs
53
+
54
+ return false if age > tolerance
55
+ end
56
+ rescue JSON::ParserError
57
+ # Non-JSON payload, signature is valid but can't check timestamp
58
+ end
59
+
60
+ true
61
+ end
62
+
63
+ def payments; @payments ||= Resource::Payments.new(self); end
64
+ def payouts; @payouts ||= Resource::Payouts.new(self); end
65
+ def crypto; @crypto ||= Resource::Crypto.new(self); end
66
+ def refunds; @refunds ||= Resource::Refunds.new(self); end
67
+ def disputes; @disputes ||= Resource::Disputes.new(self); end
68
+
69
+ def request(method:, path:, data: nil, params: nil)
70
+ uri = URI("#{base_url}#{path}")
71
+ uri.query = URI.encode_www_form(params) if params
72
+
73
+ http = Net::HTTP.new(uri.host, uri.port)
74
+ http.use_ssl = true if uri.scheme == 'https'
75
+ http.read_timeout = @timeout
76
+
77
+ req = case method.upcase
78
+ when 'POST'
79
+ request = Net::HTTP::Post.new(uri)
80
+ request.body = data.to_json if data
81
+ request
82
+ when 'GET'
83
+ Net::HTTP::Get.new(uri)
84
+ else
85
+ raise Error.new("Unsupported method #{method}")
86
+ end
87
+
88
+ req['Authorization'] = "Bearer #{secret_key}"
89
+ req['Content-Type'] = 'application/json'
90
+ req['Accept'] = 'application/json'
91
+
92
+ response = http.request(req)
93
+ res_data = JSON.parse(response.body) rescue { 'error' => 'Invalid JSON response' }
94
+
95
+ unless response.is_a?(Net::HTTPSuccess)
96
+ case response.code.to_i
97
+ when 401 then raise AuthError.new(res_data['error'] || 'Invalid API Key', status_code: 401)
98
+ else raise Error.new(res_data['error'] || response.message, status_code: response.code.to_i, code: res_data['code'])
99
+ end
100
+ end
101
+
102
+ res_data['data'] || res_data
103
+ end
104
+
105
+ # โ”€โ”€โ”€ Top-level Convenience Methods (Matches README) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
106
+
107
+ def create_payment(**data); payments.create(**data); end
108
+ def retrieve_payment(id); payments.retrieve(id); end
109
+ def list_payments(**params); payments.list(**params); end
110
+ def create_payout(**data); payouts.create(**data); end
111
+ end
112
+
113
+ module Resource
114
+ class Base
115
+ def initialize(client); @client = client; end
116
+ end
117
+
118
+ class Payments < Base
119
+ def create(**data); @client.request(method: 'POST', path: '/payments', data: data); end
120
+ def retrieve(id); @client.request(method: 'GET', path: "/payments/#{id}"); end
121
+ def list(**params); @client.request(method: 'GET', path: '/payments', params: params); end
122
+ end
123
+
124
+ class Payouts < Base
125
+ def create(**data); @client.request(method: 'POST', path: '/payouts', data: data); end
126
+ def retrieve(id); @client.request(method: 'GET', path: "/payouts/#{id}"); end
127
+ end
128
+
129
+ class Crypto < Base
130
+ def buy(**data); @client.request(method: 'POST', path: '/crypto/buy', data: data); end
131
+ end
132
+
133
+ class Refunds < Base
134
+ def create(payment_id:, amount:, reason: nil)
135
+ @client.request(method: 'POST', path: '/refunds', data: { paymentId: payment_id, amount: amount, reason: reason })
136
+ end
137
+ def list; @client.request(method: 'GET', path: '/refunds'); end
138
+ end
139
+
140
+ class Disputes < Base
141
+ def open(transaction_id:, reason:, description:)
142
+ @client.request(method: 'POST', path: '/disputes', data: { transactionId: transaction_id, reason: reason, description: description })
143
+ end
144
+ def list; @client.request(method: 'GET', path: '/disputes'); end
145
+ def resolve(dispute_id:, resolution:, resolution_details: nil)
146
+ @client.request(method: 'POST', path: "/disputes/#{dispute_id}/resolve", data: { resolution: resolution, resolutionDetails: resolution_details })
147
+ end
148
+ end
149
+ end
150
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: afconwave
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - AfconWave Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Integrate AfconWave payments, payouts, and refunds into your Ruby or
14
+ Rails applications.
15
+ email:
16
+ - support@afconwave.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - lib/afconwave.rb
24
+ - lib/afconwave/version.rb
25
+ homepage: https://github.com/afconwave/afconwave-ruby
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: 2.5.0
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.4.19
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Official Ruby SDK for the AfconWave Payments API
48
+ test_files: []