malipopay 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.
data/docs/customers.md ADDED
@@ -0,0 +1,133 @@
1
+ # Customers
2
+
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
+
5
+ ## Create a Customer
6
+
7
+ ```ruby
8
+ customer = client.customers.create(
9
+ name: 'Juma Bakari',
10
+ phone: '255712345678',
11
+ email: 'juma.bakari@example.com',
12
+ address: 'Plot 45, Samora Avenue, Dar es Salaam',
13
+ customer_type: 'individual',
14
+ notes: 'Preferred payment method: M-Pesa'
15
+ )
16
+
17
+ if customer['success']
18
+ puts "Customer created: #{customer['data']['id']}"
19
+ end
20
+ ```
21
+
22
+ ### Create a Business Customer
23
+
24
+ ```ruby
25
+ business = client.customers.create(
26
+ name: 'Kilimanjaro Trading Co.',
27
+ phone: '255222123456',
28
+ email: 'accounts@kilitrade.co.tz',
29
+ address: 'Industrial Area, Arusha',
30
+ customer_type: 'business',
31
+ tin: '123-456-789',
32
+ notes: 'Net 30 payment terms'
33
+ )
34
+ ```
35
+
36
+ ## List All Customers
37
+
38
+ ```ruby
39
+ customers = client.customers.list
40
+
41
+ if customers['success']
42
+ customers['data'].each do |c|
43
+ puts "#{c['name']} (#{c['phone']})"
44
+ end
45
+ end
46
+ ```
47
+
48
+ ## Get a Customer by ID
49
+
50
+ ```ruby
51
+ customer = client.customers.get('cust_abc123')
52
+
53
+ if customer['success']
54
+ puts "Name: #{customer['data']['name']}"
55
+ puts "Phone: #{customer['data']['phone']}"
56
+ puts "Email: #{customer['data']['email']}"
57
+ end
58
+ ```
59
+
60
+ ## Get a Customer by Phone Number
61
+
62
+ Look up a customer using their phone number:
63
+
64
+ ```ruby
65
+ customer = client.customers.get_by_phone('255712345678')
66
+
67
+ if customer['success']
68
+ puts "Found: #{customer['data']['name']}"
69
+ end
70
+ ```
71
+
72
+ ## Get a Customer by Customer Number
73
+
74
+ Look up using the MaliPoPay-assigned customer number:
75
+
76
+ ```ruby
77
+ customer = client.customers.get_by_number('CUST-2024-001')
78
+
79
+ if customer['success']
80
+ puts "Found: #{customer['data']['name']}"
81
+ end
82
+ ```
83
+
84
+ ## Search Customers
85
+
86
+ Search by name, phone, email, or other fields:
87
+
88
+ ```ruby
89
+ results = client.customers.search
90
+
91
+ if results['success']
92
+ puts "Found #{results['data'].length} customers"
93
+ end
94
+ ```
95
+
96
+ ## Verify a Customer
97
+
98
+ Customer verification is useful for KYC (Know Your Customer) compliance. This checks the customer's identity against the phone number or ID document registered with their mobile money provider:
99
+
100
+ ```ruby
101
+ verification = client.customers.verify(
102
+ phone: '255712345678',
103
+ provider: 'M-Pesa'
104
+ )
105
+
106
+ if verification['success']
107
+ puts "Verified: #{verification['data']}"
108
+ else
109
+ puts "Verification failed: #{verification['message']}"
110
+ end
111
+ ```
112
+
113
+ ## Error Handling
114
+
115
+ ```ruby
116
+ begin
117
+ customer = client.customers.create(
118
+ name: 'Incomplete Customer'
119
+ # missing phone -- will trigger validation error
120
+ )
121
+ rescue MaliPoPay::ValidationError => e
122
+ puts "Missing required fields: #{e.message}"
123
+ rescue MaliPoPay::NotFoundError
124
+ puts 'Customer not found.'
125
+ rescue MaliPoPay::Error => e
126
+ puts "Error: #{e.message}"
127
+ end
128
+ ```
129
+
130
+ ## Next Steps
131
+
132
+ - [Invoices](./invoices.md) -- create invoices for your customers
133
+ - [Payments](./payments.md) -- collect payments from customers
@@ -0,0 +1,274 @@
1
+ # Error Handling
2
+
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
+
5
+ ## Exception Hierarchy
6
+
7
+ ```
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
+ ```
17
+
18
+ ## Rescuing Specific Exceptions
19
+
20
+ ### Ordered by Specificity
21
+
22
+ ```ruby
23
+ begin
24
+ result = client.payments.collect(
25
+ amount: 50_000,
26
+ currency: 'TZS',
27
+ phone: '255712345678',
28
+ provider: 'M-Pesa',
29
+ reference: 'ORD-2024-100',
30
+ description: 'Monthly subscription'
31
+ )
32
+
33
+ puts "Collection initiated: #{result['reference']}"
34
+
35
+ rescue MaliPoPay::AuthenticationError
36
+ # API key is invalid or expired
37
+ puts 'Authentication failed. Rotate your API key at app.malipopay.co.tz'
38
+
39
+ rescue MaliPoPay::PermissionError
40
+ # API key lacks permission for this operation
41
+ puts 'Insufficient permissions. Check your API key scopes.'
42
+
43
+ rescue MaliPoPay::ValidationError => e
44
+ # The request had invalid fields
45
+ puts "Invalid request: #{e.message}"
46
+ # e.message might say: "phone must be a valid Tanzanian number (255xxxxxxxxx)"
47
+
48
+ rescue MaliPoPay::NotFoundError
49
+ puts 'The requested resource was not found.'
50
+
51
+ rescue MaliPoPay::RateLimitError
52
+ puts 'Too many requests. Back off and retry.'
53
+
54
+ rescue MaliPoPay::ApiError => e
55
+ # MaliPoPay server error -- transient, safe to retry
56
+ puts "Server error (#{e.message}). Retrying..."
57
+
58
+ rescue MaliPoPay::ConnectionError => e
59
+ # Network-level failure
60
+ puts "Connection failed: #{e.message}"
61
+
62
+ rescue MaliPoPay::Error => e
63
+ # Catch-all for any other SDK error
64
+ puts "Unexpected error: #{e.message}"
65
+ end
66
+ ```
67
+
68
+ ## Exception Properties
69
+
70
+ Every `MaliPoPay::Error` includes:
71
+
72
+ | Property | Type | Description |
73
+ |----------|------|-------------|
74
+ | `message` | `String` | Human-readable error description |
75
+ | `status_code` | `Integer` or `nil` | HTTP status code (`nil` for `ConnectionError`) |
76
+
77
+ ## Retry Strategies
78
+
79
+ The SDK automatically retries transient errors (5xx, connection timeouts) based on the `retries` option. You can also implement your own retry logic for specific cases.
80
+
81
+ ### Built-in Retries
82
+
83
+ ```ruby
84
+ client = MaliPoPay::Client.new(
85
+ api_key: ENV['MALIPOPAY_API_KEY'],
86
+ retries: 3 # retry up to 3 times on transient failures (default: 2)
87
+ )
88
+ ```
89
+
90
+ The SDK uses exponential backoff between retries. It will only retry on:
91
+ - `MaliPoPay::ApiError` (5xx responses)
92
+ - `MaliPoPay::ConnectionError` (network timeouts and DNS failures)
93
+
94
+ It will **not** retry on:
95
+ - `AuthenticationError` (fix your API key)
96
+ - `PermissionError` (check your API key scopes)
97
+ - `ValidationError` (fix your request)
98
+ - `NotFoundError` (the resource doesn't exist)
99
+ - `RateLimitError` (handled separately -- see below)
100
+
101
+ ### Custom Retry for Rate Limits
102
+
103
+ ```ruby
104
+ def with_rate_limit_retry(max_retries: 3)
105
+ attempts = 0
106
+
107
+ begin
108
+ yield
109
+ rescue MaliPoPay::RateLimitError
110
+ attempts += 1
111
+ raise if attempts > max_retries
112
+
113
+ delay = 2**attempts # exponential backoff: 2s, 4s, 8s
114
+ puts "Rate limited. Retrying in #{delay}s..."
115
+ sleep delay
116
+ retry
117
+ end
118
+ end
119
+
120
+ # Usage
121
+ result = with_rate_limit_retry do
122
+ client.payments.collect(
123
+ amount: 75_000,
124
+ currency: 'TZS',
125
+ phone: '255712345678',
126
+ provider: 'M-Pesa',
127
+ reference: 'ORD-2024-200',
128
+ description: 'Retry example'
129
+ )
130
+ end
131
+ ```
132
+
133
+ ### Generic Retry Helper
134
+
135
+ ```ruby
136
+ def with_retry(max_retries: 3, on: [MaliPoPay::ApiError, MaliPoPay::ConnectionError])
137
+ attempts = 0
138
+
139
+ begin
140
+ yield
141
+ rescue *on => e
142
+ attempts += 1
143
+ raise if attempts > max_retries
144
+
145
+ delay = 2**attempts
146
+ puts "#{e.class}: #{e.message}. Retry #{attempts}/#{max_retries} in #{delay}s..."
147
+ sleep delay
148
+ retry
149
+ end
150
+ end
151
+
152
+ # Usage
153
+ result = with_retry(max_retries: 5) do
154
+ client.payments.disburse(
155
+ amount: 250_000,
156
+ currency: 'TZS',
157
+ phone: '255754321098',
158
+ provider: 'Airtel Money',
159
+ reference: 'PAY-2024-055',
160
+ description: 'Supplier payment'
161
+ )
162
+ end
163
+ ```
164
+
165
+ ## Common Errors and Solutions
166
+
167
+ ### AuthenticationError (401)
168
+
169
+ | Error | Cause | Solution |
170
+ |-------|-------|----------|
171
+ | "Invalid API key" | The `apiToken` header is wrong or missing | Verify your key at [app.malipopay.co.tz](https://app.malipopay.co.tz) under Settings > API Keys |
172
+ | "API key expired" | Key was revoked or rotated | Generate a new key in the dashboard |
173
+ | "Unauthorized environment" | Using a production key on UAT or vice versa | Use the correct key for your environment |
174
+
175
+ ### PermissionError (403)
176
+
177
+ | Error | Cause | Solution |
178
+ |-------|-------|----------|
179
+ | "Insufficient permissions" | API key lacks required scope | Check key permissions in the dashboard |
180
+
181
+ ### ValidationError (422)
182
+
183
+ | Error | Cause | Solution |
184
+ |-------|-------|----------|
185
+ | "phone must be a valid Tanzanian number" | Phone number not in `255xxxxxxxxx` format | Use the full international format: `255712345678` |
186
+ | "amount must be greater than 0" | Zero or negative amount | Provide a positive integer amount in TZS |
187
+ | "provider is required" | Missing `provider` field | Specify one of: `M-Pesa`, `Airtel Money`, `Mixx`, `Halopesa`, `T-Pesa`, `CRDB`, `NMB` |
188
+ | "reference must be unique" | Duplicate reference string | Generate a unique reference per transaction |
189
+ | "currency must be TZS" | Unsupported currency | MaliPoPay currently supports TZS only |
190
+
191
+ ### NotFoundError (404)
192
+
193
+ | Error | Cause | Solution |
194
+ |-------|-------|----------|
195
+ | "Payment not found" | Reference doesn't match any payment | Double-check the reference string |
196
+ | "Customer not found" | Customer ID doesn't exist | Create the customer first or verify the ID |
197
+ | "Invoice not found" | Invoice ID doesn't exist | Check the invoice ID from your records |
198
+
199
+ ### RateLimitError (429)
200
+
201
+ | Error | Cause | Solution |
202
+ |-------|-------|----------|
203
+ | "Rate limit exceeded" | Too many API calls in a short period | Implement exponential backoff; batch operations where possible |
204
+
205
+ ### ApiError (5xx)
206
+
207
+ | Error | Cause | Solution |
208
+ |-------|-------|----------|
209
+ | "Internal server error" | Temporary server issue | Retry after a short delay; these are transient |
210
+ | "Service unavailable" | Maintenance or provider downtime | Check [status.malipopay.co.tz](https://status.malipopay.co.tz) for updates |
211
+
212
+ ### ConnectionError
213
+
214
+ | Error | Cause | Solution |
215
+ |-------|-------|----------|
216
+ | "Request timed out" | Network latency or server unresponsive | Increase `timeout` in client options; check network connectivity |
217
+ | "DNS resolution failed" | Cannot resolve the API hostname | Verify your DNS settings and internet connection |
218
+
219
+ ## Logging Errors
220
+
221
+ Use Ruby's Logger for production error tracking:
222
+
223
+ ```ruby
224
+ require 'logger'
225
+
226
+ logger = Logger.new($stdout)
227
+
228
+ begin
229
+ result = client.payments.collect(
230
+ amount: 30_000,
231
+ currency: 'TZS',
232
+ phone: '255622345678',
233
+ provider: 'Halopesa',
234
+ reference: 'ORD-2024-300',
235
+ description: 'Logging example'
236
+ )
237
+ rescue MaliPoPay::Error => e
238
+ logger.error("MaliPoPay API error: status=#{e.status_code} message=#{e.message}")
239
+ raise
240
+ end
241
+ ```
242
+
243
+ ### Rails Integration
244
+
245
+ In Rails, errors are automatically logged. You can add custom handling in an initializer or concern:
246
+
247
+ ```ruby
248
+ # app/controllers/concerns/malipopay_error_handling.rb
249
+ module MalipopayErrorHandling
250
+ extend ActiveSupport::Concern
251
+
252
+ included do
253
+ rescue_from MaliPoPay::AuthenticationError do |e|
254
+ Rails.logger.error("MaliPoPay auth error: #{e.message}")
255
+ render json: { error: 'Payment service authentication failed' }, status: :service_unavailable
256
+ end
257
+
258
+ rescue_from MaliPoPay::ValidationError do |e|
259
+ render json: { error: e.message }, status: :unprocessable_entity
260
+ end
261
+
262
+ rescue_from MaliPoPay::Error do |e|
263
+ Rails.logger.error("MaliPoPay error: #{e.class} - #{e.message}")
264
+ render json: { error: 'Payment service error' }, status: :service_unavailable
265
+ end
266
+ end
267
+ end
268
+ ```
269
+
270
+ ## Next Steps
271
+
272
+ - [Configuration](./configuration.md) -- configure retries and timeouts at the client level
273
+ - [Payments](./payments.md) -- payment operations that may raise these exceptions
274
+ - [Webhooks](./webhooks.md) -- webhook signature verification errors
@@ -0,0 +1,160 @@
1
+ # Getting Started with MaliPoPay Ruby SDK
2
+
3
+ ## Prerequisites
4
+
5
+ - **Ruby 3.0** or later
6
+ - **Bundler** (included with Ruby)
7
+ - A MaliPoPay merchant account with API credentials
8
+
9
+ ## Installation
10
+
11
+ Add MaliPoPay to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'malipopay'
15
+ ```
16
+
17
+ Then run:
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ Or install it directly:
24
+
25
+ ```bash
26
+ gem install malipopay
27
+ ```
28
+
29
+ ## Getting Your API Key
30
+
31
+ 1. Sign in to your merchant dashboard at [app.malipopay.co.tz](https://app.malipopay.co.tz)
32
+ 2. Navigate to **Settings > API Keys**
33
+ 3. Click **Generate New Key**
34
+ 4. Copy the API key immediately -- it will only be shown once
35
+ 5. Store it securely (environment variable, credentials file, etc.)
36
+
37
+ > **Important:** Never commit API keys to source control. Use environment variables or Rails encrypted credentials.
38
+
39
+ ## Your First Payment Collection
40
+
41
+ Collect TZS 50,000 from an M-Pesa customer in five lines:
42
+
43
+ ```ruby
44
+ require 'malipopay'
45
+
46
+ client = MaliPoPay::Client.new(api_key: ENV['MALIPOPAY_API_KEY'])
47
+
48
+ result = client.payments.collect(
49
+ amount: 50_000,
50
+ currency: 'TZS',
51
+ phone: '255712345678',
52
+ provider: 'M-Pesa',
53
+ reference: 'ORDER-2024-001',
54
+ description: 'Payment for office supplies'
55
+ )
56
+
57
+ puts "Collection initiated: #{result['reference']}" if result['success']
58
+ ```
59
+
60
+ When this runs, the customer at `255712345678` receives a USSD push prompt on their phone asking them to confirm the TZS 50,000 payment with their M-Pesa PIN.
61
+
62
+ ## Environment Selection
63
+
64
+ MaliPoPay provides two environments:
65
+
66
+ | Environment | Base URL | Purpose |
67
+ |-------------|----------|---------|
68
+ | **Production** | `https://core-prod.malipopay.co.tz` | Live transactions with real money |
69
+ | **UAT** | `https://core-uat.malipopay.co.tz` | Testing and integration development |
70
+
71
+ ### Using UAT for Testing
72
+
73
+ Always develop and test against UAT before going live:
74
+
75
+ ```ruby
76
+ client = MaliPoPay::Client.new(
77
+ api_key: ENV['MALIPOPAY_UAT_API_KEY'],
78
+ environment: :uat
79
+ )
80
+ ```
81
+
82
+ ### Custom Base URL
83
+
84
+ For advanced setups (proxies, custom routing), you can override the base URL:
85
+
86
+ ```ruby
87
+ client = MaliPoPay::Client.new(
88
+ api_key: ENV['MALIPOPAY_API_KEY'],
89
+ base_url: 'https://custom-proxy.example.com'
90
+ )
91
+ ```
92
+
93
+ When `base_url` is set, it takes precedence over the `environment` setting.
94
+
95
+ ## Configuring Timeouts and Retries
96
+
97
+ The SDK automatically retries transient failures. You can adjust timeout and retry behavior:
98
+
99
+ ```ruby
100
+ client = MaliPoPay::Client.new(
101
+ api_key: ENV['MALIPOPAY_API_KEY'],
102
+ environment: :production,
103
+ timeout: 60, # seconds (default: 30)
104
+ retries: 3 # automatic retries (default: 2)
105
+ )
106
+ ```
107
+
108
+ ## Complete Minimal Example
109
+
110
+ A full script that collects a payment and verifies it:
111
+
112
+ ```ruby
113
+ require 'malipopay'
114
+
115
+ api_key = ENV.fetch('MALIPOPAY_API_KEY') { raise 'Set the MALIPOPAY_API_KEY environment variable' }
116
+
117
+ client = MaliPoPay::Client.new(
118
+ api_key: api_key,
119
+ environment: :uat
120
+ )
121
+
122
+ begin
123
+ reference = "ORD-#{Time.now.strftime('%Y%m%d%H%M%S')}"
124
+
125
+ # Step 1: Initiate collection
126
+ collection = client.payments.collect(
127
+ amount: 15_000,
128
+ currency: 'TZS',
129
+ phone: '255754321098',
130
+ provider: 'Airtel Money',
131
+ reference: reference,
132
+ description: 'Monthly subscription'
133
+ )
134
+
135
+ puts "Collection initiated. Reference: #{reference}"
136
+
137
+ # Step 2: Wait for the customer to approve on their phone
138
+ # In production, use webhooks instead of polling
139
+ sleep 30
140
+
141
+ # Step 3: Verify the payment
142
+ verification = client.payments.verify(reference)
143
+
144
+ puts "Payment status: #{verification['status']}"
145
+
146
+ rescue MaliPoPay::AuthenticationError
147
+ puts 'Invalid API key. Check your credentials.'
148
+ rescue MaliPoPay::ValidationError => e
149
+ puts "Invalid request: #{e.message}"
150
+ rescue MaliPoPay::Error => e
151
+ puts "Payment error: #{e.message}"
152
+ end
153
+ ```
154
+
155
+ ## Next Steps
156
+
157
+ - [Payments Guide](./payments.md) -- all payment operations in detail
158
+ - [Webhooks](./webhooks.md) -- receive real-time payment notifications
159
+ - [Configuration](./configuration.md) -- advanced client setup
160
+ - [Error Handling](./error-handling.md) -- handling failures gracefully