lipwa 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ed1d0107830b6fafe1757cc253a820b57fe62c99f38ec78e1d2adde62e10eaa
4
- data.tar.gz: 85994d9fe4d49e276892154fc600ae06f420740a55c5580f9bbac6cfbcc3d14b
3
+ metadata.gz: 184f658687e8cc70f00f9053c7ee5eddf4a3d19679843dd74604abcba4806183
4
+ data.tar.gz: 7c3d66d4138ceaf01fb053d5cbb11d499321016f0e0078e63fbab24252a9e68a
5
5
  SHA512:
6
- metadata.gz: 37ebc5f8bff81213d7949b33c984ac4f18bf32b6386b25323665fcf86f8f6a1d838759cbcb4c49fba3f0f4274d1e3a6bdabcd8b78600bf24ad23554e26adc253
7
- data.tar.gz: 83d40bb47c71c2fb3f98ba51021b3bcbee44d91610d8407fa44975add4261f894c999f5cb109a370af1bae301fee906ef0f0e89de75993799e3846f7085eec89
6
+ metadata.gz: 0d6a8dd5390049cc230bca9cfffe383eaa511b112e038e1ae66aec4d87548e77714b210e53e58f29bf4be95ab830c77be8a75fd31236f7fa50018261d482c947
7
+ data.tar.gz: ce26c22b739fcdc0c27e5c97a8379722db6b8a8ddd87c2d7e744817ab3f2f162b149e5c47734c10a93582fd9aee463b73e2efc0a7aeef774ce7dec1344034eb3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.1.4] - 2026-09-07
2
+
3
+ - Use an explicit package allowlist to exclude documentation site sources,
4
+ development tasks, contributor guidance, and placeholder RBS signatures from
5
+ the installed gem while retaining runtime files, the license, README, and changelog
6
+
1
7
  ## [0.1.3] - 2026-09-06
2
8
 
3
9
  - Add Co-op Bank support for bank transfers, balance and statement inquiries,
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.3"
4
+ VERSION = "0.1.4"
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.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Oguda
@@ -147,20 +147,8 @@ extensions: []
147
147
  extra_rdoc_files: []
148
148
  files:
149
149
  - CHANGELOG.md
150
- - CODE_OF_CONDUCT.md
151
150
  - LICENSE.txt
152
151
  - README.md
153
- - Rakefile
154
- - docs/README.md
155
- - docs/_config.yml
156
- - docs/_layouts/default.html
157
- - docs/assets/css/style.css
158
- - docs/capabilities.md
159
- - docs/gateways.md
160
- - docs/getting-started.md
161
- - docs/index.md
162
- - docs/reliability.md
163
- - docs/webhooks.md
164
152
  - lib/lipwa.rb
165
153
  - lib/lipwa/auth_strategies.rb
166
154
  - lib/lipwa/auth_strategies/api_key.rb
@@ -223,7 +211,6 @@ files:
223
211
  - lib/lipwa/webhooks/jenga.rb
224
212
  - lib/lipwa/webhooks/lnbits.rb
225
213
  - lib/lipwa/webhooks/mpesa.rb
226
- - sig/lipwa.rbs
227
214
  homepage: https://github.com/kamalogudah/lipwa
228
215
  licenses:
229
216
  - MIT
