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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lipwa/version.rb +1 -1
  3. metadata +1 -2
  4. data/plan.md +0 -282
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c3c39d3377082721898d3509aad157a7736fe63907624e3af5c4a55268f9329
4
- data.tar.gz: 1a6d082d6196adb81e1ee610609663156dbf3f35e4c2f82a5f7ab54b66797a7f
3
+ metadata.gz: afc2872c6fa2654eb15ce20c62a1a6027e93f9d7a3b4aceaa0c6420ac96b29d3
4
+ data.tar.gz: a58735e4ccecf1e745d8287db798b9b6993b0da08cff0511c7927d182c239a42
5
5
  SHA512:
6
- metadata.gz: c5fdb5ec67a5b767ef60d0b709540a666cbede85e80d12880e8eaf956a09b57ae0088727ad35bff6c7c5b0ef65c5791bd290b146b093bf65c191c2b011b27a39
7
- data.tar.gz: 4e078b48e6ca4b10cf76bcff8fc1c2a5d8e5c0c0e1534dd56131b282dfb8b23c0f560adf77af02498a16d55da4c33cd636ccf31a70440248ad6673962a4dc76d
6
+ metadata.gz: e6abf4adc039b3930c73663e1dfb6888b2a3b13846a3637fd17a13ff6909776b28e0ac340ab6bc9f8db78f56b2a3eea5d41bd9f9ec5fb5a466282480c849c044
7
+ data.tar.gz: 57b7f247579e9d6c810f29fe0998b2bac9c1d027c9cd39780ae64d567a9813b0b34cd7020a7fd0e5f558559600563becdebfcb3cd7f77fc43c6694e9384e861e
data/lib/lipwa/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lipwa
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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.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.