lipwa 0.1.1 → 0.1.2
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/lib/lipwa/version.rb +1 -1
- metadata +1 -2
- data/plan.md +0 -282
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: afc2872c6fa2654eb15ce20c62a1a6027e93f9d7a3b4aceaa0c6420ac96b29d3
|
|
4
|
+
data.tar.gz: a58735e4ccecf1e745d8287db798b9b6993b0da08cff0511c7927d182c239a42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6abf4adc039b3930c73663e1dfb6888b2a3b13846a3637fd17a13ff6909776b28e0ac340ab6bc9f8db78f56b2a3eea5d41bd9f9ec5fb5a466282480c849c044
|
|
7
|
+
data.tar.gz: 57b7f247579e9d6c810f29fe0998b2bac9c1d027c9cd39780ae64d567a9813b0b34cd7020a7fd0e5f558559600563becdebfcb3cd7f77fc43c6694e9384e861e
|
data/lib/lipwa/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lipwa
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paul Oguda
|
|
@@ -182,7 +182,6 @@ files:
|
|
|
182
182
|
- lib/lipwa/version.rb
|
|
183
183
|
- lib/lipwa/webhook.rb
|
|
184
184
|
- lib/lipwa/webhooks/mpesa.rb
|
|
185
|
-
- plan.md
|
|
186
185
|
- sig/lipwa.rbs
|
|
187
186
|
homepage: https://github.com/kamalogudah/lipwa
|
|
188
187
|
licenses:
|
data/plan.md
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
# Lipwa — Plan
|
|
2
|
-
|
|
3
|
-
Unified Ruby gem for accepting and disbursing payments across African payment
|
|
4
|
-
providers — cards *and* non-card rails (mobile money, bank APIs, USSD push,
|
|
5
|
-
B2B/B2C disbursements). Spiritually `active_merchant`, but the base
|
|
6
|
-
abstraction is payment-*flow* shaped, not card-transaction shaped, and the
|
|
7
|
-
implementation style follows [dry-rb](https://dry-rb.org) conventions the
|
|
8
|
-
way [`arafa`](https://github.com/kamalogudah/arafa) does: typed value
|
|
9
|
-
objects, a `Result`/monad-based control flow instead of exceptions for
|
|
10
|
-
expected failures, a container-based gateway registry, and
|
|
11
|
-
`dry-configurable` for per-gateway settings.
|
|
12
|
-
|
|
13
|
-
## 1. Why not just copy ActiveMerchant's shape
|
|
14
|
-
|
|
15
|
-
ActiveMerchant's core contract (`purchase`, `authorize`, `capture`, `void`,
|
|
16
|
-
`refund`) assumes a card token/reference you hold and act on synchronously.
|
|
17
|
-
Most African rails don't work that way:
|
|
18
|
-
|
|
19
|
-
- **M-Pesa STK Push (Daraja)** — you *initiate* a push to the payer's phone,
|
|
20
|
-
then get the actual result later via an async **callback**. There's no
|
|
21
|
-
synchronous "yes it worked" response.
|
|
22
|
-
- **M-Pesa C2B** — payer initiates from their phone (paybill/till), your
|
|
23
|
-
system only receives a **validation** webhook then a **confirmation**
|
|
24
|
-
webhook.
|
|
25
|
-
- **M-Pesa B2C / B2B** — you're disbursing, not collecting. Different
|
|
26
|
-
direction of money flow entirely.
|
|
27
|
-
- **Co-op Bank / Jenga (Equity)** — bank-account-to-bank-account transfers,
|
|
28
|
-
balance/statement queries, bill payment, and *also* mobile money and card
|
|
29
|
-
rails bundled behind one API key.
|
|
30
|
-
|
|
31
|
-
So the gem needs a capability model, not a single fixed interface: a gateway
|
|
32
|
-
declares which capabilities it supports (`:purchase`, `:stk_push`, `:c2b`,
|
|
33
|
-
`:disbursement`, `:refund`, `:status_query`, `:webhook_handling`, ...) and
|
|
34
|
-
only those methods are expected to exist on it.
|
|
35
|
-
|
|
36
|
-
## 2. Design principles (dry-rb style, per `arafa`)
|
|
37
|
-
|
|
38
|
-
- **Functional core, thin I/O shell.** Business/validation logic is pure
|
|
39
|
-
where possible; HTTP calls are isolated behind an adapter object so they're
|
|
40
|
-
easy to stub in tests.
|
|
41
|
-
- **No exceptions for expected failure modes.** Public gateway methods
|
|
42
|
-
return `Dry::Monads::Result` (`Success(Lipwa::Response)` /
|
|
43
|
-
`Failure(Lipwa::Error)`). Exceptions are reserved for programmer errors
|
|
44
|
-
(bad config, unknown capability) — same split `arafa` uses.
|
|
45
|
-
- **Immutable value objects everywhere.** Requests, responses, money
|
|
46
|
-
amounts, and credentials are `Dry::Struct` instances built from
|
|
47
|
-
`Dry::Types`, not hashes. No mutation after construction.
|
|
48
|
-
- **Schema-validated input at the boundary.** Every gateway call validates
|
|
49
|
-
its params through a `Dry::Validation::Contract` before touching the
|
|
50
|
-
network, so a bad phone number or missing till number fails fast with a
|
|
51
|
-
structured error, not a cryptic HTTP 400 from the provider.
|
|
52
|
-
- **Config via `Dry::Configurable`.** Each gateway class exposes its own
|
|
53
|
-
settable, typed config block (`consumer_key`, `shortcode`, `passkey`,
|
|
54
|
-
`env: :sandbox | :production`, timeouts). Global `Lipwa.configure` block
|
|
55
|
-
sets shared defaults (logger, default timeout, faraday adapter).
|
|
56
|
-
- **Registry via `Dry::Container` + `Dry::System`-style auto-registration.**
|
|
57
|
-
`Lipwa::Gateways.register(:mpesa, Lipwa::Gateways::Mpesa)` — lets
|
|
58
|
-
consumers do `Lipwa.gateway(:mpesa).stk_push(...)` without knowing the
|
|
59
|
-
class name, and lets the gem add providers later without touching a
|
|
60
|
-
central case statement.
|
|
61
|
-
- **Composable capabilities as modules**, not a fat base class with
|
|
62
|
-
`raise NotImplementedError` stubs. `include Lipwa::Capabilities::StkPush`
|
|
63
|
-
brings in `#stk_push` plus its own contract; a gateway only includes what
|
|
64
|
-
it actually supports. Calling an uninlcuded capability is a `NoMethodError`
|
|
65
|
-
at the Ruby level — no silent "not supported" runtime surprises.
|
|
66
|
-
- **Explicit over magic**, small single-purpose objects, no monkey-patching
|
|
67
|
-
of core classes — same restraint dry-rb libraries hold to.
|
|
68
|
-
|
|
69
|
-
## 3. Core abstractions
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
Lipwa::Money # Dry::Struct — amount (Integer, minor units) + currency (ISO 4217)
|
|
73
|
-
Lipwa::Response # Dry::Struct — success, provider_reference, raw, message, code
|
|
74
|
-
Lipwa::Error # base StandardError; subclasses: ConfigurationError,
|
|
75
|
-
# ValidationError, GatewayError, UnsupportedCapabilityError
|
|
76
|
-
Lipwa::Gateway # abstract base: holds Dry::Configurable settings,
|
|
77
|
-
# an #http adapter, #capability?(name)
|
|
78
|
-
Lipwa::Capabilities::* # one module per flow shape (below)
|
|
79
|
-
Lipwa::Contracts::* # Dry::Validation::Contract per request type
|
|
80
|
-
Lipwa::Webhook # inbound-callback parsing/verification helper
|
|
81
|
-
Lipwa::Gateways # the Dry::Container registry + .register/.[] /.gateway
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Capability modules (each with its own request/response Structs + contract):
|
|
85
|
-
|
|
86
|
-
| Capability | Method(s) | Used by (initial) |
|
|
87
|
-
|-------------------------|-------------------------------------|--------------------|
|
|
88
|
-
| `Purchase` | `#purchase` | card gateways (later) |
|
|
89
|
-
| `Authorize`/`Capture` | `#authorize`, `#capture`, `#void` | card gateways (later) |
|
|
90
|
-
| `Refund` | `#refund` | most gateways |
|
|
91
|
-
| `StkPush` | `#stk_push` | M-Pesa |
|
|
92
|
-
| `C2B` | `#register_urls`, `#simulate` | M-Pesa |
|
|
93
|
-
| `Disbursement` (B2C/B2B)| `#disburse` | M-Pesa, Jenga |
|
|
94
|
-
| `BankTransfer` | `#transfer`, `#balance`, `#statement` | Co-op Bank, Jenga |
|
|
95
|
-
| `StatusQuery` | `#status` | all async flows |
|
|
96
|
-
| `WebhookHandling` | `.parse_webhook`, `#verify_signature` | M-Pesa, Co-op, Jenga |
|
|
97
|
-
| `LightningInvoice` | `#create_invoice`, `#check_invoice` | LNbits |
|
|
98
|
-
|
|
99
|
-
## 4. Directory structure
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
lib/lipwa.rb
|
|
103
|
-
lib/lipwa/version.rb
|
|
104
|
-
lib/lipwa/types.rb # Dry::Types module (Lipwa::Types)
|
|
105
|
-
lib/lipwa/money.rb
|
|
106
|
-
lib/lipwa/response.rb
|
|
107
|
-
lib/lipwa/errors.rb
|
|
108
|
-
lib/lipwa/gateway.rb # abstract base
|
|
109
|
-
lib/lipwa/gateways.rb # Dry::Container registry
|
|
110
|
-
lib/lipwa/http_adapter.rb # Faraday wrapper, retries/timeouts/logging
|
|
111
|
-
lib/lipwa/webhook.rb
|
|
112
|
-
lib/lipwa/capabilities/
|
|
113
|
-
purchase.rb
|
|
114
|
-
authorize.rb
|
|
115
|
-
refund.rb
|
|
116
|
-
stk_push.rb
|
|
117
|
-
c2b.rb
|
|
118
|
-
disbursement.rb
|
|
119
|
-
bank_transfer.rb
|
|
120
|
-
status_query.rb
|
|
121
|
-
webhook_handling.rb
|
|
122
|
-
lib/lipwa/contracts/
|
|
123
|
-
stk_push_contract.rb
|
|
124
|
-
disbursement_contract.rb
|
|
125
|
-
...
|
|
126
|
-
lib/lipwa/gateways/
|
|
127
|
-
mpesa.rb
|
|
128
|
-
mpesa/auth.rb # OAuth token caching for Daraja
|
|
129
|
-
coop_bank.rb
|
|
130
|
-
jenga.rb
|
|
131
|
-
test/
|
|
132
|
-
support/shared_examples/ # e.g. "a disbursement capability"
|
|
133
|
-
gateways/mpesa_test.rb
|
|
134
|
-
...
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## 5. Provider notes (drives what the abstractions must support)
|
|
138
|
-
|
|
139
|
-
- **Safaricom Daraja (M-Pesa)** — OAuth2 client-credentials token (cache +
|
|
140
|
-
auto-refresh), STK Push (Lipa Na M-Pesa Online), C2B validation/
|
|
141
|
-
confirmation webhooks, B2C, B2B, Transaction Status, Account Balance.
|
|
142
|
-
Sandbox and production have different base URLs and shortcodes.
|
|
143
|
-
- **Co-op Bank Developer Portal** — API-key + OAuth, account inquiry, funds
|
|
144
|
-
transfer (internal/RTGS/PesaLink), bill payments, statement/balance
|
|
145
|
-
queries, M-Pesa integration passthrough.
|
|
146
|
-
- **Jenga HQ (Equity)** — OAuth2 + request signing (private key signature
|
|
147
|
-
header per request), send money (mobile money/bank/RTGS/SWIFT), bill
|
|
148
|
-
payments, receive payments (webhook), forex rates, account services.
|
|
149
|
-
- Common shape across all three: OAuth-ish token acquisition, HMAC/RSA
|
|
150
|
-
request signing or bearer tokens, sandbox vs. production hosts, and
|
|
151
|
-
async confirmation via webhook for anything collection-related. This is
|
|
152
|
-
why `HttpAdapter` needs pluggable auth strategies and `Webhook` needs
|
|
153
|
-
pluggable signature verification, rather than being M-Pesa-specific.
|
|
154
|
-
- **LNbits (Bitcoin Lightning Network)** — self-hosted or hosted instance,
|
|
155
|
-
plain REST/JSON API, static `X-Api-Key` header auth (no OAuth, no request
|
|
156
|
-
signing). Invoices are BOLT11-denominated in satoshis, not ISO-4217
|
|
157
|
-
currency. No gRPC/websocket streaming needed (unlike talking to a raw
|
|
158
|
-
LND/CLN node directly) — this is why it was picked as the first Lightning
|
|
159
|
-
backend: it fits the existing `HttpAdapter`/`AuthStrategies` shape with no
|
|
160
|
-
new dependencies. LNbits does not sign its outbound payment webhooks, so
|
|
161
|
-
verification uses an integrator-minted shared-secret token embedded in the
|
|
162
|
-
webhook URL rather than an HMAC (see `LightningInvoice` capability notes).
|
|
163
|
-
A raw LND/CLN gateway (gRPC + macaroon auth + `SubscribeInvoices` streaming)
|
|
164
|
-
is a possible future gateway but is out of scope for now.
|
|
165
|
-
|
|
166
|
-
## 6. Example usage (target API)
|
|
167
|
-
|
|
168
|
-
```ruby
|
|
169
|
-
Lipwa.configure do |config|
|
|
170
|
-
config.logger = Rails.logger
|
|
171
|
-
config.default_timeout = 10
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
Lipwa::Gateways::Mpesa.configure do |c|
|
|
175
|
-
c.env = :sandbox
|
|
176
|
-
c.consumer_key = ENV["MPESA_CONSUMER_KEY"]
|
|
177
|
-
c.consumer_secret = ENV["MPESA_CONSUMER_SECRET"]
|
|
178
|
-
c.shortcode = ENV["MPESA_SHORTCODE"]
|
|
179
|
-
c.passkey = ENV["MPESA_PASSKEY"]
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
result = Lipwa.gateway(:mpesa).stk_push(
|
|
183
|
-
amount: Lipwa::Money.new(amount: 1_00, currency: "KES"),
|
|
184
|
-
phone_number: "254712345678",
|
|
185
|
-
account_reference: "ORDER-123",
|
|
186
|
-
callback_url: "https://example.com/webhooks/mpesa"
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
result.either(
|
|
190
|
-
->(response) { response.provider_reference }, # CheckoutRequestID
|
|
191
|
-
->(error) { logger.error(error.message) }
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# later, in the webhook controller:
|
|
195
|
-
event = Lipwa::Gateways::Mpesa.parse_webhook(request.body.read, headers: request.headers)
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
## 7. Dependencies
|
|
199
|
-
|
|
200
|
-
- `dry-types`, `dry-struct` — value objects
|
|
201
|
-
- `dry-monads` — `Result`/`Maybe`, `do` notation for chaining validate →
|
|
202
|
-
call → parse
|
|
203
|
-
- `dry-validation` — request contracts
|
|
204
|
-
- `dry-configurable` — per-gateway and global config
|
|
205
|
-
- `dry-container` — gateway registry
|
|
206
|
-
- `faraday` (+ `faraday-retry`) — HTTP, kept behind `Lipwa::HttpAdapter` so
|
|
207
|
-
it could be swapped later
|
|
208
|
-
- Dev/test: `webmock`, `vcr`, `minitest` (keeping the scaffolded framework).
|
|
209
|
-
|
|
210
|
-
## 8. Error handling strategy
|
|
211
|
-
|
|
212
|
-
- `Lipwa::Error` (base) → `ConfigurationError`, `ValidationError` (wraps a
|
|
213
|
-
`Dry::Validation` result), `GatewayError` (wraps provider HTTP/timeout
|
|
214
|
-
failures with `code`/`raw_body`), `UnsupportedCapabilityError`.
|
|
215
|
-
- Only `ConfigurationError` and `UnsupportedCapabilityError` are raised —
|
|
216
|
-
these are programmer mistakes, fail loud at boot/call time.
|
|
217
|
-
- `ValidationError` and `GatewayError` are wrapped in `Failure(...)`, never
|
|
218
|
-
raised, so callers must handle them explicitly via the monad.
|
|
219
|
-
|
|
220
|
-
## 9. Testing strategy
|
|
221
|
-
|
|
222
|
-
- Contract/shared-example suites per capability (e.g. "a StkPush
|
|
223
|
-
capability") that every gateway implementing it must pass — catches
|
|
224
|
-
interface drift as providers are added.
|
|
225
|
-
- VCR/WebMock cassettes per gateway for real (sanitized) sandbox
|
|
226
|
-
request/response shapes; never hit live sandboxes in CI.
|
|
227
|
-
- Explicit tests for webhook signature verification with both valid and
|
|
228
|
-
tampered payloads.
|
|
229
|
-
|
|
230
|
-
## 10. Phased roadmap
|
|
231
|
-
|
|
232
|
-
1. **Foundation** — `Types`, `Money`, `Response`, `Error` hierarchy,
|
|
233
|
-
`HttpAdapter`, `Gateway` base, `Gateways` container/registry, dry
|
|
234
|
-
dependencies wired into gemspec, CI green.
|
|
235
|
-
2. **M-Pesa (Daraja)** — OAuth token handling, `StkPush`, `C2B`
|
|
236
|
-
(validation/confirmation), `WebhookHandling`. Highest-value target
|
|
237
|
-
given ubiquity in Kenya.
|
|
238
|
-
3. **M-Pesa B2C/B2B** — `Disbursement` capability, `StatusQuery`,
|
|
239
|
-
`Refund` (reversal).
|
|
240
|
-
4. **Co-op Bank** — `BankTransfer`, account inquiry/statement, reuse
|
|
241
|
-
`WebhookHandling`/`StatusQuery` contracts against its shapes.
|
|
242
|
-
5. **Jenga HQ** — request-signing auth strategy (new `HttpAdapter` auth
|
|
243
|
-
plugin), `BankTransfer`, `Disbursement`, receive-payment webhook.
|
|
244
|
-
6. **Hardening** — idempotency key support, structured logging/redaction
|
|
245
|
-
of secrets, retry/backoff policy, richer `Money` (currency math via
|
|
246
|
-
`dry-types` constrained decimals), documentation site, README rewrite.
|
|
247
|
-
7. **Card rails (stretch)** — Pesapal / Flutterwave / Paystack as
|
|
248
|
-
`Purchase`/`Authorize`/`Capture`/`Void` implementers, proving the
|
|
249
|
-
capability model also covers the ActiveMerchant-shaped case.
|
|
250
|
-
8. **Lightning Network (LNbits), receive-only** — new `AuthStrategies::ApiKey`,
|
|
251
|
-
`LightningInvoice` capability (`#create_invoice`, `#check_invoice`),
|
|
252
|
-
`Lipwa::Gateways::Lnbits`, `Lipwa::Webhooks::Lnbits` (shared-secret-token
|
|
253
|
-
verification, no HMAC available from LNbits). Proves the capability model
|
|
254
|
-
also covers a non-fiat, non-ISO-4217 rail. Outbound `pay_invoice` (spending
|
|
255
|
-
sats, needs the LNbits admin/full key rather than invoice/read) is a
|
|
256
|
-
later phase, not v1.
|
|
257
|
-
|
|
258
|
-
## 11. Decisions
|
|
259
|
-
|
|
260
|
-
- **Test framework**: `minitest`, as already scaffolded.
|
|
261
|
-
- **Currency scope**: KES-first. `Money`/contracts aren't hardcoded to
|
|
262
|
-
KES, but no work goes into other currencies until a provider actually
|
|
263
|
-
needs one.
|
|
264
|
-
|
|
265
|
-
## 12. Open questions to confirm before/while building
|
|
266
|
-
|
|
267
|
-
- Sync vs. async webhook story: does the gem ship a Rack/Rails-agnostic
|
|
268
|
-
webhook *parser* only (current plan), or also engine/controller helpers?
|
|
269
|
-
**Resolved for LNbits**: parser-only, same as M-Pesa — no Rack/Rails
|
|
270
|
-
controller helper is being added for this gateway either. Still an open
|
|
271
|
-
question for whether the gem ever adds one generically.
|
|
272
|
-
- Multi-tenant credentials: is per-process global gateway config
|
|
273
|
-
(`Dry::Configurable` class-level) sufficient, or do we need per-instance
|
|
274
|
-
configured gateway objects for apps serving multiple merchants/accounts
|
|
275
|
-
from one process?
|
|
276
|
-
- LNbits webhook trust model: since LNbits doesn't HMAC-sign webhooks, v1
|
|
277
|
-
uses an integrator-minted shared-secret token embedded in the `webhook_url`
|
|
278
|
-
itself, verified via constant-time comparison, with `#check_invoice`
|
|
279
|
-
required as an independent re-verification before crediting payment. Revisit
|
|
280
|
-
if a future LNbits version/extension adds real HMAC signing — `WebhookEvent#verify_signature`
|
|
281
|
-
already accepts provider-specific `**opts`, so a second verification mode
|
|
282
|
-
could be added without an interface change.
|