data/CODE_OF_CONDUCT.md DELETED
@@ -1,10 +0,0 @@
1
- # Code of Conduct
2
-
3
- "lipwa" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
-
5
- * Participants will be tolerant of opposing views.
6
- * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
- * When interpreting the words and actions of others, participants should always assume good intentions.
8
- * Behaviour which can be reasonably considered harassment will not be tolerated.
9
-
10
- If you have any concerns about behaviour within this project, please contact us at ["mcpaul2058@gmail.com"](mailto:"mcpaul2058@gmail.com").
data/Rakefile DELETED
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
-
6
- Minitest::TestTask.create
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- namespace :docs do
13
- desc "Build the documentation site"
14
- task :build do
15
- require "jekyll"
16
- Jekyll::Commands::Build.process(
17
- source: File.expand_path("docs", __dir__),
18
- destination: File.expand_path("_site", __dir__)
19
- )
20
- end
21
- end
22
-
23
- task default: %i[test rubocop]
data/docs/README.md DELETED
@@ -1,15 +0,0 @@
1
- # Lipwa documentation site
2
-
3
- This directory is the source for the Lipwa documentation site. It uses Jekyll
4
- with a custom layout and has no theme or plugin dependency.
5
-
6
- From the repository root:
7
-
8
- ```bash
9
- bundle install
10
- bundle exec rake docs:build
11
- ```
12
-
13
- The generated site is written to `_site/`. GitHub Pages can publish directly
14
- from the `docs/` directory on the main branch; `baseurl` in `_config.yml` is
15
- already set for the `/lipwa` project path.
data/docs/_config.yml DELETED
@@ -1,11 +0,0 @@
1
- title: Lipwa
2
- description: Capability-based payment APIs for Ruby
3
- url: https://kamalogudah.github.io
4
- baseurl: /lipwa
5
- permalink: pretty
6
- markdown: kramdown
7
- defaults:
8
- - scope:
9
- path: ""
10
- values:
11
- layout: default
@@ -1,37 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="theme-color" content="#102a25">
7
- <meta name="description" content="{{ page.description | default: site.description }}">
8
- <title>{% if page.title %}{{ page.title }} · {% endif %}{{ site.title }}</title>
9
- <link rel="stylesheet" href="{{ '/assets/css/style.css' | relative_url }}">
10
- </head>
11
- <body>
12
- <a class="skip-link" href="#content">Skip to content</a>
13
- <header class="site-header">
14
- <a class="brand" href="{{ '/' | relative_url }}"><span class="brand-mark">L</span> Lipwa</a>
15
- <button class="nav-toggle" type="button" aria-expanded="false" aria-controls="site-nav">Menu</button>
16
- <nav id="site-nav" class="site-nav" aria-label="Primary">
17
- <a href="{{ '/getting-started/' | relative_url }}">Start</a>
18
- <a href="{{ '/capabilities/' | relative_url }}">Capabilities</a>
19
- <a href="{{ '/gateways/' | relative_url }}">Gateways</a>
20
- <a href="{{ '/webhooks/' | relative_url }}">Webhooks</a>
21
- <a href="{{ '/reliability/' | relative_url }}">Reliability</a>
22
- <a href="https://github.com/kamalogudah/lipwa">GitHub ↗</a>
23
- </nav>
24
- </header>
25
- <main id="content" class="page-shell">{{ content }}</main>
26
- <footer class="site-footer"><span>Lipwa · payment flows, honestly modeled</span><a href="https://github.com/kamalogudah/lipwa/blob/master/LICENSE.txt">MIT licensed</a></footer>
27
- <script>
28
- const button = document.querySelector(".nav-toggle");
29
- const nav = document.querySelector(".site-nav");
30
- button.addEventListener("click", () => {
31
- const open = button.getAttribute("aria-expanded") === "true";
32
- button.setAttribute("aria-expanded", String(!open));
33
- nav.classList.toggle("is-open", !open);
34
- });
35
- </script>
36
- </body>
37
- </html>
@@ -1,8 +0,0 @@
1
- :root{--ink:#15231f;--muted:#586b65;--paper:#fbfaf5;--panel:#f0f4ed;--line:#d7dfd7;--green:#16735d;--dark:#102a25;--lime:#d8f06d;--code:#11231f;--max:1120px}
2
- *{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:6rem}body{margin:0;color:var(--ink);background:radial-gradient(circle at 88% 2%,rgba(216,240,109,.24),transparent 24rem),var(--paper);font:17px/1.7 Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}a{color:var(--green);text-underline-offset:.2em}.skip-link{position:fixed;top:-4rem;left:1rem;z-index:10;padding:.65rem 1rem;color:white;background:var(--dark)}.skip-link:focus{top:1rem}
3
- .site-header{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;min-height:72px;padding:0 max(24px,calc((100vw - var(--max))/2));border-bottom:1px solid rgba(21,35,31,.12);background:rgba(251,250,245,.94);backdrop-filter:blur(16px)}.brand{display:flex;align-items:center;gap:.65rem;color:var(--dark);font-weight:800;text-decoration:none}.brand-mark{display:grid;width:34px;height:34px;place-items:center;border-radius:9px;background:var(--lime);font-family:ui-monospace,monospace}.site-nav{display:flex;gap:1.35rem}.site-nav a{color:var(--ink);font-size:.9rem;font-weight:650;text-decoration:none}.site-nav a:hover{color:var(--green)}.nav-toggle{display:none}
4
- .page-shell{width:min(calc(100% - 48px),var(--max));min-height:calc(100vh - 160px);margin:auto;padding:4.5rem 0 6rem}.hero{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(2rem,6vw,6rem);align-items:center;min-height:560px;padding:2rem 0 5rem}.eyebrow{margin:0 0 .7rem;color:var(--green);font-size:.78rem;font-weight:800;letter-spacing:.13em;text-transform:uppercase}h1,h2,h3{color:var(--dark);font-weight:800;line-height:1.12;letter-spacing:-.035em}h1{max-width:850px;margin:0 0 1.25rem;font-size:clamp(2.65rem,7vw,5.6rem)}h2{margin:3.5rem 0 1rem;font-size:clamp(1.65rem,4vw,2.6rem)}h3{margin:2.2rem 0 .5rem;font-size:1.25rem}p,ul,ol{max-width:780px}.lead{color:var(--muted);font-size:clamp(1.08rem,2vw,1.35rem)}
5
- .actions{display:flex;gap:.75rem;margin-top:2rem}.button{padding:.72rem 1.05rem;border:1px solid var(--dark);border-radius:8px;color:white;background:var(--dark);font-weight:750;text-decoration:none}.button.secondary{color:var(--dark);background:transparent}.terminal{overflow:hidden;border:1px solid #29443c;border-radius:14px;box-shadow:0 24px 80px rgba(16,42,37,.18);background:var(--code)}.terminal-bar{display:flex;gap:6px;padding:14px 16px;border-bottom:1px solid #29443c}.terminal-dot{width:9px;height:9px;border-radius:50%;background:#547269}.terminal-dot:last-child{background:var(--lime)}
6
- pre{overflow-x:auto;padding:1.25rem 1.35rem;border-radius:10px;color:#e7f1ec;background:var(--code);font:.87rem/1.65 ui-monospace,monospace}.terminal pre{margin:0;border-radius:0}code{padding:.12em .35em;border-radius:4px;color:#165b4c;background:#e7eee7;font-size:.88em}pre code{padding:0;color:inherit;background:transparent;font-size:inherit}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin:2rem 0}.card{padding:1.5rem;border:1px solid var(--line);border-radius:12px;background:rgba(255,255,255,.6)}.card h3{margin-top:0}.card p{margin-bottom:0;color:var(--muted);font-size:.95rem}.number{color:var(--green);font:700 .8rem ui-monospace,monospace}.callout{margin:2rem 0;padding:1rem 1.25rem;border-left:4px solid var(--green);background:var(--panel)}
7
- table{width:100%;margin:1.5rem 0 2.5rem;border-collapse:collapse;font-size:.94rem}th,td{padding:.8rem .9rem;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--dark);background:var(--panel);font-size:.78rem;text-transform:uppercase}.page-title{padding-bottom:2rem;border-bottom:1px solid var(--line)}.page-title h1{font-size:clamp(2.5rem,6vw,4.5rem)}.doc-nav{display:flex;justify-content:space-between;margin-top:4rem;padding-top:1.5rem;border-top:1px solid var(--line)}.site-footer{display:flex;justify-content:space-between;padding:2rem max(24px,calc((100vw - var(--max))/2));color:var(--muted);border-top:1px solid var(--line);font-size:.82rem}
8
- @media(max-width:800px){.hero{grid-template-columns:1fr;min-height:auto}.grid{grid-template-columns:1fr}.nav-toggle{display:block;padding:.45rem .7rem;border:1px solid var(--line);border-radius:6px;background:transparent}.site-nav{position:absolute;top:71px;right:0;left:0;display:none;flex-direction:column;padding:.5rem 24px 1rem;background:var(--paper)}.site-nav.is-open{display:flex}.site-nav a{padding:.6rem 0}.page-shell{width:min(calc(100% - 32px),var(--max));padding-top:3rem}.site-footer{flex-direction:column}table{display:block;overflow-x:auto}}
data/docs/capabilities.md DELETED
@@ -1,116 +0,0 @@
1
- ---
2
- title: Capabilities
3
- description: Reference for Lipwa payment operations.
4
- ---
5
-
6
- <header class="page-title"><p class="eyebrow">Guide 02</p><h1>Capabilities</h1><p class="lead">A capability describes a payment flow. Gateways include only the modules they support.</p></header>
7
-
8
- {% highlight ruby %}gateway = Lipwa.gateway(:jenga)
9
- gateway.capability?(:bank_transfer) # => true{% endhighlight %}
10
-
11
- | Capability | Methods | Gateways |
12
- | --- | --- | --- |
13
- | `stk_push` | `stk_push` | M-Pesa |
14
- | `c2b` | `register_urls`, `simulate` | M-Pesa |
15
- | `disbursement` | `disburse` | M-Pesa, Jenga |
16
- | `status_query` | `status` | M-Pesa |
17
- | `refund` | `refund` | M-Pesa |
18
- | `bank_transfer` | `transfer`, `balance`, `statement` | Co-op, Jenga |
19
- | `lightning_invoice` | `create_invoice`, `check_invoice` | LNbits-backed gateways |
20
- | `lightning_payment` | `pay_invoice`, `check_payment` | LNbits-backed gateways |
21
-
22
- Lightning invoice amounts are positive integer satoshis (`amount_sats`), not
23
- `Lipwa::Money` values. A created invoice's BOLT11 string is stored in
24
- `response.raw["payment_request"]`; its payment hash is
25
- `response.provider_reference`. Checking an unpaid invoice still returns
26
- `Success(Response)`, with `response.success? == false`.
27
-
28
- Outbound Lightning payments use the separate `lightning_payment` capability
29
- and require an explicitly configured wallet admin key. That key can spend the
30
- wallet balance; it never replaces the receive-only invoice key. A payment may
31
- remain `pending`, and a transport timeout leaves its outcome unknown. Inspect
32
- `response.raw["status"]` and call `check_payment` to reconcile it; never retry
33
- an indeterminate payment automatically.
34
-
35
- {% highlight ruby %}lnbits.pay_invoice(bolt11: "lnbc...")
36
- lnbits.check_payment(payment_hash: "..."){% endhighlight %}
37
-
38
-
39
- ## Collection
40
-
41
- {% highlight ruby %}mpesa = Lipwa.gateway(:mpesa)
42
-
43
- mpesa.stk_push(
44
- amount: Lipwa::Money.new(amount: 100, currency: "KES"),
45
- phone_number: "254712345678",
46
- account_reference: "ORDER-123",
47
- callback_url: "https://example.com/webhooks/mpesa"
48
- )
49
-
50
- mpesa.register_urls(
51
- validation_url: "https://example.com/mpesa/validation",
52
- confirmation_url: "https://example.com/mpesa/confirmation"
53
- )
54
-
55
- mpesa.simulate(
56
- amount: Lipwa::Money.new(amount: 100, currency: "KES"),
57
- phone_number: "254712345678",
58
- bill_ref_number: "ORDER-123"
59
- ){% endhighlight %}
60
-
61
- `simulate` is for M-Pesa sandbox testing.
62
-
63
- ## Disbursement, status, and refund
64
-
65
- {% highlight ruby %}mpesa.disburse(
66
- command_id: "BusinessPayment",
67
- amount: Lipwa::Money.new(amount: 500, currency: "KES"),
68
- party_b: "254712345678",
69
- remarks: "Supplier payout",
70
- result_url: "https://example.com/mpesa/payout/result",
71
- queue_timeout_url: "https://example.com/mpesa/payout/timeout"
72
- )
73
-
74
- mpesa.status(
75
- transaction_id: "OEI2AK4Q16",
76
- remarks: "Reconcile order",
77
- result_url: "https://example.com/mpesa/status/result",
78
- queue_timeout_url: "https://example.com/mpesa/status/timeout"
79
- )
80
-
81
- mpesa.refund(
82
- transaction_id: "OEI2AK4Q16",
83
- amount: Lipwa::Money.new(amount: 100, currency: "KES"),
84
- remarks: "Customer refund",
85
- result_url: "https://example.com/mpesa/refund/result",
86
- queue_timeout_url: "https://example.com/mpesa/refund/timeout"
87
- ){% endhighlight %}
88
-
89
- M-Pesa B2C command IDs are `SalaryPayment`, `BusinessPayment`, and
90
- `PromotionPayment`. B2B supports `BusinessPayBill`, `BusinessBuyGoods`, and
91
- `MerchantToMerchantTransfer` and requires `account_reference`.
92
-
93
- ## Bank operations
94
-
95
- {% highlight ruby %}bank = Lipwa.gateway(:jenga)
96
-
97
- bank.transfer(
98
- rail: :pesalink,
99
- amount: Lipwa::Money.new(amount: "2500.00", currency: "KES"),
100
- source_account: "00123456789",
101
- destination_account: "00987654321",
102
- destination_name: "Amina N.",
103
- reference: "INV-1042"
104
- )
105
-
106
- bank.balance(account_number: "00123456789")
107
- bank.statement(account_number: "00123456789",
108
- from_date: Date.new(2026, 8, 1),
109
- to_date: Date.new(2026, 8, 31)){% endhighlight %}
110
-
111
- Rails are `internal`, `rtgs`, `pesalink`, `swift`, and `bill_payment`,
112
- subject to provider availability. RTGS/SWIFT require
113
- `destination_bank_code`; bill payment requires `biller_code`. Jenga also
114
- exposes `forex_rates`.
115
-
116
- <nav class="doc-nav"><a href="{{ '/getting-started/' | relative_url }}">← Getting started</a><a href="{{ '/gateways/' | relative_url }}">Gateways →</a></nav>
data/docs/gateways.md DELETED
@@ -1,158 +0,0 @@
1
- ---
2
- title: Gateways
3
- description: Configure M-Pesa, Co-op Bank, and Jenga HQ.
4
- ---
5
-
6
- <header class="page-title"><p class="eyebrow">Guide 03</p><h1>Gateways</h1><p class="lead">Configure process-wide defaults or isolate each tenant's credentials and HTTP settings in a context.</p></header>
7
-
8
- ## Process-wide defaults
9
-
10
- `Lipwa.configure` sets shared `logger`, `default_timeout`, and Faraday `adapter`
11
- defaults. Gateway-class `.configure` sets provider credentials, environment, and
12
- HTTP overrides, as shown below. These APIs and `Lipwa.gateway(:mpesa)` remain
13
- supported and backward compatible; existing single-tenant applications do not
14
- need to adopt contexts.
15
-
16
- Configure defaults at boot before resolving gateways or constructing contexts.
17
- `Lipwa.gateway(:mpesa)` lazily constructs and memoizes one process-wide gateway.
18
- Its provider and shared settings are captured on the first lookup; later
19
- configuration changes do not update that instance. Direct construction with
20
- `Lipwa::Gateways::Mpesa.new` captures defaults at construction time instead.
21
-
22
- ## M-Pesa
23
-
24
- {% highlight ruby %}Lipwa::Gateways::Mpesa.configure do |c|
25
- c.env = :sandbox
26
- c.consumer_key = ENV.fetch("MPESA_CONSUMER_KEY")
27
- c.consumer_secret = ENV.fetch("MPESA_CONSUMER_SECRET")
28
- c.shortcode = ENV.fetch("MPESA_SHORTCODE")
29
- c.passkey = ENV.fetch("MPESA_PASSKEY")
30
- end{% endhighlight %}
31
-
32
- Lipwa chooses the Daraja host and manages OAuth tokens.
33
-
34
- ## Co-op Bank
35
-
36
- {% highlight ruby %}Lipwa::Gateways::CoopBank.configure do |c|
37
- c.env = :sandbox
38
- c.api_key = ENV.fetch("COOP_API_KEY")
39
- c.api_secret = ENV.fetch("COOP_API_SECRET")
40
- c.base_url = ENV["COOP_BASE_URL"] # optional override
41
- c.token_url = ENV["COOP_TOKEN_URL"] # optional override
42
- end{% endhighlight %}
43
-
44
- `client_id` and `client_secret` are aliases for `api_key` and `api_secret`.
45
-
46
- ## Jenga HQ
47
-
48
- {% highlight ruby %}Lipwa::Gateways::Jenga.configure do |c|
49
- c.env = :sandbox
50
- c.api_key = ENV.fetch("JENGA_API_KEY")
51
- c.merchant_code = ENV.fetch("JENGA_MERCHANT_CODE")
52
- c.consumer_secret = ENV.fetch("JENGA_CONSUMER_SECRET")
53
- c.private_key = File.read(ENV.fetch("JENGA_PRIVATE_KEY_PATH"))
54
- c.source_account = ENV.fetch("JENGA_SOURCE_ACCOUNT")
55
- c.source_name = ENV.fetch("JENGA_SOURCE_NAME")
56
- c.country_code = "KE"
57
- c.partner_id = ENV["JENGA_PARTNER_ID"]
58
- end{% endhighlight %}
59
-
60
- Lipwa obtains bearer tokens and signs provider requests.
61
-
62
- ## Per-gateway HTTP settings
63
-
64
- {% highlight ruby %}Lipwa::Gateways::Jenga.configure do |c|
65
- c.timeout = 20
66
- c.open_timeout = 5
67
- c.logger = MyStructuredLogger.new
68
- end{% endhighlight %}
69
-
70
- Logs recursively redact authorization, credentials, cookies, passwords,
71
- tokens, and secret-like fields.
72
-
73
- ## Contexts
74
-
75
- `Lipwa.context { |config| ... }` (or `Lipwa::Context.new` with the same block)
76
- captures `Lipwa.config` and every currently registered gateway class's settings
77
- before running the builder block. Unspecified settings inherit those captured
78
- defaults. `config.gateway(:mpesa) { |mpesa| ... }` overrides only that context's
79
- M-Pesa settings. Provider `timeout` and `logger` take precedence over shared
80
- `default_timeout` and `logger`; otherwise the shared defaults apply.
81
-
82
- The snapshot happens when the context is constructed, even if its first gateway
83
- lookup is much later. Subsequent global changes and registrations do not alter
84
- an existing context. Overrides do not change process-wide defaults or other
85
- contexts. Typed overrides are validated during construction.
86
-
87
- When the block finishes, configuration is immutable: configuration strings,
88
- arrays, and hashes are copied and frozen. Logger and other service objects retain
89
- their identity rather than being deeply copied or frozen. Gateway runtime state,
90
- such as HTTP clients and OAuth token caches, can still change.
91
-
92
- Each context lazily constructs and memoizes its own gateway instances. Repeated
93
- `tenant_context.gateway(:mpesa)` calls return the same instance, distinct from
94
- another context's gateway and from `Lipwa.gateway(:mpesa)`. Built-in provider HTTP
95
- clients, authentication, and token caches belong to those gateway instances.
96
- Pass contexts explicitly; there is no thread-local current tenant.
97
-
98
- ### Retain one context per tenant
99
-
100
- Construct one service object per tenant and retain it across payment calls. Here
101
- `secrets_for` is an application-supplied callable that loads that tenant's secrets
102
- from your credential store during context construction:
103
-
104
- {% highlight ruby %}class TenantPayments
105
- attr_reader :context
106
-
107
- def initialize(tenant_id:, secrets_for:)
108
- @context = Lipwa.context do |config|
109
- secrets = secrets_for.call(tenant_id)
110
- config.default_timeout = 20
111
- config.gateway(:mpesa) do |mpesa|
112
- mpesa.env = :production
113
- mpesa.consumer_key = secrets.fetch(:consumer_key)
114
- mpesa.consumer_secret = secrets.fetch(:consumer_secret)
115
- mpesa.shortcode = secrets.fetch(:shortcode)
116
- mpesa.passkey = secrets.fetch(:passkey)
117
- end
118
- end
119
- end
120
-
121
- def stk_push(**params)
122
- context.gateway(:mpesa).stk_push(**params)
123
- end
124
- end
125
-
126
- # Retain these services in your application's tenant service registry.
127
- payments_by_tenant = tenant_ids.to_h do |tenant_id|
128
- [tenant_id, TenantPayments.new(tenant_id: tenant_id, secrets_for: secrets_for)]
129
- end
130
-
131
- tenant_context = payments_by_tenant.fetch(tenant_id).context
132
- tenant_context.gateway(:mpesa) # This tenant's memoized gateway
133
- Lipwa.gateway(:mpesa) # Separate process-wide gateway using boot defaults
134
- {% endhighlight %}
135
-
136
- Do not repeatedly mutate `Lipwa::Gateways::Mpesa.configure` per request to switch
137
- tenants: those defaults are shared across requests, and memoized gateways retain
138
- their earlier snapshots. For credential rotation, construct a replacement service
139
- and context with fresh secrets and use it for subsequent calls. Existing contexts
140
- continue using their captured credentials.
141
-
142
- Tenant secrets loaded during construction remain covered by Lipwa's HTTP log
143
- redaction for authorization, credentials, tokens, and secret-like fields. This
144
- does not sanitize arbitrary application logs: do not log raw secret hashes or
145
- inspect configuration objects containing credentials.
146
-
147
- ### Explicit gateway construction and custom authentication
148
-
149
- Explicit `config:` and `global_config:` gateway constructor snapshots replace
150
- their respective defaults and are copied and finalized by the gateway.
151
-
152
- Custom `auth_strategy` objects are duplicated for each gateway. Stateful custom
153
- strategies should implement `initialize_copy` to isolate nested mutable state,
154
- or supply a zero-argument factory that creates a fresh strategy and token source.
155
- Callable token providers and clocks must not close over shared mutable token
156
- caches. Built-in OAuth clients reset their caches when copied.
157
-
158
- <nav class="doc-nav"><a href="{{ '/capabilities/' | relative_url }}">← Capabilities</a><a href="{{ '/webhooks/' | relative_url }}">Webhooks →</a></nav>
@@ -1,64 +0,0 @@
1
- ---
2
- title: Getting started
3
- description: Install Lipwa, configure a gateway, and make your first request.
4
- ---
5
-
6
- <header class="page-title"><p class="eyebrow">Guide 01</p><h1>Getting started</h1><p class="lead">Install the gem, configure one gateway, and handle the result explicitly.</p></header>
7
-
8
- ## Install and configure
9
-
10
- {% highlight bash %}bundle add lipwa{% endhighlight %}
11
-
12
- {% highlight ruby %}require "lipwa"
13
-
14
- Lipwa.configure do |config|
15
- config.logger = Rails.logger
16
- config.default_timeout = 10
17
- end
18
-
19
- Lipwa::Gateways::Mpesa.configure do |config|
20
- config.env = :sandbox
21
- config.consumer_key = ENV.fetch("MPESA_CONSUMER_KEY")
22
- config.consumer_secret = ENV.fetch("MPESA_CONSUMER_SECRET")
23
- config.shortcode = ENV.fetch("MPESA_SHORTCODE")
24
- config.passkey = ENV.fetch("MPESA_PASSKEY")
25
- end{% endhighlight %}
26
-
27
- ## Make a request
28
-
29
- {% highlight ruby %}result = Lipwa.gateway(:mpesa).stk_push(
30
- amount: Lipwa::Money.new(amount: "100.00", currency: "KES"),
31
- phone_number: "254712345678",
32
- account_reference: "ORDER-123",
33
- callback_url: "https://payments.example.com/webhooks/mpesa",
34
- idempotency_key: "stk-order-123"
35
- )
36
-
37
- result.either(
38
- ->(response) { Payment.update!(provider_reference: response.provider_reference) },
39
- ->(error) { logger.error(error.message) }
40
- ){% endhighlight %}
41
-
42
- <div class="callout"><strong>Acceptance is not settlement.</strong> STK Push, disbursement, refund, and status calls are asynchronous. Process the later verified callback for the final result.</div>
43
-
44
- ## Initiator credentials
45
-
46
- Disbursement, status, and refund also require:
47
-
48
- {% highlight ruby %}Lipwa::Gateways::Mpesa.configure do |config|
49
- config.initiator_name = ENV.fetch("MPESA_INITIATOR_NAME")
50
- config.initiator_password = ENV.fetch("MPESA_INITIATOR_PASSWORD")
51
- config.security_credential_cert =
52
- File.read(ENV.fetch("MPESA_CERTIFICATE_PATH"))
53
- end{% endhighlight %}
54
-
55
- Sandbox and production certificates differ. Keep certificate content outside
56
- source control.
57
-
58
- ## Idempotency
59
-
60
- Reuse one stable `idempotency_key` when retrying one logical write. Lipwa
61
- retries safe reads automatically, but writes only when this key is present.
62
- The provider controls key retention and duplicate detection.
63
-
64
- <nav class="doc-nav"><a href="{{ '/' | relative_url }}">← Home</a><a href="{{ '/capabilities/' | relative_url }}">Capabilities →</a></nav>
data/docs/index.md DELETED
@@ -1,46 +0,0 @@
1
- ---
2
- title: Home
3
- description: Unified, capability-based African payment APIs for Ruby.
4
- ---
5
-
6
- <section class="hero"><div>
7
- <p class="eyebrow">Payment flows, honestly modeled</p>
8
- <h1>One Ruby API. Only the capabilities you need.</h1>
9
- <p class="lead">Lipwa unifies mobile money, bank transfers, payouts, and asynchronous callbacks without pretending every provider behaves like a card gateway.</p>
10
- <div class="actions"><a class="button" href="{{ '/getting-started/' | relative_url }}">Get started</a><a class="button secondary" href="{{ '/capabilities/' | relative_url }}">Explore capabilities</a></div>
11
- </div><div class="terminal"><div class="terminal-bar"><span class="terminal-dot"></span><span class="terminal-dot"></span><span class="terminal-dot"></span></div>
12
- {% highlight ruby %}gateway = Lipwa.gateway(:mpesa)
13
-
14
- result = gateway.stk_push(
15
- amount: Lipwa::Money.new(
16
- amount: "100.00",
17
- currency: "KES"
18
- ),
19
- phone_number: "254712345678",
20
- account_reference: "ORDER-123",
21
- callback_url: callback_url,
22
- idempotency_key: "stk-order-123"
23
- )
24
-
25
- result.fmap(&:provider_reference){% endhighlight %}
26
- </div></section>
27
-
28
- ## Designed around real payment flows
29
-
30
- <div class="grid">
31
- <article class="card"><span class="number">01</span><h3>Capability first</h3><p>Gateways expose operations they truly implement. Inspect support with <code>capability?</code> and route deliberately.</p></article>
32
- <article class="card"><span class="number">02</span><h3>Failure explicit</h3><p>Validation and provider failures travel through <code>Dry::Monads::Result</code>.</p></article>
33
- <article class="card"><span class="number">03</span><h3>Async aware</h3><p>Request acknowledgement and final outcome stay distinct. Normalized webhook events complete the flow.</p></article>
34
- </div>
35
-
36
- ## Supported gateways
37
-
38
- | Gateway | What it can do |
39
- | --- | --- |
40
- | M-Pesa | STK Push, C2B, B2C/B2B, status, refunds |
41
- | Co-op Bank | Transfers, balances, and statements |
42
- | Jenga HQ | Bank/mobile transfers, account queries, forex |
43
-
44
- <div class="callout"><strong>Start with a flow, not a provider.</strong> Choose the capability your product needs, then select a gateway that advertises it.</div>
45
-
46
- <nav class="doc-nav"><span></span><a href="{{ '/getting-started/' | relative_url }}">Getting started →</a></nav>
data/docs/reliability.md DELETED
@@ -1,50 +0,0 @@
1
- ---
2
- title: Reliability
3
- description: Results, retries, idempotency, and money safety.
4
- ---
5
-
6
- <header class="page-title"><p class="eyebrow">Guide 05</p><h1>Reliability</h1><p class="lead">Expected failures are explicit, and automatic recovery is limited to operations safe to repeat.</p></header>
7
-
8
- ## Results and errors
9
-
10
- | Type | Delivery | Meaning |
11
- | --- | --- | --- |
12
- | `ValidationError` | `Failure` | Input failed a capability contract |
13
- | `GatewayError` | `Failure` | Network or provider HTTP failure |
14
- | `WebhookParseError` | `Failure` | Invalid inbound JSON |
15
- | `ConfigurationError` | raised | Missing or invalid setup |
16
- | `UnsupportedProviderError` | raised | No webhook parser registered |
17
-
18
- `Lipwa::Response` exposes `success?`, `provider_reference`, `message`,
19
- `code`, and `raw`.
20
-
21
- ## Retry policy
22
-
23
- Lipwa retries transient network exceptions and HTTP 429, 500, 502, 503, and
24
- 504 at most twice. Backoff begins at 0.5 seconds, doubles with up to 50%
25
- jitter, and caps waits at 5 seconds. Server `Retry-After` and
26
- `RateLimit-Reset` values are honored within that cap.
27
-
28
- Safe reads retry automatically. Writes retry only with `Idempotency-Key`.
29
-
30
- ## Decimal money
31
-
32
- {% highlight ruby %}subtotal = Lipwa::Money.new(amount: "199.95", currency: "KES")
33
- fee = Lipwa::Money.new(amount: "5.05", currency: "KES")
34
-
35
- total = subtotal + fee
36
- total.to_s # => "205.0 KES"
37
- total > subtotal # => true{% endhighlight %}
38
-
39
- Amounts and results are non-negative. Addition, subtraction, and comparison
40
- across currencies raise `ArgumentError`. Currency conversion is never hidden.
41
-
42
- ## Async reconciliation
43
-
44
- 1. Persist a pending operation and idempotency key before calling the API.
45
- 2. Store the synchronous provider reference.
46
- 3. Verify and process callbacks idempotently.
47
- 4. Use status query where supported to reconcile missing callbacks.
48
- 5. Treat acknowledgements and callbacks as repeatable messages.
49
-
50
- <nav class="doc-nav"><a href="{{ '/webhooks/' | relative_url }}">← Webhooks</a><a href="{{ '/' | relative_url }}">Home →</a></nav>
data/docs/webhooks.md DELETED
@@ -1,72 +0,0 @@
1
- ---
2
- title: Webhooks
3
- description: Parse and verify M-Pesa, Jenga, and LNbits callbacks.
4
- ---
5
-
6
- <header class="page-title"><p class="eyebrow">Guide 04</p><h1>Webhooks</h1><p class="lead">Normalize provider payloads, then perform the provider-specific trust check before changing financial state.</p></header>
7
-
8
- ## Parse
9
-
10
- {% highlight ruby %}result = Lipwa::Webhook.parse_webhook(
11
- provider: :mpesa,
12
- body: request.body.read,
13
- headers: request.headers.to_h
14
- )
15
-
16
- result.either(
17
- ->(event) { process_event(event) },
18
- ->(error) { head :bad_request }
19
- ){% endhighlight %}
20
-
21
- `WebhookEvent` exposes `provider`, `event_type`, `success?`,
22
- `provider_reference`, `message`, `raw`, and `verify_signature`.
23
-
24
- ## Verify M-Pesa
25
-
26
- Daraja does not cryptographically sign callbacks. Lipwa checks the remote
27
- address against Safaricom's published callback IP list:
28
-
29
- {% highlight ruby %}return head :forbidden unless event.verify_signature(
30
- source_ip: request.remote_ip
31
- ){% endhighlight %}
32
-
33
- Configure trusted proxies carefully so clients cannot spoof forwarded IPs.
34
- Event types are `stk_callback`, `c2b`, `transaction_status`,
35
- `disbursement`, and `refund`.
36
-
37
- ## Verify Jenga
38
-
39
- Jenga receive-payment IPNs normalize to `receive_payment`:
40
-
41
- {% highlight ruby %}return head :forbidden unless event.verify_signature(
42
- username: ENV.fetch("JENGA_WEBHOOK_USERNAME"),
43
- password: ENV.fetch("JENGA_WEBHOOK_PASSWORD")
44
- ){% endhighlight %}
45
-
46
- ## Verify LNbits
47
-
48
- LNbits does not HMAC-sign webhooks by default. Give each invoice a strong,
49
- random token in its webhook URL, persist that token alongside the payment hash,
50
- and compare the URL parameter when the callback arrives:
51
-
52
- {% highlight ruby %}return head :forbidden unless event.verify_signature(
53
- expected_token: invoice.webhook_token,
54
- provided_token: params[:token]
55
- ){% endhighlight %}
56
-
57
- This is a shared-secret URL check, not proof that the webhook body came from
58
- LNbits. Anyone who learns the token can forge a payload. Always call
59
- `check_invoice(payment_hash: event.provider_reference)` and confirm its status
60
- before crediting funds; use the webhook only as a low-latency trigger.
61
-
62
- ## Processing checklist
63
-
64
- 1. Retain the raw body and parse it through Lipwa.
65
- 2. Verify using provider-specific options.
66
- 3. Correlate by `provider_reference`.
67
- 4. Apply the transition idempotently inside a transaction.
68
- 5. Return quickly and defer slow work to a job.
69
-
70
- <div class="callout"><strong>Never credit from an unverified callback.</strong> Parsing normalizes data; verification determines trust.</div>
71
-
72
- <nav class="doc-nav"><a href="{{ '/gateways/' | relative_url }}">← Gateways</a><a href="{{ '/reliability/' | relative_url }}">Reliability →</a></nav>
data/sig/lipwa.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Lipwa
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end