malipopay 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +89 -0
- data/README.md +18 -18
- data/docs/configuration.md +22 -22
- data/docs/customers.md +5 -5
- data/docs/error-handling.md +32 -32
- data/docs/getting-started.md +12 -12
- data/docs/invoices.md +4 -4
- data/docs/payments.md +9 -9
- data/docs/sms.md +4 -4
- data/docs/webhooks.md +15 -15
- data/lib/malipopay/client.rb +12 -12
- data/lib/malipopay/errors.rb +2 -2
- data/lib/malipopay/http_client.rb +11 -11
- data/lib/malipopay/resources/account.rb +1 -1
- data/lib/malipopay/resources/customers.rb +1 -1
- data/lib/malipopay/resources/invoices.rb +1 -1
- data/lib/malipopay/resources/payments.rb +2 -2
- data/lib/malipopay/resources/products.rb +1 -1
- data/lib/malipopay/resources/references.rb +6 -6
- data/lib/malipopay/resources/sms.rb +1 -1
- data/lib/malipopay/resources/transactions.rb +1 -1
- data/lib/malipopay/version.rb +2 -2
- data/lib/malipopay/webhooks/verifier.rb +5 -5
- data/lib/malipopay.rb +3 -3
- data/malipopay.gemspec +3 -3
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e472a0e35ebdb2ad9aa143d0eb17be62a2f9b3c7bec38953c0f846c43c2e6fa4
|
|
4
|
+
data.tar.gz: c2ffc9936a1e8b99945f7e076cfb7347f56d51180ea9b3f047fd4ad9b9ebeda1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f52cb4533c575fee99dc51e84e2194d2a6879eb533899b15d936db30e0e599757886e1aae530aca77c9c847e13fd631b707898dae5587772e4cc311ed760b41
|
|
7
|
+
data.tar.gz: 1baff51a27700c5f6ea005ab114a1356e7dc0af9542e799c893697223027c9554862826988196c877133815f0f6c1fbd8f0ebaf0daa86d00f94cf6781410b6b7
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Malipopay SDK Development Rules
|
|
2
|
+
|
|
3
|
+
## Naming Convention — CRITICAL
|
|
4
|
+
|
|
5
|
+
The brand name is **Malipopay** (single capital M, all lowercase after). Never use "MaliPoPay" with capital P-o-P.
|
|
6
|
+
|
|
7
|
+
| Context | Correct | Wrong |
|
|
8
|
+
|---------|---------|-------|
|
|
9
|
+
| PascalCase class | `Malipopay` | `MaliPoPay` |
|
|
10
|
+
| Error class | `MalipopayError` | `MaliPoPayError` |
|
|
11
|
+
| Exception class | `MalipopayException` | `MaliPoPayException` |
|
|
12
|
+
| Config type | `MalipopayConfig` | `MaliPoPayConfig` |
|
|
13
|
+
| Options type | `MalipopayOptions` | `MaliPoPayOptions` |
|
|
14
|
+
| Client class | `MalipopayClient` | `MaliPoPayClient` |
|
|
15
|
+
| Module/package | `malipopay` | ✅ already correct |
|
|
16
|
+
| In docs/prose | "Malipopay" or "malipopay" | "MaliPoPay" |
|
|
17
|
+
| URLs/paths | `malipopay` | ✅ already correct |
|
|
18
|
+
|
|
19
|
+
### Per-Language Examples
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Node.js / TypeScript
|
|
23
|
+
import { Malipopay, MalipopayError } from "malipopay";
|
|
24
|
+
const client = new Malipopay("api-key");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# Python
|
|
29
|
+
from malipopay import Malipopay, AsyncMalipopay, MalipopayError
|
|
30
|
+
client = Malipopay("api-key")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```php
|
|
34
|
+
// PHP
|
|
35
|
+
use Malipopay\Malipopay;
|
|
36
|
+
use Malipopay\Exceptions\MalipopayException;
|
|
37
|
+
$client = new Malipopay("api-key");
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```java
|
|
41
|
+
// Java
|
|
42
|
+
import co.tz.malipopay.Malipopay;
|
|
43
|
+
import co.tz.malipopay.MalipopayConfig;
|
|
44
|
+
Malipopay client = new Malipopay("api-key");
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```csharp
|
|
48
|
+
// C# / .NET
|
|
49
|
+
using Malipopay;
|
|
50
|
+
var client = new MalipopayClient("api-key");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Ruby
|
|
55
|
+
require "malipopay"
|
|
56
|
+
client = Malipopay::Client.new(api_key: "api-key")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## SDK Architecture
|
|
60
|
+
|
|
61
|
+
- **Resource-oriented:** `client.payments.collect()`, `client.customers.create()`
|
|
62
|
+
- **Auth:** `apiToken` header (API key from dashboard)
|
|
63
|
+
- **Base URLs:**
|
|
64
|
+
- Production: `https://core-prod.malipopay.co.tz`
|
|
65
|
+
- UAT: `https://core-uat.malipopay.co.tz`
|
|
66
|
+
- **Error hierarchy:** `MalipopayError` → `AuthenticationError`, `ValidationError`, `NotFoundError`, `PermissionError`, `RateLimitError`, `ApiError`, `ConnectionError`
|
|
67
|
+
|
|
68
|
+
## GitHub Organization
|
|
69
|
+
|
|
70
|
+
https://github.com/Malipopay
|
|
71
|
+
|
|
72
|
+
## Package Registries
|
|
73
|
+
|
|
74
|
+
| Language | Package | Registry |
|
|
75
|
+
|----------|---------|----------|
|
|
76
|
+
| Node.js | `malipopay` | npm |
|
|
77
|
+
| Python | `malipopay` | PyPI |
|
|
78
|
+
| PHP | `malipopay/malipopay-php` | Packagist |
|
|
79
|
+
| Java | `co.tz.malipopay:malipopay-java` | Maven Central |
|
|
80
|
+
| .NET | `Malipopay` | NuGet |
|
|
81
|
+
| Ruby | `malipopay` | RubyGems |
|
|
82
|
+
|
|
83
|
+
## Company
|
|
84
|
+
|
|
85
|
+
- **Company:** Lockwood Technology Ltd
|
|
86
|
+
- **Product:** Malipopay
|
|
87
|
+
- **Website:** malipopay.co.tz
|
|
88
|
+
- **Developer Docs:** developers.malipopay.co.tz
|
|
89
|
+
- **Support:** support@malipopay.co.tz
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Malipopay Ruby SDK
|
|
2
2
|
|
|
3
|
-
Official Ruby SDK for the [
|
|
3
|
+
Official Ruby SDK for the [Malipopay](https://malipopay.co.tz) payment platform (Tanzania).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -27,7 +27,7 @@ gem install malipopay
|
|
|
27
27
|
```ruby
|
|
28
28
|
require "malipopay"
|
|
29
29
|
|
|
30
|
-
client =
|
|
30
|
+
client = Malipopay::Client.new(
|
|
31
31
|
api_key: "your_api_token",
|
|
32
32
|
environment: :production # or :uat for testing
|
|
33
33
|
)
|
|
@@ -37,7 +37,7 @@ client = MaliPoPay::Client.new(
|
|
|
37
37
|
|
|
38
38
|
| Option | Default | Description |
|
|
39
39
|
|------------------|---------------|--------------------------------------|
|
|
40
|
-
| `api_key` | *required* | Your
|
|
40
|
+
| `api_key` | *required* | Your Malipopay API token |
|
|
41
41
|
| `environment` | `:production` | `:production` or `:uat` |
|
|
42
42
|
| `base_url` | `nil` | Override the base URL |
|
|
43
43
|
| `timeout` | `30` | Request timeout in seconds |
|
|
@@ -201,7 +201,7 @@ client.account.trial_balance
|
|
|
201
201
|
client.sms.send_sms(
|
|
202
202
|
to: "255712345678",
|
|
203
203
|
message: "Your payment of TZS 10,000 has been received.",
|
|
204
|
-
senderId: "
|
|
204
|
+
senderId: "Malipopay"
|
|
205
205
|
)
|
|
206
206
|
|
|
207
207
|
# Send bulk SMS
|
|
@@ -210,14 +210,14 @@ client.sms.send_bulk(
|
|
|
210
210
|
{ to: "255712345678", message: "Hello John!" },
|
|
211
211
|
{ to: "255798765432", message: "Hello Jane!" }
|
|
212
212
|
],
|
|
213
|
-
senderId: "
|
|
213
|
+
senderId: "Malipopay"
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
# Schedule SMS
|
|
217
217
|
client.sms.schedule(
|
|
218
218
|
to: "255712345678",
|
|
219
219
|
message: "Reminder: Your invoice is due tomorrow.",
|
|
220
|
-
senderId: "
|
|
220
|
+
senderId: "Malipopay",
|
|
221
221
|
scheduledAt: "2026-04-15T09:00:00Z"
|
|
222
222
|
)
|
|
223
223
|
```
|
|
@@ -235,15 +235,15 @@ business_types = client.references.business_types
|
|
|
235
235
|
## Webhooks
|
|
236
236
|
|
|
237
237
|
```ruby
|
|
238
|
-
client =
|
|
238
|
+
client = Malipopay::Client.new(
|
|
239
239
|
api_key: "your_api_token",
|
|
240
240
|
webhook_secret: "whsec_your_secret"
|
|
241
241
|
)
|
|
242
242
|
|
|
243
243
|
# In your webhook endpoint (e.g., Sinatra, Rails controller)
|
|
244
244
|
payload = request.body.read
|
|
245
|
-
signature = request.headers["X-
|
|
246
|
-
timestamp = request.headers["X-
|
|
245
|
+
signature = request.headers["X-Malipopay-Signature"]
|
|
246
|
+
timestamp = request.headers["X-Malipopay-Timestamp"]
|
|
247
247
|
|
|
248
248
|
event = client.webhooks.construct_event(payload, signature, timestamp: timestamp)
|
|
249
249
|
|
|
@@ -260,20 +260,20 @@ end
|
|
|
260
260
|
```ruby
|
|
261
261
|
begin
|
|
262
262
|
client.payments.collect(amount: 10_000, phone: "255712345678")
|
|
263
|
-
rescue
|
|
263
|
+
rescue Malipopay::AuthenticationError => e
|
|
264
264
|
# Invalid API key (401)
|
|
265
|
-
rescue
|
|
265
|
+
rescue Malipopay::PermissionError => e
|
|
266
266
|
# Insufficient permissions (403)
|
|
267
|
-
rescue
|
|
267
|
+
rescue Malipopay::NotFoundError => e
|
|
268
268
|
# Resource not found (404)
|
|
269
|
-
rescue
|
|
269
|
+
rescue Malipopay::ValidationError => e
|
|
270
270
|
# Invalid parameters (400/422)
|
|
271
271
|
puts e.errors
|
|
272
|
-
rescue
|
|
272
|
+
rescue Malipopay::RateLimitError => e
|
|
273
273
|
# Rate limited (429) - retry after e.retry_after seconds
|
|
274
|
-
rescue
|
|
274
|
+
rescue Malipopay::ApiError => e
|
|
275
275
|
# Server error (5xx)
|
|
276
|
-
rescue
|
|
276
|
+
rescue Malipopay::ConnectionError => e
|
|
277
277
|
# Network error
|
|
278
278
|
end
|
|
279
279
|
```
|
|
@@ -305,7 +305,7 @@ MIT License - Copyright (c) 2026 [Lockwood Technology Ltd](https://lockwood.co.t
|
|
|
305
305
|
| [Python](https://github.com/Malipopay/malipopay-python) | `pip install malipopay` |
|
|
306
306
|
| [PHP](https://github.com/Malipopay/malipopay-php) | `composer require malipopay/malipopay-php` |
|
|
307
307
|
| [Java](https://github.com/Malipopay/malipopay-java) | Maven / Gradle |
|
|
308
|
-
| [.NET](https://github.com/Malipopay/malipopay-dotnet) | `dotnet add package
|
|
308
|
+
| [.NET](https://github.com/Malipopay/malipopay-dotnet) | `dotnet add package Malipopay` |
|
|
309
309
|
| [Ruby](https://github.com/Malipopay/malipopay-ruby) | `gem install malipopay` |
|
|
310
310
|
|
|
311
311
|
[API Reference](https://developers.malipopay.co.tz) | [OpenAPI Spec](https://github.com/Malipopay/malipopay-openapi) | [Test Scenarios](https://github.com/Malipopay/malipopay-sdk-tests)
|
data/docs/configuration.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Configuration
|
|
2
2
|
|
|
3
|
-
This guide covers all the ways to configure the
|
|
3
|
+
This guide covers all the ways to configure the Malipopay Ruby SDK, from basic client setup to Rails integration.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Malipopay::Client Options
|
|
6
6
|
|
|
7
|
-
The `
|
|
7
|
+
The `Malipopay::Client.new` constructor accepts the following options:
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
|
-
client =
|
|
10
|
+
client = Malipopay::Client.new(
|
|
11
11
|
api_key: 'your_api_key',
|
|
12
12
|
environment: :production, # or :uat
|
|
13
13
|
base_url: nil, # overrides environment if set
|
|
@@ -21,7 +21,7 @@ client = MaliPoPay::Client.new(
|
|
|
21
21
|
|
|
22
22
|
| Option | Type | Default | Description |
|
|
23
23
|
|--------|------|---------|-------------|
|
|
24
|
-
| `api_key` | `String` | *required* | Your
|
|
24
|
+
| `api_key` | `String` | *required* | Your Malipopay API key |
|
|
25
25
|
| `environment` | `Symbol` | `:production` | `:production` or `:uat` |
|
|
26
26
|
| `base_url` | `String` | `nil` | Custom base URL; overrides `environment` when set |
|
|
27
27
|
| `timeout` | `Integer` | `30` | HTTP request timeout in seconds |
|
|
@@ -41,10 +41,10 @@ The SDK accepts both symbols and strings for the `environment` option. Symbols a
|
|
|
41
41
|
|
|
42
42
|
```ruby
|
|
43
43
|
# Preferred (symbol)
|
|
44
|
-
client =
|
|
44
|
+
client = Malipopay::Client.new(api_key: key, environment: :uat)
|
|
45
45
|
|
|
46
46
|
# Also works (string)
|
|
47
|
-
client =
|
|
47
|
+
client = Malipopay::Client.new(api_key: key, environment: 'uat')
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
The same applies to provider names in payment methods -- you can use either:
|
|
@@ -61,13 +61,13 @@ client.payments.collect(provider: :'M-Pesa', ...)
|
|
|
61
61
|
|
|
62
62
|
```ruby
|
|
63
63
|
# Production with defaults (30s timeout, 2 retries)
|
|
64
|
-
client =
|
|
64
|
+
client = Malipopay::Client.new(api_key: ENV['MALIPOPAY_API_KEY'])
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### UAT for Testing
|
|
68
68
|
|
|
69
69
|
```ruby
|
|
70
|
-
client =
|
|
70
|
+
client = Malipopay::Client.new(
|
|
71
71
|
api_key: ENV['MALIPOPAY_UAT_API_KEY'],
|
|
72
72
|
environment: :uat
|
|
73
73
|
)
|
|
@@ -76,7 +76,7 @@ client = MaliPoPay::Client.new(
|
|
|
76
76
|
### Custom Timeout and Retries
|
|
77
77
|
|
|
78
78
|
```ruby
|
|
79
|
-
client =
|
|
79
|
+
client = Malipopay::Client.new(
|
|
80
80
|
api_key: ENV['MALIPOPAY_API_KEY'],
|
|
81
81
|
environment: :production,
|
|
82
82
|
timeout: 60, # longer timeout for slow networks
|
|
@@ -88,7 +88,7 @@ client = MaliPoPay::Client.new(
|
|
|
88
88
|
|
|
89
89
|
### Automatic Based on RACK_ENV / RAILS_ENV
|
|
90
90
|
|
|
91
|
-
Tie the
|
|
91
|
+
Tie the Malipopay environment to your application environment:
|
|
92
92
|
|
|
93
93
|
```ruby
|
|
94
94
|
malipopay_env = if ENV['RACK_ENV'] == 'production' || ENV['RAILS_ENV'] == 'production'
|
|
@@ -103,7 +103,7 @@ api_key = if malipopay_env == :production
|
|
|
103
103
|
ENV.fetch('MALIPOPAY_UAT_API_KEY')
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
client =
|
|
106
|
+
client = Malipopay::Client.new(
|
|
107
107
|
api_key: api_key,
|
|
108
108
|
environment: malipopay_env
|
|
109
109
|
)
|
|
@@ -114,7 +114,7 @@ client = MaliPoPay::Client.new(
|
|
|
114
114
|
For proxies or custom routing:
|
|
115
115
|
|
|
116
116
|
```ruby
|
|
117
|
-
client =
|
|
117
|
+
client = Malipopay::Client.new(
|
|
118
118
|
api_key: ENV['MALIPOPAY_API_KEY'],
|
|
119
119
|
base_url: 'https://custom-proxy.example.com'
|
|
120
120
|
)
|
|
@@ -131,7 +131,7 @@ Create a Rails initializer to configure the client globally:
|
|
|
131
131
|
```ruby
|
|
132
132
|
# config/initializers/malipopay.rb
|
|
133
133
|
|
|
134
|
-
MALIPOPAY_CLIENT =
|
|
134
|
+
MALIPOPAY_CLIENT = Malipopay::Client.new(
|
|
135
135
|
api_key: Rails.application.credentials.dig(:malipopay, :api_key) ||
|
|
136
136
|
ENV.fetch('MALIPOPAY_API_KEY'),
|
|
137
137
|
environment: Rails.env.production? ? :production : :uat,
|
|
@@ -161,7 +161,7 @@ class PaymentsController < ApplicationController
|
|
|
161
161
|
else
|
|
162
162
|
render json: { error: result['message'] }, status: :unprocessable_entity
|
|
163
163
|
end
|
|
164
|
-
rescue
|
|
164
|
+
rescue Malipopay::Error => e
|
|
165
165
|
render json: { error: e.message }, status: :service_unavailable
|
|
166
166
|
end
|
|
167
167
|
end
|
|
@@ -175,7 +175,7 @@ Store your API keys securely with Rails credentials:
|
|
|
175
175
|
EDITOR=vim rails credentials:edit
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
Add your
|
|
178
|
+
Add your Malipopay keys:
|
|
179
179
|
|
|
180
180
|
```yaml
|
|
181
181
|
malipopay:
|
|
@@ -224,7 +224,7 @@ require 'sinatra'
|
|
|
224
224
|
require 'malipopay'
|
|
225
225
|
|
|
226
226
|
configure do
|
|
227
|
-
set :malipopay,
|
|
227
|
+
set :malipopay, Malipopay::Client.new(
|
|
228
228
|
api_key: ENV.fetch('MALIPOPAY_API_KEY'),
|
|
229
229
|
environment: settings.production? ? :production : :uat
|
|
230
230
|
)
|
|
@@ -248,14 +248,14 @@ end
|
|
|
248
248
|
|
|
249
249
|
### Timeout
|
|
250
250
|
|
|
251
|
-
The `timeout` option controls how long the SDK waits for an API response before raising `
|
|
251
|
+
The `timeout` option controls how long the SDK waits for an API response before raising `Malipopay::ConnectionError`:
|
|
252
252
|
|
|
253
253
|
```ruby
|
|
254
254
|
# Short timeout for fast-fail scenarios
|
|
255
|
-
client =
|
|
255
|
+
client = Malipopay::Client.new(api_key: key, timeout: 10)
|
|
256
256
|
|
|
257
257
|
# Longer timeout for slow networks or large batch operations
|
|
258
|
-
client =
|
|
258
|
+
client = Malipopay::Client.new(api_key: key, timeout: 120)
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
### Retries
|
|
@@ -264,10 +264,10 @@ The `retries` option controls how many times the SDK retries on transient errors
|
|
|
264
264
|
|
|
265
265
|
```ruby
|
|
266
266
|
# No automatic retries (handle retries yourself)
|
|
267
|
-
client =
|
|
267
|
+
client = Malipopay::Client.new(api_key: key, retries: 0)
|
|
268
268
|
|
|
269
269
|
# Aggressive retries for critical operations
|
|
270
|
-
client =
|
|
270
|
+
client = Malipopay::Client.new(api_key: key, retries: 5)
|
|
271
271
|
```
|
|
272
272
|
|
|
273
273
|
The SDK uses exponential backoff between retries (1s, 2s, 4s, ...).
|
data/docs/customers.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Customers
|
|
2
2
|
|
|
3
|
-
The `customers` resource lets you create, retrieve, search, and verify customer records. Customers are linked to payments, invoices, and transaction history in
|
|
3
|
+
The `customers` resource lets you create, retrieve, search, and verify customer records. Customers are linked to payments, invoices, and transaction history in Malipopay.
|
|
4
4
|
|
|
5
5
|
## Create a Customer
|
|
6
6
|
|
|
@@ -71,7 +71,7 @@ end
|
|
|
71
71
|
|
|
72
72
|
## Get a Customer by Customer Number
|
|
73
73
|
|
|
74
|
-
Look up using the
|
|
74
|
+
Look up using the Malipopay-assigned customer number:
|
|
75
75
|
|
|
76
76
|
```ruby
|
|
77
77
|
customer = client.customers.get_by_number('CUST-2024-001')
|
|
@@ -118,11 +118,11 @@ begin
|
|
|
118
118
|
name: 'Incomplete Customer'
|
|
119
119
|
# missing phone -- will trigger validation error
|
|
120
120
|
)
|
|
121
|
-
rescue
|
|
121
|
+
rescue Malipopay::ValidationError => e
|
|
122
122
|
puts "Missing required fields: #{e.message}"
|
|
123
|
-
rescue
|
|
123
|
+
rescue Malipopay::NotFoundError
|
|
124
124
|
puts 'Customer not found.'
|
|
125
|
-
rescue
|
|
125
|
+
rescue Malipopay::Error => e
|
|
126
126
|
puts "Error: #{e.message}"
|
|
127
127
|
end
|
|
128
128
|
```
|
data/docs/error-handling.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Error Handling
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The Malipopay Ruby SDK uses a structured exception hierarchy so you can rescue specific error types and respond appropriately. All exceptions inherit from `Malipopay::Error`.
|
|
4
4
|
|
|
5
5
|
## Exception Hierarchy
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
9
|
-
├──
|
|
10
|
-
├──
|
|
11
|
-
├──
|
|
12
|
-
├──
|
|
13
|
-
├──
|
|
14
|
-
├──
|
|
15
|
-
└──
|
|
8
|
+
Malipopay::Error (base)
|
|
9
|
+
├── Malipopay::AuthenticationError (HTTP 401 -- invalid or missing API key)
|
|
10
|
+
├── Malipopay::PermissionError (HTTP 403 -- insufficient permissions)
|
|
11
|
+
├── Malipopay::NotFoundError (HTTP 404 -- resource does not exist)
|
|
12
|
+
├── Malipopay::ValidationError (HTTP 422 -- invalid request parameters)
|
|
13
|
+
├── Malipopay::RateLimitError (HTTP 429 -- too many requests)
|
|
14
|
+
├── Malipopay::ApiError (HTTP 5xx -- server-side error)
|
|
15
|
+
└── Malipopay::ConnectionError (network timeout, DNS failure, etc.)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Rescuing Specific Exceptions
|
|
@@ -32,34 +32,34 @@ begin
|
|
|
32
32
|
|
|
33
33
|
puts "Collection initiated: #{result['reference']}"
|
|
34
34
|
|
|
35
|
-
rescue
|
|
35
|
+
rescue Malipopay::AuthenticationError
|
|
36
36
|
# API key is invalid or expired
|
|
37
37
|
puts 'Authentication failed. Rotate your API key at app.malipopay.co.tz'
|
|
38
38
|
|
|
39
|
-
rescue
|
|
39
|
+
rescue Malipopay::PermissionError
|
|
40
40
|
# API key lacks permission for this operation
|
|
41
41
|
puts 'Insufficient permissions. Check your API key scopes.'
|
|
42
42
|
|
|
43
|
-
rescue
|
|
43
|
+
rescue Malipopay::ValidationError => e
|
|
44
44
|
# The request had invalid fields
|
|
45
45
|
puts "Invalid request: #{e.message}"
|
|
46
46
|
# e.message might say: "phone must be a valid Tanzanian number (255xxxxxxxxx)"
|
|
47
47
|
|
|
48
|
-
rescue
|
|
48
|
+
rescue Malipopay::NotFoundError
|
|
49
49
|
puts 'The requested resource was not found.'
|
|
50
50
|
|
|
51
|
-
rescue
|
|
51
|
+
rescue Malipopay::RateLimitError
|
|
52
52
|
puts 'Too many requests. Back off and retry.'
|
|
53
53
|
|
|
54
|
-
rescue
|
|
55
|
-
#
|
|
54
|
+
rescue Malipopay::ApiError => e
|
|
55
|
+
# Malipopay server error -- transient, safe to retry
|
|
56
56
|
puts "Server error (#{e.message}). Retrying..."
|
|
57
57
|
|
|
58
|
-
rescue
|
|
58
|
+
rescue Malipopay::ConnectionError => e
|
|
59
59
|
# Network-level failure
|
|
60
60
|
puts "Connection failed: #{e.message}"
|
|
61
61
|
|
|
62
|
-
rescue
|
|
62
|
+
rescue Malipopay::Error => e
|
|
63
63
|
# Catch-all for any other SDK error
|
|
64
64
|
puts "Unexpected error: #{e.message}"
|
|
65
65
|
end
|
|
@@ -67,7 +67,7 @@ end
|
|
|
67
67
|
|
|
68
68
|
## Exception Properties
|
|
69
69
|
|
|
70
|
-
Every `
|
|
70
|
+
Every `Malipopay::Error` includes:
|
|
71
71
|
|
|
72
72
|
| Property | Type | Description |
|
|
73
73
|
|----------|------|-------------|
|
|
@@ -81,15 +81,15 @@ The SDK automatically retries transient errors (5xx, connection timeouts) based
|
|
|
81
81
|
### Built-in Retries
|
|
82
82
|
|
|
83
83
|
```ruby
|
|
84
|
-
client =
|
|
84
|
+
client = Malipopay::Client.new(
|
|
85
85
|
api_key: ENV['MALIPOPAY_API_KEY'],
|
|
86
86
|
retries: 3 # retry up to 3 times on transient failures (default: 2)
|
|
87
87
|
)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
The SDK uses exponential backoff between retries. It will only retry on:
|
|
91
|
-
- `
|
|
92
|
-
- `
|
|
91
|
+
- `Malipopay::ApiError` (5xx responses)
|
|
92
|
+
- `Malipopay::ConnectionError` (network timeouts and DNS failures)
|
|
93
93
|
|
|
94
94
|
It will **not** retry on:
|
|
95
95
|
- `AuthenticationError` (fix your API key)
|
|
@@ -106,7 +106,7 @@ def with_rate_limit_retry(max_retries: 3)
|
|
|
106
106
|
|
|
107
107
|
begin
|
|
108
108
|
yield
|
|
109
|
-
rescue
|
|
109
|
+
rescue Malipopay::RateLimitError
|
|
110
110
|
attempts += 1
|
|
111
111
|
raise if attempts > max_retries
|
|
112
112
|
|
|
@@ -133,7 +133,7 @@ end
|
|
|
133
133
|
### Generic Retry Helper
|
|
134
134
|
|
|
135
135
|
```ruby
|
|
136
|
-
def with_retry(max_retries: 3, on: [
|
|
136
|
+
def with_retry(max_retries: 3, on: [Malipopay::ApiError, Malipopay::ConnectionError])
|
|
137
137
|
attempts = 0
|
|
138
138
|
|
|
139
139
|
begin
|
|
@@ -186,7 +186,7 @@ end
|
|
|
186
186
|
| "amount must be greater than 0" | Zero or negative amount | Provide a positive integer amount in TZS |
|
|
187
187
|
| "provider is required" | Missing `provider` field | Specify one of: `M-Pesa`, `Airtel Money`, `Mixx`, `Halopesa`, `T-Pesa`, `CRDB`, `NMB` |
|
|
188
188
|
| "reference must be unique" | Duplicate reference string | Generate a unique reference per transaction |
|
|
189
|
-
| "currency must be TZS" | Unsupported currency |
|
|
189
|
+
| "currency must be TZS" | Unsupported currency | Malipopay currently supports TZS only |
|
|
190
190
|
|
|
191
191
|
### NotFoundError (404)
|
|
192
192
|
|
|
@@ -234,8 +234,8 @@ begin
|
|
|
234
234
|
reference: 'ORD-2024-300',
|
|
235
235
|
description: 'Logging example'
|
|
236
236
|
)
|
|
237
|
-
rescue
|
|
238
|
-
logger.error("
|
|
237
|
+
rescue Malipopay::Error => e
|
|
238
|
+
logger.error("Malipopay API error: status=#{e.status_code} message=#{e.message}")
|
|
239
239
|
raise
|
|
240
240
|
end
|
|
241
241
|
```
|
|
@@ -250,17 +250,17 @@ module MalipopayErrorHandling
|
|
|
250
250
|
extend ActiveSupport::Concern
|
|
251
251
|
|
|
252
252
|
included do
|
|
253
|
-
rescue_from
|
|
254
|
-
Rails.logger.error("
|
|
253
|
+
rescue_from Malipopay::AuthenticationError do |e|
|
|
254
|
+
Rails.logger.error("Malipopay auth error: #{e.message}")
|
|
255
255
|
render json: { error: 'Payment service authentication failed' }, status: :service_unavailable
|
|
256
256
|
end
|
|
257
257
|
|
|
258
|
-
rescue_from
|
|
258
|
+
rescue_from Malipopay::ValidationError do |e|
|
|
259
259
|
render json: { error: e.message }, status: :unprocessable_entity
|
|
260
260
|
end
|
|
261
261
|
|
|
262
|
-
rescue_from
|
|
263
|
-
Rails.logger.error("
|
|
262
|
+
rescue_from Malipopay::Error do |e|
|
|
263
|
+
Rails.logger.error("Malipopay error: #{e.class} - #{e.message}")
|
|
264
264
|
render json: { error: 'Payment service error' }, status: :service_unavailable
|
|
265
265
|
end
|
|
266
266
|
end
|
data/docs/getting-started.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Getting Started with
|
|
1
|
+
# Getting Started with Malipopay Ruby SDK
|
|
2
2
|
|
|
3
3
|
## Prerequisites
|
|
4
4
|
|
|
5
5
|
- **Ruby 3.0** or later
|
|
6
6
|
- **Bundler** (included with Ruby)
|
|
7
|
-
- A
|
|
7
|
+
- A Malipopay merchant account with API credentials
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
Add
|
|
11
|
+
Add Malipopay to your Gemfile:
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
14
|
gem 'malipopay'
|
|
@@ -43,7 +43,7 @@ Collect TZS 50,000 from an M-Pesa customer in five lines:
|
|
|
43
43
|
```ruby
|
|
44
44
|
require 'malipopay'
|
|
45
45
|
|
|
46
|
-
client =
|
|
46
|
+
client = Malipopay::Client.new(api_key: ENV['MALIPOPAY_API_KEY'])
|
|
47
47
|
|
|
48
48
|
result = client.payments.collect(
|
|
49
49
|
amount: 50_000,
|
|
@@ -61,7 +61,7 @@ When this runs, the customer at `255712345678` receives a USSD push prompt on th
|
|
|
61
61
|
|
|
62
62
|
## Environment Selection
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Malipopay provides two environments:
|
|
65
65
|
|
|
66
66
|
| Environment | Base URL | Purpose |
|
|
67
67
|
|-------------|----------|---------|
|
|
@@ -73,7 +73,7 @@ MaliPoPay provides two environments:
|
|
|
73
73
|
Always develop and test against UAT before going live:
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
|
-
client =
|
|
76
|
+
client = Malipopay::Client.new(
|
|
77
77
|
api_key: ENV['MALIPOPAY_UAT_API_KEY'],
|
|
78
78
|
environment: :uat
|
|
79
79
|
)
|
|
@@ -84,7 +84,7 @@ client = MaliPoPay::Client.new(
|
|
|
84
84
|
For advanced setups (proxies, custom routing), you can override the base URL:
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
|
-
client =
|
|
87
|
+
client = Malipopay::Client.new(
|
|
88
88
|
api_key: ENV['MALIPOPAY_API_KEY'],
|
|
89
89
|
base_url: 'https://custom-proxy.example.com'
|
|
90
90
|
)
|
|
@@ -97,7 +97,7 @@ When `base_url` is set, it takes precedence over the `environment` setting.
|
|
|
97
97
|
The SDK automatically retries transient failures. You can adjust timeout and retry behavior:
|
|
98
98
|
|
|
99
99
|
```ruby
|
|
100
|
-
client =
|
|
100
|
+
client = Malipopay::Client.new(
|
|
101
101
|
api_key: ENV['MALIPOPAY_API_KEY'],
|
|
102
102
|
environment: :production,
|
|
103
103
|
timeout: 60, # seconds (default: 30)
|
|
@@ -114,7 +114,7 @@ require 'malipopay'
|
|
|
114
114
|
|
|
115
115
|
api_key = ENV.fetch('MALIPOPAY_API_KEY') { raise 'Set the MALIPOPAY_API_KEY environment variable' }
|
|
116
116
|
|
|
117
|
-
client =
|
|
117
|
+
client = Malipopay::Client.new(
|
|
118
118
|
api_key: api_key,
|
|
119
119
|
environment: :uat
|
|
120
120
|
)
|
|
@@ -143,11 +143,11 @@ begin
|
|
|
143
143
|
|
|
144
144
|
puts "Payment status: #{verification['status']}"
|
|
145
145
|
|
|
146
|
-
rescue
|
|
146
|
+
rescue Malipopay::AuthenticationError
|
|
147
147
|
puts 'Invalid API key. Check your credentials.'
|
|
148
|
-
rescue
|
|
148
|
+
rescue Malipopay::ValidationError => e
|
|
149
149
|
puts "Invalid request: #{e.message}"
|
|
150
|
-
rescue
|
|
150
|
+
rescue Malipopay::Error => e
|
|
151
151
|
puts "Payment error: #{e.message}"
|
|
152
152
|
end
|
|
153
153
|
```
|
data/docs/invoices.md
CHANGED
|
@@ -39,7 +39,7 @@ In this example, the subtotal is TZS 3,175,000 (2,500,000 + 75,000 + 600,000), a
|
|
|
39
39
|
|
|
40
40
|
## Tax Calculation
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Malipopay calculates tax automatically based on the `tax_rate` you provide:
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
45
|
# Invoice with 18% VAT (Tanzania standard rate)
|
|
@@ -184,11 +184,11 @@ begin
|
|
|
184
184
|
{ description: 'Test', quantity: 1, unit_price: 1_000 }
|
|
185
185
|
]
|
|
186
186
|
)
|
|
187
|
-
rescue
|
|
187
|
+
rescue Malipopay::ValidationError => e
|
|
188
188
|
puts "Invalid invoice data: #{e.message}"
|
|
189
|
-
rescue
|
|
189
|
+
rescue Malipopay::NotFoundError
|
|
190
190
|
puts 'Customer not found. Create the customer first.'
|
|
191
|
-
rescue
|
|
191
|
+
rescue Malipopay::Error => e
|
|
192
192
|
puts "Invoice error: #{e.message}"
|
|
193
193
|
end
|
|
194
194
|
```
|
data/docs/payments.md
CHANGED
|
@@ -4,7 +4,7 @@ The `payments` resource handles all payment operations: mobile money collections
|
|
|
4
4
|
|
|
5
5
|
## How Payments Work in Tanzania
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Malipopay integrates with Tanzania's major mobile money and banking networks:
|
|
8
8
|
|
|
9
9
|
- **Vodacom M-Pesa** -- largest mobile money network
|
|
10
10
|
- **Airtel Money** -- second-largest MNO
|
|
@@ -18,16 +18,16 @@ MaliPoPay integrates with Tanzania's major mobile money and banking networks:
|
|
|
18
18
|
### Collection Flow
|
|
19
19
|
|
|
20
20
|
1. Your app calls `collect` with the customer's phone number and amount
|
|
21
|
-
2.
|
|
21
|
+
2. Malipopay sends a USSD push to the customer's phone
|
|
22
22
|
3. The customer sees a prompt like: *"Pay TZS 50,000 to ACME Ltd? Enter PIN to confirm"*
|
|
23
23
|
4. The customer enters their mobile money PIN
|
|
24
|
-
5.
|
|
24
|
+
5. Malipopay receives the confirmation and notifies you via webhook
|
|
25
25
|
6. You can also poll using `verify`
|
|
26
26
|
|
|
27
27
|
### Disbursement Flow
|
|
28
28
|
|
|
29
29
|
1. Your app calls `disburse` with the recipient's phone/account and amount
|
|
30
|
-
2.
|
|
30
|
+
2. Malipopay processes the transfer from your merchant float
|
|
31
31
|
3. The recipient receives the funds in their mobile money or bank account
|
|
32
32
|
4. You receive a webhook notification with the result
|
|
33
33
|
|
|
@@ -259,19 +259,19 @@ begin
|
|
|
259
259
|
reference: 'ORDER-001',
|
|
260
260
|
description: 'Test payment'
|
|
261
261
|
)
|
|
262
|
-
rescue
|
|
262
|
+
rescue Malipopay::ValidationError => e
|
|
263
263
|
# Invalid parameters (wrong phone format, missing fields, etc.)
|
|
264
264
|
puts "Validation error: #{e.message}"
|
|
265
|
-
rescue
|
|
265
|
+
rescue Malipopay::AuthenticationError
|
|
266
266
|
# Bad API key
|
|
267
267
|
puts 'Check your API key.'
|
|
268
|
-
rescue
|
|
268
|
+
rescue Malipopay::RateLimitError
|
|
269
269
|
# Too many requests -- back off and retry
|
|
270
270
|
puts 'Rate limited. Please wait and retry.'
|
|
271
|
-
rescue
|
|
271
|
+
rescue Malipopay::ConnectionError => e
|
|
272
272
|
# Network issue
|
|
273
273
|
puts "Network error: #{e.message}"
|
|
274
|
-
rescue
|
|
274
|
+
rescue Malipopay::Error => e
|
|
275
275
|
# Catch-all for other SDK errors
|
|
276
276
|
puts "Payment error: #{e.message}"
|
|
277
277
|
end
|
data/docs/sms.md
CHANGED
|
@@ -29,9 +29,9 @@ begin
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
puts "Message delivered: #{sms['data']}"
|
|
32
|
-
rescue
|
|
32
|
+
rescue Malipopay::ValidationError => e
|
|
33
33
|
puts "Invalid request: #{e.message}"
|
|
34
|
-
rescue
|
|
34
|
+
rescue Malipopay::Error => e
|
|
35
35
|
puts "SMS error: #{e.message}"
|
|
36
36
|
end
|
|
37
37
|
```
|
|
@@ -122,12 +122,12 @@ The `sender_id` field controls what appears as the sender on the recipient's pho
|
|
|
122
122
|
|
|
123
123
|
| Sender ID | Description |
|
|
124
124
|
|-----------|-------------|
|
|
125
|
-
| `MALIPOPAY` | Default
|
|
125
|
+
| `MALIPOPAY` | Default Malipopay sender ID |
|
|
126
126
|
| Custom (e.g., `ACME`) | Your registered brand name |
|
|
127
127
|
|
|
128
128
|
### Registering a Custom Sender ID
|
|
129
129
|
|
|
130
|
-
Custom sender IDs must be registered and approved in your
|
|
130
|
+
Custom sender IDs must be registered and approved in your Malipopay dashboard:
|
|
131
131
|
|
|
132
132
|
1. Go to [app.malipopay.co.tz](https://app.malipopay.co.tz) > **Settings > SMS > Sender IDs**
|
|
133
133
|
2. Click **Request New Sender ID**
|
data/docs/webhooks.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Webhooks
|
|
2
2
|
|
|
3
|
-
Webhooks let you receive real-time notifications when events happen in
|
|
3
|
+
Webhooks let you receive real-time notifications when events happen in Malipopay -- payment completed, payment failed, disbursement processed, etc. Instead of polling the API, you register a URL and Malipopay sends HTTP POST requests to it.
|
|
4
4
|
|
|
5
5
|
## How Webhooks Work
|
|
6
6
|
|
|
7
|
-
1. You register a webhook URL in your
|
|
8
|
-
2.
|
|
9
|
-
3. When an event occurs,
|
|
7
|
+
1. You register a webhook URL in your Malipopay dashboard at [app.malipopay.co.tz](https://app.malipopay.co.tz) under **Settings > Webhooks**
|
|
8
|
+
2. Malipopay generates a **webhook signing secret** for you
|
|
9
|
+
3. When an event occurs, Malipopay sends a POST request to your URL with:
|
|
10
10
|
- The event payload as JSON in the request body
|
|
11
|
-
- An `X-
|
|
11
|
+
- An `X-Malipopay-Signature` header containing the HMAC-SHA256 signature
|
|
12
12
|
4. Your endpoint verifies the signature and processes the event
|
|
13
13
|
|
|
14
14
|
## Event Types
|
|
@@ -32,7 +32,7 @@ require 'json'
|
|
|
32
32
|
require 'malipopay'
|
|
33
33
|
|
|
34
34
|
WEBHOOK_SECRET = ENV.fetch('MALIPOPAY_WEBHOOK_SECRET')
|
|
35
|
-
verifier =
|
|
35
|
+
verifier = Malipopay::Webhooks::Verifier.new(WEBHOOK_SECRET)
|
|
36
36
|
|
|
37
37
|
post '/webhooks/malipopay' do
|
|
38
38
|
payload = request.body.read
|
|
@@ -63,7 +63,7 @@ post '/webhooks/malipopay' do
|
|
|
63
63
|
|
|
64
64
|
status 200
|
|
65
65
|
'OK'
|
|
66
|
-
rescue
|
|
66
|
+
rescue Malipopay::Error => e
|
|
67
67
|
puts "Webhook verification failed: #{e.message}"
|
|
68
68
|
halt 401, 'Invalid signature'
|
|
69
69
|
end
|
|
@@ -72,7 +72,7 @@ end
|
|
|
72
72
|
|
|
73
73
|
## Rails Controller Example
|
|
74
74
|
|
|
75
|
-
A full Rails controller for handling
|
|
75
|
+
A full Rails controller for handling Malipopay webhooks:
|
|
76
76
|
|
|
77
77
|
```ruby
|
|
78
78
|
# app/controllers/webhooks/malipopay_controller.rb
|
|
@@ -82,7 +82,7 @@ module Webhooks
|
|
|
82
82
|
|
|
83
83
|
def create
|
|
84
84
|
payload = request.body.read
|
|
85
|
-
signature = request.headers['X-
|
|
85
|
+
signature = request.headers['X-Malipopay-Signature']
|
|
86
86
|
|
|
87
87
|
unless signature.present?
|
|
88
88
|
render json: { error: 'Missing signature' }, status: :bad_request
|
|
@@ -93,7 +93,7 @@ module Webhooks
|
|
|
93
93
|
event = webhook_verifier.construct_event(payload, signature)
|
|
94
94
|
handle_event(event)
|
|
95
95
|
head :ok
|
|
96
|
-
rescue
|
|
96
|
+
rescue Malipopay::Error => e
|
|
97
97
|
Rails.logger.error("Webhook verification failed: #{e.message}")
|
|
98
98
|
head :unauthorized
|
|
99
99
|
end
|
|
@@ -102,7 +102,7 @@ module Webhooks
|
|
|
102
102
|
private
|
|
103
103
|
|
|
104
104
|
def webhook_verifier
|
|
105
|
-
@webhook_verifier ||=
|
|
105
|
+
@webhook_verifier ||= Malipopay::Webhooks::Verifier.new(
|
|
106
106
|
ENV.fetch('MALIPOPAY_WEBHOOK_SECRET')
|
|
107
107
|
)
|
|
108
108
|
end
|
|
@@ -164,12 +164,12 @@ end
|
|
|
164
164
|
|
|
165
165
|
## Signature Verification
|
|
166
166
|
|
|
167
|
-
Every webhook request includes an `X-
|
|
167
|
+
Every webhook request includes an `X-Malipopay-Signature` header. The signature is an HMAC-SHA256 hash of the raw request body, signed with your webhook secret.
|
|
168
168
|
|
|
169
|
-
The `
|
|
169
|
+
The `Malipopay::Webhooks::Verifier` handles this for you:
|
|
170
170
|
|
|
171
171
|
```ruby
|
|
172
|
-
verifier =
|
|
172
|
+
verifier = Malipopay::Webhooks::Verifier.new('your_webhook_secret')
|
|
173
173
|
|
|
174
174
|
# Just verify (returns true/false)
|
|
175
175
|
valid = verifier.verify(payload, signature)
|
|
@@ -195,7 +195,7 @@ end
|
|
|
195
195
|
|
|
196
196
|
1. **Always verify signatures.** Never process a webhook without checking the signature. This prevents spoofed requests.
|
|
197
197
|
|
|
198
|
-
2. **Return 200 quickly.** Process the event asynchronously if needed (e.g., with Sidekiq or ActiveJob).
|
|
198
|
+
2. **Return 200 quickly.** Process the event asynchronously if needed (e.g., with Sidekiq or ActiveJob). Malipopay expects a response within 30 seconds. If you don't return 200, the webhook will be retried.
|
|
199
199
|
|
|
200
200
|
3. **Handle duplicates.** Webhooks may be delivered more than once. Use the `reference` or `transaction_id` as an idempotency key.
|
|
201
201
|
|
data/lib/malipopay/client.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Malipopay
|
|
4
4
|
class Client
|
|
5
5
|
attr_reader :http_client, :webhook_secret
|
|
6
6
|
|
|
7
|
-
# Initialize a new
|
|
7
|
+
# Initialize a new Malipopay client
|
|
8
8
|
#
|
|
9
|
-
# @param api_key [String] Your
|
|
9
|
+
# @param api_key [String] Your Malipopay API token
|
|
10
10
|
# @param environment [Symbol] :production or :uat (default: :production)
|
|
11
11
|
# @param base_url [String, nil] Override the base URL
|
|
12
12
|
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
@@ -25,47 +25,47 @@ module MaliPoPay
|
|
|
25
25
|
@webhook_secret = webhook_secret
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# @return [
|
|
28
|
+
# @return [Malipopay::Resources::Payments]
|
|
29
29
|
def payments
|
|
30
30
|
@payments ||= Resources::Payments.new(@http_client)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# @return [
|
|
33
|
+
# @return [Malipopay::Resources::Customers]
|
|
34
34
|
def customers
|
|
35
35
|
@customers ||= Resources::Customers.new(@http_client)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# @return [
|
|
38
|
+
# @return [Malipopay::Resources::Invoices]
|
|
39
39
|
def invoices
|
|
40
40
|
@invoices ||= Resources::Invoices.new(@http_client)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# @return [
|
|
43
|
+
# @return [Malipopay::Resources::Products]
|
|
44
44
|
def products
|
|
45
45
|
@products ||= Resources::Products.new(@http_client)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
# @return [
|
|
48
|
+
# @return [Malipopay::Resources::Transactions]
|
|
49
49
|
def transactions
|
|
50
50
|
@transactions ||= Resources::Transactions.new(@http_client)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
# @return [
|
|
53
|
+
# @return [Malipopay::Resources::Account]
|
|
54
54
|
def account
|
|
55
55
|
@account ||= Resources::Account.new(@http_client)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# @return [
|
|
58
|
+
# @return [Malipopay::Resources::Sms]
|
|
59
59
|
def sms
|
|
60
60
|
@sms ||= Resources::Sms.new(@http_client)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# @return [
|
|
63
|
+
# @return [Malipopay::Resources::References]
|
|
64
64
|
def references
|
|
65
65
|
@references ||= Resources::References.new(@http_client)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
# @return [
|
|
68
|
+
# @return [Malipopay::Webhooks::Verifier]
|
|
69
69
|
# @raise [ArgumentError] if webhook_secret was not provided
|
|
70
70
|
def webhooks
|
|
71
71
|
raise ArgumentError, "webhook_secret is required for webhook verification" unless @webhook_secret
|
data/lib/malipopay/errors.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "faraday"
|
|
|
4
4
|
require "faraday/retry"
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Malipopay
|
|
8
8
|
class HttpClient
|
|
9
9
|
BASE_URLS = {
|
|
10
10
|
production: "https://core-prod.malipopay.co.tz",
|
|
@@ -59,7 +59,7 @@ module MaliPoPay
|
|
|
59
59
|
conn.headers["apiToken"] = @api_key
|
|
60
60
|
conn.headers["Content-Type"] = "application/json"
|
|
61
61
|
conn.headers["Accept"] = "application/json"
|
|
62
|
-
conn.headers["User-Agent"] = "malipopay-ruby/#{
|
|
62
|
+
conn.headers["User-Agent"] = "malipopay-ruby/#{Malipopay::VERSION}"
|
|
63
63
|
|
|
64
64
|
conn.options.timeout = @timeout
|
|
65
65
|
conn.options.open_timeout = 10
|
|
@@ -82,9 +82,9 @@ module MaliPoPay
|
|
|
82
82
|
|
|
83
83
|
handle_response(response)
|
|
84
84
|
rescue Faraday::ConnectionFailed => e
|
|
85
|
-
raise
|
|
85
|
+
raise Malipopay::ConnectionError.new("Connection failed: #{e.message}")
|
|
86
86
|
rescue Faraday::TimeoutError => e
|
|
87
|
-
raise
|
|
87
|
+
raise Malipopay::ConnectionError.new("Request timed out: #{e.message}")
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def handle_response(response)
|
|
@@ -92,46 +92,46 @@ module MaliPoPay
|
|
|
92
92
|
when 200..299
|
|
93
93
|
response.body
|
|
94
94
|
when 400
|
|
95
|
-
raise
|
|
95
|
+
raise Malipopay::ValidationError.new(
|
|
96
96
|
error_message(response),
|
|
97
97
|
errors: response.body&.dig("errors"),
|
|
98
98
|
http_status: response.status,
|
|
99
99
|
response_body: response.body
|
|
100
100
|
)
|
|
101
101
|
when 401
|
|
102
|
-
raise
|
|
102
|
+
raise Malipopay::AuthenticationError.new(
|
|
103
103
|
error_message(response),
|
|
104
104
|
http_status: response.status,
|
|
105
105
|
response_body: response.body
|
|
106
106
|
)
|
|
107
107
|
when 403
|
|
108
|
-
raise
|
|
108
|
+
raise Malipopay::PermissionError.new(
|
|
109
109
|
error_message(response),
|
|
110
110
|
http_status: response.status,
|
|
111
111
|
response_body: response.body
|
|
112
112
|
)
|
|
113
113
|
when 404
|
|
114
|
-
raise
|
|
114
|
+
raise Malipopay::NotFoundError.new(
|
|
115
115
|
error_message(response),
|
|
116
116
|
http_status: response.status,
|
|
117
117
|
response_body: response.body
|
|
118
118
|
)
|
|
119
119
|
when 422
|
|
120
|
-
raise
|
|
120
|
+
raise Malipopay::ValidationError.new(
|
|
121
121
|
error_message(response),
|
|
122
122
|
errors: response.body&.dig("errors"),
|
|
123
123
|
http_status: response.status,
|
|
124
124
|
response_body: response.body
|
|
125
125
|
)
|
|
126
126
|
when 429
|
|
127
|
-
raise
|
|
127
|
+
raise Malipopay::RateLimitError.new(
|
|
128
128
|
error_message(response),
|
|
129
129
|
retry_after: response.headers["Retry-After"]&.to_i,
|
|
130
130
|
http_status: response.status,
|
|
131
131
|
response_body: response.body
|
|
132
132
|
)
|
|
133
133
|
else
|
|
134
|
-
raise
|
|
134
|
+
raise Malipopay::ApiError.new(
|
|
135
135
|
error_message(response),
|
|
136
136
|
http_status: response.status,
|
|
137
137
|
response_body: response.body
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Malipopay
|
|
4
4
|
module Resources
|
|
5
5
|
class Payments
|
|
6
6
|
def initialize(http_client)
|
|
@@ -81,7 +81,7 @@ module MaliPoPay
|
|
|
81
81
|
# @param params [Hash] Payment link parameters
|
|
82
82
|
# @return [Hash] Payment link response
|
|
83
83
|
def create_link(params)
|
|
84
|
-
@http.post("/api/v1/
|
|
84
|
+
@http.post("/api/v1/payment/link", body: params)
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Malipopay
|
|
4
4
|
module Resources
|
|
5
5
|
class References
|
|
6
6
|
def initialize(http_client)
|
|
@@ -11,35 +11,35 @@ module MaliPoPay
|
|
|
11
11
|
# @param params [Hash] Query parameters
|
|
12
12
|
# @return [Hash] List of banks
|
|
13
13
|
def banks(params = {})
|
|
14
|
-
@http.get("/api/v1/banks", params: params)
|
|
14
|
+
@http.get("/api/v1/standard/banks", params: params)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# List all supported financial institutions
|
|
18
18
|
# @param params [Hash] Query parameters
|
|
19
19
|
# @return [Hash] List of institutions
|
|
20
20
|
def institutions(params = {})
|
|
21
|
-
@http.get("/api/v1/
|
|
21
|
+
@http.get("/api/v1/standard/institutions", params: params)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# List all supported currencies
|
|
25
25
|
# @param params [Hash] Query parameters
|
|
26
26
|
# @return [Hash] List of currencies
|
|
27
27
|
def currencies(params = {})
|
|
28
|
-
@http.get("/api/v1/currency", params: params)
|
|
28
|
+
@http.get("/api/v1/standard/currency", params: params)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# List all supported countries
|
|
32
32
|
# @param params [Hash] Query parameters
|
|
33
33
|
# @return [Hash] List of countries
|
|
34
34
|
def countries(params = {})
|
|
35
|
-
@http.get("/api/v1/countries", params: params)
|
|
35
|
+
@http.get("/api/v1/standard/countries", params: params)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# List business types
|
|
39
39
|
# @param params [Hash] Query parameters
|
|
40
40
|
# @return [Hash] List of business types
|
|
41
41
|
def business_types(params = {})
|
|
42
|
-
@http.get("/api/v1/
|
|
42
|
+
@http.get("/api/v1/standard/businessType", params: params)
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
data/lib/malipopay/version.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "openssl"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module Malipopay
|
|
7
7
|
module Webhooks
|
|
8
8
|
class Verifier
|
|
9
9
|
TOLERANCE_IN_SECONDS = 300 # 5 minutes
|
|
@@ -14,8 +14,8 @@ module MaliPoPay
|
|
|
14
14
|
|
|
15
15
|
# Verify a webhook signature
|
|
16
16
|
# @param payload [String] Raw request body
|
|
17
|
-
# @param signature [String] Signature from X-
|
|
18
|
-
# @param timestamp [String, nil] Timestamp from X-
|
|
17
|
+
# @param signature [String] Signature from X-Malipopay-Signature header
|
|
18
|
+
# @param timestamp [String, nil] Timestamp from X-Malipopay-Timestamp header
|
|
19
19
|
# @return [Boolean] Whether the signature is valid
|
|
20
20
|
def verify(payload, signature, timestamp: nil)
|
|
21
21
|
return false if signature.nil? || signature.empty?
|
|
@@ -34,10 +34,10 @@ module MaliPoPay
|
|
|
34
34
|
# @param signature [String] Signature from header
|
|
35
35
|
# @param timestamp [String, nil] Timestamp from header
|
|
36
36
|
# @return [Hash] Parsed event data
|
|
37
|
-
# @raise [
|
|
37
|
+
# @raise [Malipopay::Error] If signature is invalid
|
|
38
38
|
def construct_event(payload, signature, timestamp: nil)
|
|
39
39
|
unless verify(payload, signature, timestamp: timestamp)
|
|
40
|
-
raise
|
|
40
|
+
raise Malipopay::AuthenticationError.new("Invalid webhook signature")
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
JSON.parse(payload)
|
data/lib/malipopay.rb
CHANGED
|
@@ -18,11 +18,11 @@ require_relative "malipopay/resources/references"
|
|
|
18
18
|
# Webhooks
|
|
19
19
|
require_relative "malipopay/webhooks/verifier"
|
|
20
20
|
|
|
21
|
-
module
|
|
21
|
+
module Malipopay
|
|
22
22
|
# Convenience method to create a new client
|
|
23
23
|
#
|
|
24
|
-
# @param options [Hash] Options passed to
|
|
25
|
-
# @return [
|
|
24
|
+
# @param options [Hash] Options passed to Malipopay::Client.new
|
|
25
|
+
# @return [Malipopay::Client]
|
|
26
26
|
def self.client(**options)
|
|
27
27
|
Client.new(**options)
|
|
28
28
|
end
|
data/malipopay.gemspec
CHANGED
|
@@ -4,12 +4,12 @@ require_relative "lib/malipopay/version"
|
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "malipopay"
|
|
7
|
-
spec.version =
|
|
7
|
+
spec.version = Malipopay::VERSION
|
|
8
8
|
spec.authors = ["Lockwood Technology Ltd"]
|
|
9
9
|
spec.email = ["developers@malipopay.co.tz"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Official Ruby SDK for the
|
|
12
|
-
spec.description = "Ruby client library for integrating with
|
|
11
|
+
spec.summary = "Official Ruby SDK for the Malipopay payment platform"
|
|
12
|
+
spec.description = "Ruby client library for integrating with Malipopay payment APIs. " \
|
|
13
13
|
"Supports mobile money collections, disbursements, invoicing, " \
|
|
14
14
|
"SMS, customer management, and more."
|
|
15
15
|
spec.homepage = "https://github.com/malipopay/malipopay-ruby"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: malipopay
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lockwood Technology Ltd
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -108,7 +108,7 @@ dependencies:
|
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '1.0'
|
|
111
|
-
description: Ruby client library for integrating with
|
|
111
|
+
description: Ruby client library for integrating with Malipopay payment APIs. Supports
|
|
112
112
|
mobile money collections, disbursements, invoicing, SMS, customer management, and
|
|
113
113
|
more.
|
|
114
114
|
email:
|
|
@@ -117,6 +117,7 @@ executables: []
|
|
|
117
117
|
extensions: []
|
|
118
118
|
extra_rdoc_files: []
|
|
119
119
|
files:
|
|
120
|
+
- CLAUDE.md
|
|
120
121
|
- Gemfile
|
|
121
122
|
- LICENSE
|
|
122
123
|
- README.md
|
|
@@ -170,5 +171,5 @@ requirements: []
|
|
|
170
171
|
rubygems_version: 3.0.3.1
|
|
171
172
|
signing_key:
|
|
172
173
|
specification_version: 4
|
|
173
|
-
summary: Official Ruby SDK for the
|
|
174
|
+
summary: Official Ruby SDK for the Malipopay payment platform
|
|
174
175
|
test_files: []
|