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/invoices.md ADDED
@@ -0,0 +1,200 @@
1
+ # Invoices
2
+
3
+ The `invoices` resource lets you create, manage, and track invoices. Invoices can include line items with tax, and you can record payments against them as they come in.
4
+
5
+ ## Create an Invoice
6
+
7
+ ```ruby
8
+ invoice = client.invoices.create(
9
+ customer_id: 'cust_abc123',
10
+ currency: 'TZS',
11
+ due_date: '2024-03-15',
12
+ items: [
13
+ {
14
+ description: 'Website Development',
15
+ quantity: 1,
16
+ unit_price: 2_500_000
17
+ },
18
+ {
19
+ description: 'Domain Registration (.co.tz)',
20
+ quantity: 1,
21
+ unit_price: 75_000
22
+ },
23
+ {
24
+ description: 'Hosting (12 months)',
25
+ quantity: 12,
26
+ unit_price: 50_000
27
+ }
28
+ ],
29
+ notes: 'Payment due within 15 days. M-Pesa and bank transfer accepted.',
30
+ tax_rate: 18 # VAT at 18%
31
+ )
32
+
33
+ if invoice['success']
34
+ puts "Invoice created: #{invoice['data']['id']}"
35
+ end
36
+ ```
37
+
38
+ In this example, the subtotal is TZS 3,175,000 (2,500,000 + 75,000 + 600,000), and with 18% VAT the total would be TZS 3,746,500.
39
+
40
+ ## Tax Calculation
41
+
42
+ MaliPoPay calculates tax automatically based on the `tax_rate` you provide:
43
+
44
+ ```ruby
45
+ # Invoice with 18% VAT (Tanzania standard rate)
46
+ invoice = client.invoices.create(
47
+ customer_id: 'cust_abc123',
48
+ currency: 'TZS',
49
+ due_date: '2024-04-30',
50
+ items: [
51
+ { description: 'Consulting (8 hours)', quantity: 8, unit_price: 150_000 }
52
+ ],
53
+ tax_rate: 18
54
+ )
55
+
56
+ # Subtotal: TZS 1,200,000
57
+ # VAT (18%): TZS 216,000
58
+ # Total: TZS 1,416,000
59
+ ```
60
+
61
+ For tax-exempt invoices, omit the `tax_rate` field or set it to `0`:
62
+
63
+ ```ruby
64
+ invoice = client.invoices.create(
65
+ customer_id: 'cust_abc123',
66
+ currency: 'TZS',
67
+ due_date: '2024-04-30',
68
+ items: [
69
+ { description: 'Government service (exempt)', quantity: 1, unit_price: 500_000 }
70
+ ],
71
+ tax_rate: 0
72
+ )
73
+ ```
74
+
75
+ ## List Invoices
76
+
77
+ ```ruby
78
+ invoices = client.invoices.list
79
+
80
+ if invoices['success']
81
+ invoices['data'].each do |inv|
82
+ puts "#{inv['id']}: TZS #{inv['total']} (#{inv['status']})"
83
+ end
84
+ end
85
+ ```
86
+
87
+ ## Get an Invoice by ID
88
+
89
+ ```ruby
90
+ invoice = client.invoices.get('inv_xyz789')
91
+
92
+ if invoice['success']
93
+ puts "Invoice: #{invoice['data']}"
94
+ end
95
+ ```
96
+
97
+ ## Record a Payment Against an Invoice
98
+
99
+ When a customer makes a partial or full payment, record it against the invoice:
100
+
101
+ ```ruby
102
+ # Record a partial payment
103
+ partial = client.invoices.record_payment(
104
+ invoice_id: 'inv_xyz789',
105
+ amount: 1_000_000,
106
+ reference: 'MPESA-TXN-ABC123',
107
+ payment_method: 'M-Pesa',
108
+ notes: 'Partial payment received via M-Pesa'
109
+ )
110
+
111
+ # Later, record the remaining balance
112
+ final = client.invoices.record_payment(
113
+ invoice_id: 'inv_xyz789',
114
+ amount: 2_746_500,
115
+ reference: 'CRDB-TXN-DEF456',
116
+ payment_method: 'Bank Transfer',
117
+ notes: 'Final payment via CRDB bank transfer'
118
+ )
119
+ ```
120
+
121
+ ## Approve a Draft Invoice
122
+
123
+ Invoices may start as drafts. Approve them when ready to send to the customer:
124
+
125
+ ```ruby
126
+ approval = client.invoices.approve_draft(invoice_id: 'inv_draft_001')
127
+
128
+ if approval['success']
129
+ puts 'Invoice approved and ready to send.'
130
+ end
131
+ ```
132
+
133
+ ## Invoice Workflow
134
+
135
+ A typical invoice lifecycle:
136
+
137
+ 1. **Create** the invoice with line items and tax
138
+ 2. **Approve** the draft (optional, depending on your workflow)
139
+ 3. **Send** the invoice to the customer (via email, SMS, or payment link)
140
+ 4. **Collect** payment using a mobile money collection or payment link
141
+ 5. **Record** the payment against the invoice
142
+ 6. Invoice status moves to **Paid** when the full amount is received
143
+
144
+ ### Combining Invoices with Payment Collection
145
+
146
+ ```ruby
147
+ # Step 1: Create the invoice
148
+ invoice = client.invoices.create(
149
+ customer_id: 'cust_abc123',
150
+ currency: 'TZS',
151
+ due_date: '2024-02-28',
152
+ items: [
153
+ { description: 'Consulting (8 hours)', quantity: 8, unit_price: 150_000 }
154
+ ]
155
+ )
156
+
157
+ # Step 2: Collect payment via M-Pesa
158
+ collection = client.payments.collect(
159
+ amount: 1_200_000,
160
+ currency: 'TZS',
161
+ phone: '255712345678',
162
+ provider: 'M-Pesa',
163
+ reference: 'inv_xyz789',
164
+ description: 'Invoice #INV-2024-0042 payment'
165
+ )
166
+
167
+ # Step 3: After webhook confirms payment, record it
168
+ record = client.invoices.record_payment(
169
+ invoice_id: 'inv_xyz789',
170
+ amount: 1_200_000,
171
+ reference: 'MPESA-TXN-GHI789',
172
+ payment_method: 'M-Pesa'
173
+ )
174
+ ```
175
+
176
+ ## Error Handling
177
+
178
+ ```ruby
179
+ begin
180
+ invoice = client.invoices.create(
181
+ customer_id: 'nonexistent_customer',
182
+ currency: 'TZS',
183
+ items: [
184
+ { description: 'Test', quantity: 1, unit_price: 1_000 }
185
+ ]
186
+ )
187
+ rescue MaliPoPay::ValidationError => e
188
+ puts "Invalid invoice data: #{e.message}"
189
+ rescue MaliPoPay::NotFoundError
190
+ puts 'Customer not found. Create the customer first.'
191
+ rescue MaliPoPay::Error => e
192
+ puts "Invoice error: #{e.message}"
193
+ end
194
+ ```
195
+
196
+ ## Next Steps
197
+
198
+ - [Payments](./payments.md) -- collect payments for your invoices
199
+ - [Customers](./customers.md) -- manage the customers you invoice
200
+ - [Webhooks](./webhooks.md) -- get notified when payments are received
data/docs/payments.md ADDED
@@ -0,0 +1,284 @@
1
+ # Payments
2
+
3
+ The `payments` resource handles all payment operations: mobile money collections, disbursements, payment verification, payment links, and search.
4
+
5
+ ## How Payments Work in Tanzania
6
+
7
+ MaliPoPay integrates with Tanzania's major mobile money and banking networks:
8
+
9
+ - **Vodacom M-Pesa** -- largest mobile money network
10
+ - **Airtel Money** -- second-largest MNO
11
+ - **Tigo Pesa (Mixx by Yas)** -- merged MNO brand
12
+ - **Halotel Halopesa** -- growing rural network
13
+ - **TTCL T-Pesa** -- state telco mobile money
14
+ - **USSD (*146*08#)** -- unified USSD payment channel
15
+ - **Bank transfers** -- CRDB, NMB, and other banks via H2H integration
16
+ - **Card payments** -- Visa and Mastercard
17
+
18
+ ### Collection Flow
19
+
20
+ 1. Your app calls `collect` with the customer's phone number and amount
21
+ 2. MaliPoPay sends a USSD push to the customer's phone
22
+ 3. The customer sees a prompt like: *"Pay TZS 50,000 to ACME Ltd? Enter PIN to confirm"*
23
+ 4. The customer enters their mobile money PIN
24
+ 5. MaliPoPay receives the confirmation and notifies you via webhook
25
+ 6. You can also poll using `verify`
26
+
27
+ ### Disbursement Flow
28
+
29
+ 1. Your app calls `disburse` with the recipient's phone/account and amount
30
+ 2. MaliPoPay processes the transfer from your merchant float
31
+ 3. The recipient receives the funds in their mobile money or bank account
32
+ 4. You receive a webhook notification with the result
33
+
34
+ ## Mobile Money Collection
35
+
36
+ Collect payments from any supported mobile money provider:
37
+
38
+ ```ruby
39
+ # M-Pesa collection
40
+ mpesa = client.payments.collect(
41
+ amount: 75_000,
42
+ currency: 'TZS',
43
+ phone: '255712345678',
44
+ provider: 'M-Pesa',
45
+ reference: 'INV-2024-0042',
46
+ description: 'Invoice payment - Office furniture'
47
+ )
48
+
49
+ # Airtel Money collection
50
+ airtel = client.payments.collect(
51
+ amount: 25_000,
52
+ currency: 'TZS',
53
+ phone: '255782345678',
54
+ provider: 'Airtel Money',
55
+ reference: 'INV-2024-0043',
56
+ description: 'Delivery fee'
57
+ )
58
+
59
+ # Halopesa collection
60
+ halo = client.payments.collect(
61
+ amount: 10_000,
62
+ currency: 'TZS',
63
+ phone: '255622345678',
64
+ provider: 'Halopesa',
65
+ reference: 'INV-2024-0044',
66
+ description: 'Service charge'
67
+ )
68
+
69
+ # Tigo Pesa / Mixx by Yas collection
70
+ tigo = client.payments.collect(
71
+ amount: 35_000,
72
+ currency: 'TZS',
73
+ phone: '255652345678',
74
+ provider: 'Mixx',
75
+ reference: 'INV-2024-0045',
76
+ description: 'Consultation fee'
77
+ )
78
+
79
+ # T-Pesa collection
80
+ tpesa = client.payments.collect(
81
+ amount: 20_000,
82
+ currency: 'TZS',
83
+ phone: '255742345678',
84
+ provider: 'T-Pesa',
85
+ reference: 'INV-2024-0046',
86
+ description: 'Registration fee'
87
+ )
88
+ ```
89
+
90
+ ## Card Payments
91
+
92
+ Collect payments via Visa or Mastercard:
93
+
94
+ ```ruby
95
+ card = client.payments.collect(
96
+ amount: 150_000,
97
+ currency: 'TZS',
98
+ provider: 'Card',
99
+ reference: 'INV-2024-0050',
100
+ description: 'Annual membership',
101
+ customer_email: 'juma@example.com',
102
+ redirect_url: 'https://yoursite.co.tz/payment/success'
103
+ )
104
+
105
+ # The result includes a checkout URL for the customer
106
+ puts "Redirect customer to: #{card['checkout_url']}"
107
+ ```
108
+
109
+ ## Disbursement
110
+
111
+ Send money to mobile money wallets or bank accounts:
112
+
113
+ ```ruby
114
+ # Disburse to M-Pesa wallet
115
+ disbursement = client.payments.disburse(
116
+ amount: 150_000,
117
+ currency: 'TZS',
118
+ phone: '255712345678',
119
+ provider: 'M-Pesa',
120
+ reference: 'PAYOUT-2024-001',
121
+ description: 'Salary payment - January 2024'
122
+ )
123
+
124
+ # Disburse to bank account (CRDB)
125
+ bank_disbursement = client.payments.disburse(
126
+ amount: 500_000,
127
+ currency: 'TZS',
128
+ account_number: '01J1234567890',
129
+ bank_code: 'CRDB',
130
+ account_name: 'Juma Hassan',
131
+ reference: 'PAYOUT-2024-002',
132
+ description: 'Vendor payment'
133
+ )
134
+
135
+ # Disburse to NMB account
136
+ nmb_disbursement = client.payments.disburse(
137
+ amount: 2_500_000,
138
+ currency: 'TZS',
139
+ account_number: '2345678901',
140
+ bank_code: 'NMB',
141
+ account_name: 'Maria Joseph',
142
+ reference: 'PAYOUT-2024-003',
143
+ description: 'Contractor payout'
144
+ )
145
+ ```
146
+
147
+ ## Payment Verification
148
+
149
+ After initiating a collection, verify the payment status. In production you should rely on webhooks, but polling is useful for synchronous flows and reconciliation:
150
+
151
+ ```ruby
152
+ # Verify by reference
153
+ status = client.payments.verify('INV-2024-0042')
154
+
155
+ if status['success']
156
+ puts "Payment status: #{status['status']}"
157
+ end
158
+ ```
159
+
160
+ ### Polling with Timeout
161
+
162
+ For cases where you need to wait for the customer to complete the payment:
163
+
164
+ ```ruby
165
+ def wait_for_payment(client, reference, timeout: 120, interval: 5)
166
+ deadline = Time.now + timeout
167
+
168
+ while Time.now < deadline
169
+ result = client.payments.verify(reference)
170
+
171
+ return true if result['status'] == 'completed'
172
+ return false if result['status'] == 'failed'
173
+
174
+ sleep interval
175
+ end
176
+
177
+ false # timed out
178
+ end
179
+
180
+ # Wait up to 2 minutes, polling every 5 seconds
181
+ paid = wait_for_payment(client, 'INV-2024-0042')
182
+ ```
183
+
184
+ > **Recommendation:** Use webhooks instead of polling in production. Polling is acceptable for testing, CLI tools, and reconciliation scripts.
185
+
186
+ ## Payment Links
187
+
188
+ Generate a shareable payment link that customers can use to pay via any supported method:
189
+
190
+ ```ruby
191
+ link = client.payments.pay(
192
+ amount: 250_000,
193
+ currency: 'TZS',
194
+ reference: 'LINK-2024-001',
195
+ description: 'Annual membership fee',
196
+ customer_name: 'Asha Mwalimu',
197
+ customer_email: 'asha@example.com',
198
+ customer_phone: '255712345678',
199
+ redirect_url: 'https://yoursite.co.tz/payment/success',
200
+ callback_url: 'https://yoursite.co.tz/api/webhooks/malipopay'
201
+ )
202
+
203
+ puts "Send this link to the customer: #{link['payment_url']}"
204
+ ```
205
+
206
+ ## Retry Failed Collections
207
+
208
+ If a collection failed due to a transient issue (timeout, network error), you can retry it:
209
+
210
+ ```ruby
211
+ retry_result = client.payments.retry('INV-2024-0042')
212
+
213
+ if retry_result['success']
214
+ puts 'Collection retry initiated. Customer will receive a new USSD push.'
215
+ end
216
+ ```
217
+
218
+ ## List and Search Payments
219
+
220
+ ### List All Payments
221
+
222
+ ```ruby
223
+ payments = client.payments.list
224
+
225
+ if payments['success']
226
+ puts "Total payments: #{payments['data'].length}"
227
+ end
228
+ ```
229
+
230
+ ### Search Payments
231
+
232
+ ```ruby
233
+ results = client.payments.search
234
+ ```
235
+
236
+ ### Get Payment by Reference
237
+
238
+ ```ruby
239
+ payment = client.payments.get_by_reference('INV-2024-0042')
240
+
241
+ if payment['success']
242
+ puts "Amount: #{payment['data']['amount']}"
243
+ puts "Status: #{payment['data']['status']}"
244
+ puts "Provider: #{payment['data']['provider']}"
245
+ end
246
+ ```
247
+
248
+ ## Error Handling for Payments
249
+
250
+ Always wrap payment operations in begin/rescue:
251
+
252
+ ```ruby
253
+ begin
254
+ result = client.payments.collect(
255
+ amount: 50_000,
256
+ currency: 'TZS',
257
+ phone: '255712345678',
258
+ provider: 'M-Pesa',
259
+ reference: 'ORDER-001',
260
+ description: 'Test payment'
261
+ )
262
+ rescue MaliPoPay::ValidationError => e
263
+ # Invalid parameters (wrong phone format, missing fields, etc.)
264
+ puts "Validation error: #{e.message}"
265
+ rescue MaliPoPay::AuthenticationError
266
+ # Bad API key
267
+ puts 'Check your API key.'
268
+ rescue MaliPoPay::RateLimitError
269
+ # Too many requests -- back off and retry
270
+ puts 'Rate limited. Please wait and retry.'
271
+ rescue MaliPoPay::ConnectionError => e
272
+ # Network issue
273
+ puts "Network error: #{e.message}"
274
+ rescue MaliPoPay::Error => e
275
+ # Catch-all for other SDK errors
276
+ puts "Payment error: #{e.message}"
277
+ end
278
+ ```
279
+
280
+ ## Next Steps
281
+
282
+ - [Webhooks](./webhooks.md) -- receive real-time payment status updates
283
+ - [Invoices](./invoices.md) -- create invoices and record payments against them
284
+ - [Error Handling](./error-handling.md) -- comprehensive error handling patterns
data/docs/sms.md ADDED
@@ -0,0 +1,182 @@
1
+ # SMS
2
+
3
+ The `sms` resource lets you send transactional and promotional SMS messages to Tanzanian phone numbers. You can send single messages, bulk messages to multiple recipients, and schedule messages for future delivery.
4
+
5
+ ## Sending a Single SMS
6
+
7
+ ```ruby
8
+ sms = client.sms.send_message(
9
+ to: '255712345678',
10
+ message: 'Your payment of TZS 50,000 has been confirmed. Reference: ORD-2024-100. Thank you for shopping with us!',
11
+ sender_id: 'MALIPOPAY'
12
+ )
13
+
14
+ if sms['success']
15
+ puts "SMS sent: #{sms['data']}"
16
+ else
17
+ puts "Failed: #{sms['message']}"
18
+ end
19
+ ```
20
+
21
+ ### With Error Handling
22
+
23
+ ```ruby
24
+ begin
25
+ sms = client.sms.send_message(
26
+ to: '255754321098',
27
+ message: 'Dear Amina, your invoice INV-2024-042 of TZS 3,746,500 is due on 15 March 2024. Pay via M-Pesa to avoid late fees.',
28
+ sender_id: 'ACME'
29
+ )
30
+
31
+ puts "Message delivered: #{sms['data']}"
32
+ rescue MaliPoPay::ValidationError => e
33
+ puts "Invalid request: #{e.message}"
34
+ rescue MaliPoPay::Error => e
35
+ puts "SMS error: #{e.message}"
36
+ end
37
+ ```
38
+
39
+ ## Bulk SMS
40
+
41
+ Send the same message or different messages to multiple recipients at once.
42
+
43
+ ### Same Message to Multiple Recipients
44
+
45
+ ```ruby
46
+ bulk = client.sms.send_bulk(
47
+ recipients: [
48
+ '255712345678',
49
+ '255754321098',
50
+ '255622345678',
51
+ '255652345678',
52
+ '255742345678'
53
+ ],
54
+ message: 'Reminder: Our office will be closed on Monday 1st January for the New Year holiday. Happy New Year from ACME Ltd!',
55
+ sender_id: 'ACME'
56
+ )
57
+
58
+ if bulk['success']
59
+ puts "Bulk SMS sent to #{bulk['data']} recipients"
60
+ end
61
+ ```
62
+
63
+ ### Individual Messages per Recipient
64
+
65
+ ```ruby
66
+ personalized = client.sms.send_bulk(
67
+ messages: [
68
+ {
69
+ to: '255712345678',
70
+ message: 'Hi Juma, your account balance is TZS 125,000. Login at app.malipopay.co.tz to view details.'
71
+ },
72
+ {
73
+ to: '255754321098',
74
+ message: 'Hi Amina, your account balance is TZS 340,500. Login at app.malipopay.co.tz to view details.'
75
+ },
76
+ {
77
+ to: '255622345678',
78
+ message: 'Hi Baraka, your account balance is TZS 78,200. Login at app.malipopay.co.tz to view details.'
79
+ }
80
+ ],
81
+ sender_id: 'ACME'
82
+ )
83
+ ```
84
+
85
+ ## Scheduling SMS
86
+
87
+ Schedule messages for future delivery by providing a `scheduled_at` timestamp in ISO 8601 format. All times are in East Africa Time (EAT, UTC+3):
88
+
89
+ ```ruby
90
+ # Schedule a payment reminder for 9:00 AM tomorrow
91
+ scheduled = client.sms.send_message(
92
+ to: '255712345678',
93
+ message: 'Reminder: Your subscription of TZS 15,000 is due tomorrow. Pay now via M-Pesa to avoid interruption.',
94
+ sender_id: 'ACME',
95
+ scheduled_at: '2024-03-15T09:00:00+03:00'
96
+ )
97
+
98
+ if scheduled['success']
99
+ puts "SMS scheduled: #{scheduled['data']}"
100
+ end
101
+ ```
102
+
103
+ ### Schedule Bulk SMS
104
+
105
+ ```ruby
106
+ # Schedule a promotional message for Friday at 2:00 PM
107
+ scheduled_bulk = client.sms.send_bulk(
108
+ recipients: [
109
+ '255712345678',
110
+ '255754321098',
111
+ '255652345678'
112
+ ],
113
+ message: 'Weekend offer! Get 20% off all services this Saturday and Sunday. Visit our shop on Samora Avenue or pay via M-Pesa. Code: WEEKEND20',
114
+ sender_id: 'ACME',
115
+ scheduled_at: '2024-03-15T14:00:00+03:00'
116
+ )
117
+ ```
118
+
119
+ ## Sender IDs
120
+
121
+ The `sender_id` field controls what appears as the sender on the recipient's phone. This is the name or short code displayed instead of a phone number.
122
+
123
+ | Sender ID | Description |
124
+ |-----------|-------------|
125
+ | `MALIPOPAY` | Default MaliPoPay sender ID |
126
+ | Custom (e.g., `ACME`) | Your registered brand name |
127
+
128
+ ### Registering a Custom Sender ID
129
+
130
+ Custom sender IDs must be registered and approved in your MaliPoPay dashboard:
131
+
132
+ 1. Go to [app.malipopay.co.tz](https://app.malipopay.co.tz) > **Settings > SMS > Sender IDs**
133
+ 2. Click **Request New Sender ID**
134
+ 3. Enter your desired sender name (up to 11 characters, alphanumeric)
135
+ 4. Submit for approval -- this typically takes 24-48 hours
136
+
137
+ > **Note:** TCRA regulations in Tanzania require sender IDs to be registered. Unregistered sender IDs will be replaced with a default numeric sender.
138
+
139
+ ## SMS Character Limits
140
+
141
+ Standard SMS messages have a 160-character limit per segment. Messages longer than 160 characters are split into multiple segments and reassembled on the recipient's device:
142
+
143
+ | Length | Segments | Effective Characters per Segment |
144
+ |--------|----------|----------------------------------|
145
+ | 1--160 | 1 | 160 |
146
+ | 161--306 | 2 | 153 (7 bytes used for concatenation header) |
147
+ | 307--459 | 3 | 153 |
148
+
149
+ Billing is per segment. Keep messages concise to minimize costs.
150
+
151
+ ## Complete Example: Payment Confirmation SMS
152
+
153
+ A common pattern is sending an SMS after a successful payment webhook:
154
+
155
+ ```ruby
156
+ # In your Sinatra/Rails webhook handler
157
+ post '/webhooks/malipopay' do
158
+ # ... verify signature (see Webhooks guide) ...
159
+
160
+ event = verifier.construct_event(payload, signature)
161
+
162
+ if event['event_type'] == 'payment.completed'
163
+ client.sms.send_message(
164
+ to: event['phone'],
165
+ message: "Payment confirmed! TZS #{event['amount']} received for #{event['reference']}. " \
166
+ "Transaction ID: #{event['transaction_id']}. Thank you!",
167
+ sender_id: 'ACME'
168
+ )
169
+
170
+ puts "Confirmation SMS sent to #{event['phone']}"
171
+ end
172
+
173
+ status 200
174
+ end
175
+ ```
176
+
177
+ ## Next Steps
178
+
179
+ - [Payments](./payments.md) -- collect payments that trigger SMS notifications
180
+ - [Webhooks](./webhooks.md) -- automate SMS sending from webhook events
181
+ - [Error Handling](./error-handling.md) -- handle SMS delivery failures
182
+ - [Configuration](./configuration.md) -- configure timeouts and retries