lunixi-sdk 0.1.2 → 1.0.1
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/CHANGELOG.md +15 -0
- data/MIGRATION-1.0.md +19 -0
- data/README.md +20 -18
- data/README.tr.md +19 -12
- data/docs/PUBLISHING.md +17 -3
- data/docs/ruby.en.md +11 -12
- data/docs/ruby.tr.md +11 -12
- data/lib/lunixi/client.rb +18 -8
- data/lib/lunixi/money.rb +77 -0
- data/lib/lunixi/payments.rb +12 -14
- data/lib/lunixi/version.rb +1 -1
- data/lib/lunixi.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40366427b51fa84757c9c338364d43e025105e683a04790919a4707d300418ec
|
|
4
|
+
data.tar.gz: 703ff10b22fdd261e095f129ac5c9c7e25b643843cf2cbeabc3b88075ac5b79f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcfe1c791b4270392c9ce4f7c13d0592c9f2923ede5ad9be524ac7df8e487d3dd69d4468c17de7e33c11a7d8e243618da9a8e5c74dfb046b824140dda3991985
|
|
7
|
+
data.tar.gz: a10f6eaadfeae29b023b67fd5293d3d65979ed35019a20ef467c794a481dc6e5d850fd772b2a30788580367b78007a78c73d2c2be9f87212c9e92684706cb8ae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.1 — 2026-11-01
|
|
4
|
+
|
|
5
|
+
- Align Rails sample API mocks and expectations with canonical minor-unit strings while preserving numeric-input rejection tests; runtime admission is unchanged.
|
|
6
|
+
- Preserve response parsing limits and error handling in a private helper; close RuboCop findings without suppressions.
|
|
7
|
+
- Update 1.0.1 installation and exact-tag publishing guidance.
|
|
8
|
+
|
|
9
|
+
## 1.0.0 — 2026-11-01
|
|
10
|
+
|
|
11
|
+
- Reject explicit null capture/refund amounts; full remaining operations use omission or the dedicated API described in MIGRATION-1.0.md.
|
|
12
|
+
|
|
13
|
+
- Breaking: public money request/response fields require canonical minor-unit strings; JSON numbers are rejected.
|
|
14
|
+
- Exact signed 64-bit fiat arithmetic and currency-aware decimal text conversion preserve values above JavaScript’s safe-integer limit.
|
|
15
|
+
- See [MIGRATION-1.0.md](MIGRATION-1.0.md) before upgrading.
|
|
16
|
+
|
|
17
|
+
|
|
3
18
|
## 0.1.2 — 2026-10-31
|
|
4
19
|
|
|
5
20
|
- Add strict backend payment-page URL and official embedded-form configuration helpers, without rebuilding hosted URLs from tokens.
|
data/MIGRATION-1.0.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 1.0.0 migration
|
|
2
|
+
|
|
3
|
+
All public monetary request and response fields now use canonical base-10 **strings in minor units**. For example, `amount: "1990"` represents TRY 19.90. This includes basket `price`, split and note amounts, capture/refund, balances and fixed fees on supported endpoints. Numeric JSON money is rejected; convert trusted native exact integers explicitly at your application boundary. Rates, basis points, quantities, counts and timestamps remain numeric.
|
|
4
|
+
|
|
5
|
+
Use the package’s decimal-text conversion helper with the actual currency. JPY has exponent 0, TRY/USD 2, KWD 3. Do not parse decimal text through float/double or assume a factor of 100. `"9007199254740993"` remains exact. Fiat amounts must fit signed 64-bit storage (maximum `"9223372036854775807"`); provider/platform limits may be lower. Crypto is a separate domain.
|
|
6
|
+
|
|
7
|
+
Canonical strings reject whitespace, signs on nonnegative values, leading zeros, decimals, exponents, `NaN`, `Infinity`, and negative zero. Payments are positive. Settlement totals may be zero; balances may be signed. Only omission retains remaining-amount semantics. Explicit JSON `null` and `"0"` are invalid. Split-refund amount remains required where the existing API requires it.
|
|
8
|
+
|
|
9
|
+
Update call sites, persisted outbound request schemas and gateway response fixtures together. Preserve an existing attempt’s exact body, idempotency key, environment and credential profile; reconcile historical attempts before creating new ones. The SDK does not silently rewrite historical payment bytes or accept both numeric and string wire money. Native internal exact integer storage may remain unchanged.
|
|
10
|
+
|
|
11
|
+
Raw webhook verification, signatures, authentication and replay/idempotency protections are unchanged. Upgrade the gateway and integrations as one coordinated breaking cut. Install version 1.0.0 after it is available in this package’s distribution channel.
|
|
12
|
+
|
|
13
|
+
Use `payments.create_intent({ amount: '1990', ... }, idempotency_key: key)` and `Lunixi::Money.decimal_to_minor('19.90', 'TRY')`. Capture/refund accept strings or omitted keywords; numeric money raises `ConfigurationError`.
|
|
14
|
+
|
|
15
|
+
## Optional capture/refund amounts
|
|
16
|
+
|
|
17
|
+
Omit the `amount:` keyword for the remaining balance: `payments.capture(id, idempotency_key: key)` and `payments.refund(id, idempotency_key: key, reason: reason)`. Explicit `amount: nil` is rejected. The omission marker is private and never serialized.
|
|
18
|
+
|
|
19
|
+
The advanced/raw request boundary also rejects a present `amount: null` on capture/refund routes. Unrelated nullable response fields and merchant metadata keep their existing meaning.
|
data/README.md
CHANGED
|
@@ -2,24 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
# Lunixi Ruby SDK
|
|
4
4
|
|
|
5
|
+
**Version 1.0.1 uses canonical minor-unit strings for money.** This is a breaking change from 0.x; update request and response handling together. See [migration](MIGRATION-1.0.md).
|
|
6
|
+
|
|
5
7
|
[English](README.md) · [Türkçe](README.tr.md) · [Documentation](https://docs.lunixi.com/sdks/ruby) · [Rails sample](samples/rails)
|
|
6
8
|
|
|
7
|
-
Server-side Ruby client for Lunixi payments, fraud and signed gateway requests. It signs requests with OpenSSL Ed25519, caches tokens across threads, preserves
|
|
9
|
+
Server-side Ruby client for Lunixi payments, fraud and signed gateway requests. It signs requests with OpenSSL Ed25519, caches tokens across threads, preserves canonical minor-unit strings, retries safe operations with the same idempotency key, and verifies raw-body v2 webhooks. Private keys and card details stay out of the browser and application logs.
|
|
8
10
|
|
|
9
11
|
## Requirements and installation
|
|
10
12
|
|
|
11
13
|
Ruby **3.3+**, OpenSSL with **Ed25519** support, and Bundler. The runnable Rails sample uses **Rails 8.1** and SQLite. The core gem does not depend on Rails.
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
After version 1.0.1 is available on RubyGems, add the dependency and run `bundle install`:
|
|
14
16
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
bundle install
|
|
19
|
-
gem build lunixi-sdk.gemspec
|
|
20
|
-
gem install ./lunixi-sdk-0.1.2.gem
|
|
17
|
+
```ruby
|
|
18
|
+
source 'https://rubygems.org'
|
|
19
|
+
gem 'lunixi-sdk', '1.0.1'
|
|
21
20
|
```
|
|
22
21
|
|
|
22
|
+
Keep `Gemfile.lock` to retain the reviewed dependency resolution.
|
|
23
|
+
|
|
23
24
|
Inside this checkout, `bundle exec ruby …` loads the local gem. An application can use a reviewed checkout with `gem 'lunixi-sdk', path: '/absolute/path/to/lunixi-ruby'`. Pin an audited commit when using a Bundler Git source. Never use an unpinned moving branch for a production deployment.
|
|
24
25
|
|
|
25
26
|
## Create a client
|
|
@@ -55,11 +56,11 @@ Calculate the total on the server and persist a UUID idempotency key with the pa
|
|
|
55
56
|
```ruby
|
|
56
57
|
result = lunixi.payments.create_intent(
|
|
57
58
|
{
|
|
58
|
-
amount:
|
|
59
|
+
amount: "10050", # 100.50 TRY; integer kuruş, never 100.50
|
|
59
60
|
currency: 'TRY',
|
|
60
61
|
orderId: order.public_id,
|
|
61
62
|
callbackUrl: 'https://merchant.example/payments/return',
|
|
62
|
-
basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price:
|
|
63
|
+
basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price: "10050" }]
|
|
63
64
|
},
|
|
64
65
|
idempotency_key: order.persisted_payment_attempt_key
|
|
65
66
|
)
|
|
@@ -67,7 +68,6 @@ result = lunixi.payments.create_intent(
|
|
|
67
68
|
intent_id = result.fetch('data').fetch('paymentId')
|
|
68
69
|
checkout_token = result.fetch('data').fetch('token')
|
|
69
70
|
# Persist the intent id and encrypt the checkout token at rest.
|
|
70
|
-
# The Rails sample validates the token and builds a fixed-origin redirect:
|
|
71
71
|
# Optional hosted checkout uses the validated backend paymentPageUrl, never a URL built from this token.
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -82,8 +82,8 @@ The shared Lunixi form owns enabled payment methods and supports the methods con
|
|
|
82
82
|
| Create intent | `payments.create_intent(body, idempotency_key:)` |
|
|
83
83
|
| Get/list payment | `payments.get(id)`, `payments.list(**filters)` |
|
|
84
84
|
| Enabled methods | `payments.methods(payment_method: nil)` |
|
|
85
|
-
| Capture/preauthorization | `payments.capture(id, idempotency_key
|
|
86
|
-
| Full/partial refund | `payments.refund(id, idempotency_key:,
|
|
85
|
+
| Capture/preauthorization | `payments.capture(id, idempotency_key:)` |
|
|
86
|
+
| Full/partial refund | `payments.refund(id, idempotency_key:, reason: nil)` |
|
|
87
87
|
| Void authorization | `payments.void(id, idempotency_key:)` |
|
|
88
88
|
| Split payment (1–5 legs) | `payments.split(body, idempotency_key:)` |
|
|
89
89
|
| Split status/refund | `payments.get_split(id)`, `payments.refund_split(id, amount:, idempotency_key:, reason: nil)` |
|
|
@@ -91,17 +91,17 @@ The shared Lunixi form owns enabled payment methods and supports the methods con
|
|
|
91
91
|
| Fraud transaction event | `fraud.record_event(body, idempotency_key:)` |
|
|
92
92
|
| Other documented endpoints | `request(method, path, body: nil, query: {}, idempotency_key: nil, signed: true, binary: false)` |
|
|
93
93
|
|
|
94
|
-
`amount`, split `totalAmount` and each leg/item price must be positive
|
|
94
|
+
`amount`, split `totalAmount` and each leg/item price must be positive canonical minor-unit strings within the signed 64-bit fiat range. Root `price`/`paidPrice` are removed fields and rejected even when null. Basket and split totals must agree. Intent installments belong to the checkout form; `installment` is rejected on intent creation. Set `preauth: true` for authorization, then capture separately with a new durable operation key. Omitting the capture/refund `amount:` keyword asks the gateway to use the remaining amount; explicit nil, numeric, zero, false and noncanonical string amounts are rejected.
|
|
95
95
|
|
|
96
96
|
```ruby
|
|
97
|
-
lunixi.payments.refund('intent_123', amount:
|
|
97
|
+
lunixi.payments.refund('intent_123', amount: "2500",
|
|
98
98
|
reason: 'Customer request', idempotency_key: refund.persisted_operation_key)
|
|
99
99
|
|
|
100
100
|
# Advanced transport: a documented existing endpoint, not a second HTTP stack.
|
|
101
101
|
links = lunixi.request('GET', '/api/v1/payments/links', query: { limit: 20 })
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Payment, fraud and webhook helpers are explicit. Other products use the signed transport with the published gateway schema; version 0.1
|
|
104
|
+
Payment, fraud and webhook helpers are explicit. Other products use the signed transport with the published gateway schema; version 1.0.1 does not claim full typed service-client parity with the PHP SDK. JSON is compact UTF-8 with JavaScript integer-index key ordering and compatible numeric formatting, so the signed bytes are exactly the transmitted bytes.
|
|
105
105
|
|
|
106
106
|
## Raw-body webhooks and durable deduplication
|
|
107
107
|
|
|
@@ -154,8 +154,10 @@ The Rails example defaults to `LUNIXI_CHECKOUT_MODE=embedded`. It renders the ex
|
|
|
154
154
|
|
|
155
155
|
Set `LUNIXI_CHECKOUT_MODE=hosted` explicitly for optional redirect. `Lunixi::Checkout.payment_page_url(data.fetch('paymentPageUrl'))` and the Rails redirect wrapper validate the exact backend URL: HTTPS `pay.lunixi.com/payment/` followed by a 16–4096-character ASCII opaque value, with no userinfo, port, query, fragment or controls. It can differ from the embedded token. Do not construct a URL from that token. Both modes expose the same merchant-enabled form methods, multi-card, installments and insurance; these features do not require redirecting. The existing fixed-total single-intent settlement limitation still applies.
|
|
156
156
|
|
|
157
|
-
Run `rails db:migrate` before deploying 0.1.
|
|
157
|
+
Run `rails db:migrate` before deploying 1.0.1. New request/mode/callback and start time are persisted before network I/O; request/token/backend URL use Active Record Encryption. Retries reuse the saved body and key and stop financial dispatch after 23 hours. Existing rows are marked legacy hosted; unknown legacy requests or missing backend URLs remain reconciliation-only, without rewriting origin, resetting a key or inventing a URL. Global mode changes affect new reservations; existing requests retain their saved presentation. CSRF/demo authorization, raw webhooks, native receipt transactions and the bounded 3DS/split return adapter remain unchanged.
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
Historical focused 0.1.2 verification: 24 Rails form/return/URL/CSP security tests with 225 assertions, 2 SDK helper tests with 5 assertions, RuboCop and Brakeman (one exact documented strict-URL redirect false positive). Prior full 0.1.1 results remain the baseline; live/merchant acceptance is deferred.
|
|
160
160
|
|
|
161
161
|
The checkout page’s CSP permits form POSTs only to `'self'` and the configured gateway origin (default `https://api-gateway.lunixi.com`). This preserves wallet/provider and legacy POST handoffs supported by the browser SDK. For another reviewed provider, set server-only `LUNIXI_FORM_ACTION_ORIGINS` to a comma-separated list of exact HTTPS origins, without spaces, paths, trailing slashes, credentials, query, fragment or wildcards. Invalid configuration fails closed; gateway responses and browser fields never expand this list. Script nonces, frame/connect restrictions and informational return handling remain in force.
|
|
162
|
+
|
|
163
|
+
Before upgrading from 0.x, follow [the money-string migration](MIGRATION-1.0.md).
|
data/README.tr.md
CHANGED
|
@@ -4,20 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.md) · [Türkçe](README.tr.md) · [Dokümantasyon](https://docs.lunixi.com/tr/sdks/ruby) · [Rails örneği](samples/rails)
|
|
6
6
|
|
|
7
|
-
Lunixi ödemeleri, fraud işlemleri ve imzalı gateway çağrıları için sunucu tarafı Ruby SDK’sı. Ed25519 imzasını OpenSSL ile üretir; token’ı eşzamanlı istekler arasında önbelleğe alır, tutarları
|
|
7
|
+
Lunixi ödemeleri, fraud işlemleri ve imzalı gateway çağrıları için sunucu tarafı Ruby SDK’sı. Ed25519 imzasını OpenSSL ile üretir; token’ı eşzamanlı istekler arasında önbelleğe alır, tutarları kanonik alt birim string olarak korur ve güvenli tekrar denemelerde aynı idempotency anahtarını kullanır. v2 webhook doğrulaması özgün istek baytları üzerinden yapılır.
|
|
8
8
|
|
|
9
9
|
## Gereksinimler ve kurulum
|
|
10
10
|
|
|
11
11
|
Ruby **3.3+**, **Ed25519** destekli OpenSSL ve Bundler gerekir. Çalıştırılabilir örnek **Rails 8.1** ve SQLite kullanır; ana gem Rails’e bağımlı değildir.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
1.0.1 [RubyGems](https://rubygems.org/gems/lunixi-sdk) üzerinde kullanılabilir olduğunda `Gemfile` dosyanıza tam sürümü ekleyip `bundle install` çalıştırın. [Para sözleşmesi geçişini](MIGRATION-1.0.md) uygulayın:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
source 'https://rubygems.org'
|
|
17
|
+
gem 'lunixi-sdk', '1.0.1'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
İncelenen bağımlılık çözümünü korumak için `Gemfile.lock` dosyasını saklayın. Kaynak koddan kurulum alternatifi:
|
|
14
21
|
|
|
15
22
|
```bash
|
|
16
|
-
git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
|
|
23
|
+
git clone --branch v1.0.1 --depth 1 https://github.com/Lunixi-Payment/lunixi-ruby.git
|
|
17
24
|
cd lunixi-ruby
|
|
18
25
|
bundle install
|
|
19
26
|
gem build lunixi-sdk.gemspec
|
|
20
|
-
gem install ./lunixi-sdk-0.1.
|
|
27
|
+
gem install ./lunixi-sdk-1.0.1.gem
|
|
21
28
|
```
|
|
22
29
|
|
|
23
30
|
Bu dizinde `bundle exec ruby …` yerel SDK’yı yükler. Uygulamanızın Gemfile’ında incelenmiş bir kopyayı `gem 'lunixi-sdk', path: '/tam/yol/lunixi-ruby'` ile kullanabilirsiniz. Bundler Git kaynağı kullanırken denetlenmiş commit’i sabitleyin.
|
|
@@ -49,16 +56,16 @@ Toplamı sunucuda hesaplayın. Çağrıdan **önce** ödeme denemesine bir UUID
|
|
|
49
56
|
|
|
50
57
|
```ruby
|
|
51
58
|
result = lunixi.payments.create_intent(
|
|
52
|
-
{ amount:
|
|
59
|
+
{ amount: "10050", currency: 'TRY', orderId: order.public_id,
|
|
53
60
|
callbackUrl: 'https://merchant.example/payments/return',
|
|
54
|
-
basketItems: [{ id: 'urun-1', name: 'Product', itemType: 'PHYSICAL', price:
|
|
61
|
+
basketItems: [{ id: 'urun-1', name: 'Product', itemType: 'PHYSICAL', price: "10050" }] },
|
|
55
62
|
idempotency_key: order.persisted_payment_attempt_key
|
|
56
63
|
)
|
|
57
64
|
intent_id = result.fetch('data').fetch('paymentId')
|
|
58
65
|
checkout_token = result.fetch('data').fetch('token')
|
|
59
66
|
```
|
|
60
67
|
|
|
61
|
-
`
|
|
68
|
+
`"10050"`, 100,50 TRY’nin kuruş karşılığıdır. Tutarlar pozitif, kanonik alt birim string olmalı ve işaretli 64 bit fiat aralığında kalmalıdır; sayısal değerler kabul edilmez. Eski kök `price`/`paidPrice` alanları null olsa bile reddedilir. Sepet toplamı ve split bacak toplamı ana tutarla aynı olmalıdır. Intent’te `installment` gönderilmez; taksiti ödeme formu seçtirir. `preauth: true` ile provizyon alınabilir; capture ayrı işlem ve anahtarla yapılır.
|
|
62
69
|
|
|
63
70
|
Varsayılan form mağaza içinde açılır; isteğe bağlı yönlendirme yalnız doğrulanmış backend `paymentPageUrl` adresini kullanır. Token ve adres şifrelenir; iki mod aynı Lunixi form ayarlarını paylaşır. PAN/CVV toplamayın.
|
|
64
71
|
|
|
@@ -71,15 +78,15 @@ HTTP 2xx, token, dönüş sayfası veya devam eden split durumu tahsilat kanıt
|
|
|
71
78
|
| Intent oluşturma | `payments.create_intent(body, idempotency_key:)` |
|
|
72
79
|
| Ödeme okuma/listeleme | `payments.get(id)`, `payments.list(**filters)` |
|
|
73
80
|
| Açık yöntemler | `payments.methods(payment_method: nil)` |
|
|
74
|
-
| Capture | `payments.capture(id, idempotency_key
|
|
75
|
-
| Tam/kısmi iade | `payments.refund(id, idempotency_key:,
|
|
81
|
+
| Capture | `payments.capture(id, idempotency_key:)` |
|
|
82
|
+
| Tam/kısmi iade | `payments.refund(id, idempotency_key:, reason: nil)` |
|
|
76
83
|
| Provizyon iptali | `payments.void(id, idempotency_key:)` |
|
|
77
84
|
| Çoklu ödeme | `payments.split(body, idempotency_key:)` |
|
|
78
85
|
| Split durum/iade | `payments.get_split(id)`, `payments.refund_split(id, amount:, idempotency_key:, reason: nil)` |
|
|
79
86
|
| Fraud | `fraud.evaluate(body)`, `fraud.record_event(body, idempotency_key:)` |
|
|
80
87
|
| Diğer dokümante uçlar | `request(method, path, body: nil, query: {}, idempotency_key: nil, signed: true, binary: false)` |
|
|
81
88
|
|
|
82
|
-
Capture/iade için `amount
|
|
89
|
+
Capture/iade için `amount:` anahtarını göndermemek, gateway’in kalan tutarı kullanmasını ister; açık `amount: nil` reddedilir. `"0"`, `false`, sayısal değerler ve kanonik olmayan string tutar reddedilir. Her capture/iade/iptal için ayrı ve kalıcı işlem anahtarı gerekir. Yanıt zarfı korunur. Payment, fraud ve webhook yardımcıları açıktır; diğer servisler dokümante şema ile imzalı transport üzerinden çağrılır. Bu sürüm PHP SDK’sındaki tüm servisler için ayrı tipli istemci sunduğunu iddia etmez.
|
|
83
90
|
|
|
84
91
|
## Webhook güvenliği
|
|
85
92
|
|
|
@@ -127,8 +134,8 @@ Rails örneğinde varsayılan `LUNIXI_CHECKOUT_MODE=embedded` değeridir. Mevcut
|
|
|
127
134
|
|
|
128
135
|
İsteğe bağlı yönlendirme için açıkça `LUNIXI_CHECKOUT_MODE=hosted` seçin. `Lunixi::Checkout.payment_page_url(data.fetch('paymentPageUrl'))` backend adresini birebir doğrular: HTTPS `pay.lunixi.com/payment/` ve 16–4096 karakter ASCII opak değer; userinfo, port, sorgu, fragment veya kontrol karakteri kabul edilmez. Bu değer gömülü token'dan farklı olabilir; token'dan URL üretilmez. İki mod aynı açık yöntemleri, çoklu kartı, taksiti ve sigortayı sunar; yönlendirme şart değildir. Sabit toplamlı tek-intent tahsilat sınırlaması korunur.
|
|
129
136
|
|
|
130
|
-
|
|
137
|
+
Rails örneğini yükseltmeden önce `rails db:migrate` çalıştırın. Yeni istek/mod/dönüş adresi/zaman ağdan önce kaydedilir; gövde, token ve backend adresi Active Record Encryption ile şifrelenir. Tekrar aynı gövde/anahtarla gider; 23 saatten eski finansal gönderim durur. Mevcut satırlar eski hosted akışı olarak işaretlenir; bilinmeyen eski istek veya eksik backend adresi origin/anahtar değiştirilmeden ve adres üretilmeden mutabakat bekler. Genel mod değişikliği yeni rezervasyonlara uygulanır; mevcut istek kayıtlı modunu korur. CSRF/demo yetkisi, ham webhook, atomik receipt ve sınırlı 3DS/çoklu kart dönüşü korunur.
|
|
131
138
|
|
|
132
|
-
0.1.2 odaklı kontrolü: 24 Rails form/dönüş/URL/CSP güvenlik testi ve 225 assertion, 2 SDK yardımcı testi ve 5 assertion, RuboCop ve Brakeman (belgeli tek strict-URL yönlendirme false positive'i). Önceki tam 0.1.1 sonuçları temel kanıttır; canlı/merchant kabulü ertelenmiştir.
|
|
139
|
+
Geçmiş 0.1.2 odaklı kontrolü: 24 Rails form/dönüş/URL/CSP güvenlik testi ve 225 assertion, 2 SDK yardımcı testi ve 5 assertion, RuboCop ve Brakeman (belgeli tek strict-URL yönlendirme false positive'i). Önceki tam 0.1.1 sonuçları temel kanıttır; canlı/merchant kabulü ertelenmiştir.
|
|
133
140
|
|
|
134
141
|
Checkout sayfasının CSP’si form POST’u için yalnız `'self'` ve yapılandırılan gateway origin’ine (varsayılan `https://api-gateway.lunixi.com`) izin verir. Böylece tarayıcı SDK’sının desteklediği cüzdan/sağlayıcı ve eski POST geçişleri korunur. İncelenmiş başka bir sağlayıcı gerekiyorsa yalnız sunucuda `LUNIXI_FORM_ACTION_ORIGINS` tanımlayın: virgülle ayrılmış tam HTTPS origin’leri; boşluk, yol, sondaki slash, kullanıcı bilgisi, sorgu, fragment ve wildcard kabul edilmez. Geçersiz ayar reddedilir; gateway yanıtı veya tarayıcı alanı listeyi genişletemez. Script nonce, frame/connect sınırları ve bilgilendirici dönüş güvenliği korunur.
|
data/docs/PUBLISHING.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# RubyGems publication
|
|
2
2
|
|
|
3
|
-
`publish-rubygems.yml` is a manual
|
|
3
|
+
`publish-rubygems.yml` is a manual workflow dispatched from the exact release tag. For 1.0.1, select `v1.0.1` and enter `1.0.1`. It resolves the tag to the invocation SHA, requires successful public push CI for that same commit, then checks out the immutable source. Gem identity/version and SHA-256 are verified before publishing. No permanent API key is stored in the repository.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use the existing account-owned trusted publisher for `lunixi-sdk`. Its authorized scope is listed below; registry publication does not require creating a second publisher.
|
|
6
6
|
|
|
7
7
|
| Setting | Value |
|
|
8
8
|
| --- | --- |
|
|
@@ -12,9 +12,23 @@ For the first `lunixi-sdk` release, sign in with the intended confirmed RubyGems
|
|
|
12
12
|
| Environment | `release` |
|
|
13
13
|
| Reusable workflow fields | Blank |
|
|
14
14
|
|
|
15
|
-
The account
|
|
15
|
+
The approved initial account is asserted by immutable RubyGems **owner ID `255358`**. The exchange sends `owner` as the JSON string `"255358"`; RubyGems rejects a JSON number and resolves all-digit strings by immutable ID rather than by a reusable handle.
|
|
16
|
+
|
|
17
|
+
The released `configure-rubygems-credentials` **v2.1.0** does not expose an owner input: its [pinned action definition](https://github.com/rubygems/configure-rubygems-credentials/blob/dc5a8d8553e6ee01fc26761a49e99e733d17954a/action.yml) and [exchange implementation](https://github.com/rubygems/configure-rubygems-credentials/blob/dc5a8d8553e6ee01fc26761a49e99e733d17954a/src/oidc/trustedPublisher.ts) send only the JWT. Until an owner-capable release is reviewed, the workflow embeds a Python-standard-library exchange before its push step. It requests a GitHub ID token for audience `rubygems.org`, then POSTs only `{jwt, owner}` to the fixed HTTPS endpoint `https://rubygems.org/api/v1/oidc/trusted_publisher/exchange_token`.
|
|
18
|
+
|
|
19
|
+
The contract was checked against the [RubyGems controller at commit `5a641a1e1ac8bd37ba0b11d56b5d7ee379107f85`](https://github.com/rubygems/rubygems.org/blob/5a641a1e1ac8bd37ba0b11d56b5d7ee379107f85/app/controllers/api/v1/oidc/trusted_publisher_controller.rb) and its [owner-binding regression tests](https://github.com/rubygems/rubygems.org/blob/5a641a1e1ac8bd37ba0b11d56b5d7ee379107f85/test/integration/api/v1/oidc/trusted_publisher_controller_test.rb). The server verifies the signed OIDC claims, atomically binds the asserted owner, rejects replay of the same token, and issues a **push-only, 15-minute** credential with HTTP **201**. The workflow rejects redirects, other HTTP statuses, malformed/oversized responses, additional scopes and expiry beyond 16 minutes. It masks request/JWT/API credentials before use, exports only `GEM_HOST_API_KEY` through a mode-600 runner environment file, and never logs HTTP bodies or underlying exception details. It creates no permanent API key and does not alter the publisher's permissions.
|
|
20
|
+
|
|
21
|
+
The account owner must review and authorize this package-specific publishing delegation. The workflow alone does not create the publisher or publish a gem. For an authorized release, run it from the matching `v1.0.1` tag with version `1.0.1`; it refuses a version that already exists. Verify the resulting version and downloaded gem digest from RubyGems.
|
|
16
22
|
|
|
17
23
|
The build does not repeat the functional suite. Ephemeral runner credentials are removed after the upload.
|
|
18
24
|
|
|
25
|
+
## Verified 0.1.2 publication
|
|
26
|
+
|
|
27
|
+
The account owner authorized the exact publisher above. [Publish run 41249047391](https://github.com/Lunixi-Payment/lunixi-ruby/actions/runs/41249047391) completed successfully from workflow commit `7a4a0e7b6f647490f7f09b650847c38f0b7d3b19`, while the package checkout remained pinned to `579cdf8afeaad0cdba2764c312005bc60480227b`. The original `v0.1.2` tag is unchanged.
|
|
28
|
+
|
|
29
|
+
[RubyGems 0.1.2](https://rubygems.org/gems/lunixi-sdk/versions/0.1.2) is available with SHA-256 `c9c6386c03a3b853d56335c2f21e91f18d3450a83906d5ad15d9fc07ac1ce16f`. That historical release remains immutable. The current workflow also refuses an already published version.
|
|
30
|
+
|
|
31
|
+
The downloaded 39,424-byte archive matches the registry checksum. All 23 packaged files match both the prepared gem and the immutable release source; build metadata can differ between independently built archives.
|
|
32
|
+
|
|
19
33
|
- [Trusted Publishing and pending publishers](https://guides.rubygems.org/trusted-publishing/)
|
|
20
34
|
- [RubyGems publishing](https://guides.rubygems.org/publishing/)
|
data/docs/ruby.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Ruby SDK
|
|
3
|
-
description: Lunixi Ruby SDK —
|
|
3
|
+
description: Lunixi Ruby SDK — installation, Ed25519 signing, payments, fraud, durable idempotency and raw-body v2 webhooks.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
|
|
@@ -8,20 +8,19 @@ description: Lunixi Ruby SDK — source installation, Ed25519 signing, payments,
|
|
|
8
8
|
|
|
9
9
|
`lunixi-sdk` is a server-side Ruby client for Lunixi. It handles OpenSSL Ed25519 request signing, synchronized token caching, compact JavaScript-compatible JSON, safe retries and raw-body webhook verification. It works with Rails, Sinatra, Rack and plain Ruby. The core gem has no framework dependency; the executable [Rails integration](/sdks/rails) is included in the source repository.
|
|
10
10
|
|
|
11
|
-
## Requirements and
|
|
11
|
+
## Requirements and installation
|
|
12
12
|
|
|
13
13
|
Ruby **3.3+**, Bundler and an OpenSSL build supporting **Ed25519** are required. The Rails example uses Rails 8.1.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
Version 0.1.1 is distributed from source. RubyGems publication is a separate release step; a registry install is not assumed here.
|
|
15
|
+
After 1.0.1 is available on RubyGems, add `gem 'lunixi-sdk', '1.0.1'` to your Gemfile and run `bundle install`. Follow the [money-string migration](../MIGRATION-1.0.md). The following source build is an optional development workflow:
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
```bash
|
|
20
|
-
git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
|
|
19
|
+
git clone --branch v1.0.1 --depth 1 https://github.com/Lunixi-Payment/lunixi-ruby.git
|
|
21
20
|
cd lunixi-ruby
|
|
22
21
|
bundle install
|
|
23
22
|
gem build lunixi-sdk.gemspec
|
|
24
|
-
gem install ./lunixi-sdk-0.1.
|
|
23
|
+
gem install ./lunixi-sdk-1.0.1.gem
|
|
25
24
|
```
|
|
26
25
|
|
|
27
26
|
Source: [Lunixi-Payment/lunixi-ruby](https://github.com/Lunixi-Payment/lunixi-ruby). An application may also use a reviewed local checkout with `gem 'lunixi-sdk', path: '/absolute/path/to/lunixi-ruby'`. Pin an audited commit if you use a Bundler Git source.
|
|
@@ -59,11 +58,11 @@ Persist the payment attempt and its UUID idempotency key before the first reques
|
|
|
59
58
|
```ruby
|
|
60
59
|
result = lunixi.payments.create_intent(
|
|
61
60
|
{
|
|
62
|
-
amount:
|
|
61
|
+
amount: "10050",
|
|
63
62
|
currency: 'TRY',
|
|
64
63
|
orderId: order.public_id,
|
|
65
64
|
callbackUrl: 'https://merchant.example/payments/return',
|
|
66
|
-
basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price:
|
|
65
|
+
basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price: "10050" }]
|
|
67
66
|
},
|
|
68
67
|
idempotency_key: order.persisted_payment_attempt_key
|
|
69
68
|
)
|
|
@@ -78,7 +77,7 @@ checkout_token = result.fetch('data').fetch('token')
|
|
|
78
77
|
An intent/token, HTTP 2xx, return URL or intermediate split state does not prove capture. Correlate a verified webhook with the persisted order and authenticate a payment status read; confirm final status, amount and currency before fulfillment.
|
|
79
78
|
|
|
80
79
|
|
|
81
|
-
Amounts must be positive
|
|
80
|
+
Amounts must be positive canonical minor-unit strings within the signed 64-bit fiat range. Root `price` and `paidPrice` are rejected even when null. Basket prices and split leg amounts must equal the relevant total. Intent `installment` is rejected because installments are selected in the checkout form. `preauth: true` requests authorization; capture is a separate operation.
|
|
82
81
|
|
|
83
82
|
## Available helpers
|
|
84
83
|
|
|
@@ -87,15 +86,15 @@ Amounts must be positive integers within JavaScript's safe integer range. Root `
|
|
|
87
86
|
| Create an intent | `lunixi.payments.create_intent(body, idempotency_key:)` |
|
|
88
87
|
| Read/list payments | `lunixi.payments.get(id)`, `lunixi.payments.list(**filters)` |
|
|
89
88
|
| Merchant methods | `lunixi.payments.methods(payment_method: nil)` |
|
|
90
|
-
| Capture | `lunixi.payments.capture(id, idempotency_key
|
|
91
|
-
| Full/partial refund | `lunixi.payments.refund(id, idempotency_key:,
|
|
89
|
+
| Capture | `lunixi.payments.capture(id, idempotency_key:)` |
|
|
90
|
+
| Full/partial refund | `lunixi.payments.refund(id, idempotency_key:, reason: nil)` |
|
|
92
91
|
| Void | `lunixi.payments.void(id, idempotency_key:)` |
|
|
93
92
|
| Split payment | `lunixi.payments.split(body, idempotency_key:)` |
|
|
94
93
|
| Split status/refund | `lunixi.payments.get_split(id)`, `lunixi.payments.refund_split(id, amount:, idempotency_key:, reason: nil)` |
|
|
95
94
|
| Fraud | `lunixi.fraud.evaluate(body)`, `lunixi.fraud.record_event(body, idempotency_key:)` |
|
|
96
95
|
| Advanced signed HTTP | `lunixi.request(method, path, body: nil, query: {}, idempotency_key: nil, signed: true, binary: false)` |
|
|
97
96
|
|
|
98
|
-
|
|
97
|
+
Omit the capture/refund `amount:` keyword to use the remaining amount. Explicit nil, false, zero, numeric and noncanonical string amounts are rejected. Persist a separate operation key for each capture, refund or void. Running split HTTP 400 protocol responses are returned only for documented running states; they remain pending, not paid. Response envelopes are preserved.
|
|
99
98
|
|
|
100
99
|
The advanced transport supports other **documented** gateway routes without a second HTTP implementation:
|
|
101
100
|
|
data/docs/ruby.tr.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Ruby SDK
|
|
3
|
-
description: Lunixi Ruby SDK —
|
|
3
|
+
description: Lunixi Ruby SDK — kurulum, Ed25519 imza, ödeme ve fraud, kalıcı idempotency ve özgün baytlarla v2 webhook doğrulama.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
|
|
@@ -8,20 +8,19 @@ description: Lunixi Ruby SDK — kaynak kurulum, Ed25519 imza, ödeme ve fraud,
|
|
|
8
8
|
|
|
9
9
|
`lunixi-sdk`, Lunixi için sunucu tarafı Ruby istemcisidir. Ed25519 imzayı OpenSSL ile üretir; token’ı eşzamanlı istekler arasında saklar, kompakt JavaScript uyumlu JSON oluşturur, güvenli tekrar denemeleri ve özgün baytlarla webhook doğrulamayı sağlar. Rails, Sinatra, Rack ve düz Ruby ile çalışır. Ana gem çerçeveden bağımsızdır; kaynak depodaki çalıştırılabilir [Rails örneği](/sdks/rails) ayrı adaptördür.
|
|
10
10
|
|
|
11
|
-
## Gereksinimler ve
|
|
11
|
+
## Gereksinimler ve kurulum
|
|
12
12
|
|
|
13
13
|
Ruby **3.3+**, Bundler ve **Ed25519** destekli OpenSSL gerekir. Rails örneği Rails 8.1 kullanır.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
0.1.1 kaynak kod üzerinden dağıtılır. RubyGems yayını ayrı bir sürüm adımıdır; aşağıdaki kurulum yayımlanmış bir registry paketi varsaymaz.
|
|
15
|
+
1.0.1 RubyGems üzerinde kullanılabilir olduğunda Gemfile’a `gem 'lunixi-sdk', '1.0.1'` ekleyip `bundle install` çalıştırın. [Para sözleşmesi geçişini](../MIGRATION-1.0.md) uygulayın. Aşağıdaki kaynak derlemesi geliştirme için isteğe bağlıdır:
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
```bash
|
|
20
|
-
git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
|
|
19
|
+
git clone --branch v1.0.1 --depth 1 https://github.com/Lunixi-Payment/lunixi-ruby.git
|
|
21
20
|
cd lunixi-ruby
|
|
22
21
|
bundle install
|
|
23
22
|
gem build lunixi-sdk.gemspec
|
|
24
|
-
gem install ./lunixi-sdk-0.1.
|
|
23
|
+
gem install ./lunixi-sdk-1.0.1.gem
|
|
25
24
|
```
|
|
26
25
|
|
|
27
26
|
Kaynak: [Lunixi-Payment/lunixi-ruby](https://github.com/Lunixi-Payment/lunixi-ruby). İncelenmiş yerel kopyayı `gem 'lunixi-sdk', path: '/tam/yol/lunixi-ruby'` ile kullanabilirsiniz. Bundler Git kaynağı kullanırken denetlenmiş commit’i sabitleyin.
|
|
@@ -59,11 +58,11 @@ Public anahtarı Lunixi paneline kaydedip verilen key id’yi kullanın. Özel a
|
|
|
59
58
|
```ruby
|
|
60
59
|
result = lunixi.payments.create_intent(
|
|
61
60
|
{
|
|
62
|
-
amount:
|
|
61
|
+
amount: "10050",
|
|
63
62
|
currency: 'TRY',
|
|
64
63
|
orderId: order.public_id,
|
|
65
64
|
callbackUrl: 'https://merchant.example/payments/return',
|
|
66
|
-
basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price:
|
|
65
|
+
basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price: "10050" }]
|
|
67
66
|
},
|
|
68
67
|
idempotency_key: order.persisted_payment_attempt_key
|
|
69
68
|
)
|
|
@@ -78,7 +77,7 @@ checkout_token = result.fetch('data').fetch('token')
|
|
|
78
77
|
Intent/token, HTTP 2xx, dönüş sayfası veya ara split durumu tahsilat kanıtı değildir. İmzalı webhook’u kalıcı siparişle eşleştirin; sunucudan ödeme sorgusunda nihai durum, tutar ve para birimi uyuşmadan siparişi tamamlamayın.
|
|
79
78
|
|
|
80
79
|
|
|
81
|
-
Tutarlar pozitif ve
|
|
80
|
+
Tutarlar pozitif, kanonik alt birim string olmalı ve işaretli 64 bit fiat aralığında kalmalıdır. Kök `price`/`paidPrice` null olsa bile reddedilir. Sepet ve split bacak toplamları ana toplamla aynı olmalıdır. Intent’te `installment` gönderilmez; taksit formda seçilir. `preauth: true` provizyon ister; capture ayrı işlemdir.
|
|
82
81
|
|
|
83
82
|
## Yardımcı API’ler
|
|
84
83
|
|
|
@@ -87,15 +86,15 @@ Tutarlar pozitif ve JavaScript güvenli tam sayı aralığında olmalıdır. Kö
|
|
|
87
86
|
| Intent | `lunixi.payments.create_intent(body, idempotency_key:)` |
|
|
88
87
|
| Ödeme okuma/liste | `lunixi.payments.get(id)`, `lunixi.payments.list(**filters)` |
|
|
89
88
|
| Açık yöntemler | `lunixi.payments.methods(payment_method: nil)` |
|
|
90
|
-
| Capture | `lunixi.payments.capture(id, idempotency_key
|
|
91
|
-
| Tam/kısmi iade | `lunixi.payments.refund(id, idempotency_key:,
|
|
89
|
+
| Capture | `lunixi.payments.capture(id, idempotency_key:)` |
|
|
90
|
+
| Tam/kısmi iade | `lunixi.payments.refund(id, idempotency_key:, reason: nil)` |
|
|
92
91
|
| Provizyon iptali | `lunixi.payments.void(id, idempotency_key:)` |
|
|
93
92
|
| Split | `lunixi.payments.split(body, idempotency_key:)` |
|
|
94
93
|
| Split durum/iade | `lunixi.payments.get_split(id)`, `lunixi.payments.refund_split(id, amount:, idempotency_key:, reason: nil)` |
|
|
95
94
|
| Fraud | `lunixi.fraud.evaluate(body)`, `lunixi.fraud.record_event(body, idempotency_key:)` |
|
|
96
95
|
| İmzalı genel HTTP | `lunixi.request(method, path, body: nil, query: {}, idempotency_key: nil, signed: true, binary: false)` |
|
|
97
96
|
|
|
98
|
-
Capture/iade `amount
|
|
97
|
+
Capture/iade `amount:` anahtarı gönderilmezse kalan tutar kullanılır; açık `amount: nil` reddedilir. False, sıfır, sayısal değerler ve kanonik olmayan string tutar reddedilir. Her capture/iade/iptal için ayrı, kalıcı anahtar saklayın. Belgelenmiş devam eden split durumlarındaki HTTP 400 yanıtları protokol sonucu olarak döndürülür; ödenmiş sayılmaz. Yanıt zarfı korunur.
|
|
99
98
|
|
|
100
99
|
Diğer **dokümante** gateway uçları aynı imzalı transport üzerinden çağrılabilir:
|
|
101
100
|
|
data/lib/lunixi/client.rb
CHANGED
|
@@ -39,6 +39,11 @@ module Lunixi
|
|
|
39
39
|
|
|
40
40
|
path = safe_path(path, query)
|
|
41
41
|
key = self.class.idempotency_key!(idempotency_key) if idempotency_key
|
|
42
|
+
Money.validate_operation_amount(path, body)
|
|
43
|
+
if Money.fiat_route?(path)
|
|
44
|
+
Money.validate_fields(body)
|
|
45
|
+
Money.validate_fields(query)
|
|
46
|
+
end
|
|
42
47
|
bytes = body.nil? ? nil : JsonBody.dump(body)
|
|
43
48
|
raise ConfigurationError, 'Request body exceeds the size limit' if bytes && bytes.bytesize > 1024 * 1024
|
|
44
49
|
|
|
@@ -77,7 +82,7 @@ module Lunixi
|
|
|
77
82
|
retries += 1
|
|
78
83
|
next
|
|
79
84
|
end
|
|
80
|
-
return decode_response(response, binary: binary, running_split: method == 'POST' && path == '/api/v1/payments/split')
|
|
85
|
+
return decode_response(response, binary: binary, fiat: Money.fiat_route?(path), running_split: method == 'POST' && path == '/api/v1/payments/split')
|
|
81
86
|
rescue TransportError
|
|
82
87
|
raise unless safe_to_retry && retries < @max_retries
|
|
83
88
|
|
|
@@ -187,15 +192,20 @@ module Lunixi
|
|
|
187
192
|
@sleeper.call(seconds)
|
|
188
193
|
end
|
|
189
194
|
|
|
190
|
-
def
|
|
195
|
+
def response_payload(response)
|
|
196
|
+
payload = response.body.empty? ? {} : JSON.parse(response.body, max_nesting: 64)
|
|
197
|
+
raise ApiError.new(status: response.status, code: 'invalid_json_response') unless payload.is_a?(Hash)
|
|
198
|
+
|
|
199
|
+
payload
|
|
200
|
+
rescue JSON::ParserError
|
|
201
|
+
raise ApiError.new(status: response.status, code: 'invalid_json_response'), cause: nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def decode_response(response, binary: false, running_split: false, fiat: false)
|
|
191
205
|
return response.body.b.freeze if binary && response.status.between?(200, 299)
|
|
192
206
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
rescue JSON::ParserError
|
|
196
|
-
raise ApiError.new(status: response.status, code: 'invalid_json_response'), cause: nil
|
|
197
|
-
end
|
|
198
|
-
raise ApiError.new(status: response.status, code: 'invalid_json_response') unless payload.is_a?(Hash)
|
|
207
|
+
payload = response_payload(response)
|
|
208
|
+
Money.validate_fields(payload, response: true) if fiat
|
|
199
209
|
return payload if response.status.between?(200, 299)
|
|
200
210
|
|
|
201
211
|
error_class = case response.status
|
data/lib/lunixi/money.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lunixi
|
|
4
|
+
# Canonical public fiat money. Arithmetic uses Ruby's exact Integer only after validation.
|
|
5
|
+
module Money
|
|
6
|
+
MAX = 9_223_372_036_854_775_807
|
|
7
|
+
EXPONENTS = %w[TRY USD EUR GBP RUB AED SAR QAR CHF CAD AUD CNY AZN].to_h { |c| [c, 2] }.merge('JPY' => 0, 'KWD' => 3).freeze
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def fiat_route?(path)
|
|
11
|
+
path.match?(%r{\A/api/v1/(?:payments|wallet|insurance|bank-connect|fraud)(?:[/?]|\z)})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate_operation_amount(path, body)
|
|
15
|
+
return unless path.match?(%r{\A/api/v1/payments/(?:split/)?[^/]+/(?:capture|refund)(?:[?]|\z)}) && body.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
body.each { |key, value| minor(value) if key.to_s == 'amount' }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def minor(value, positive: true, signed: false)
|
|
21
|
+
pattern = if signed
|
|
22
|
+
/\A(?:0|-?[1-9][0-9]*)\z/
|
|
23
|
+
elsif positive
|
|
24
|
+
/\A[1-9][0-9]*\z/
|
|
25
|
+
else
|
|
26
|
+
/\A(?:0|[1-9][0-9]*)\z/
|
|
27
|
+
end
|
|
28
|
+
unless value.is_a?(String) && value.match?(pattern) && value.to_i.between?(-MAX - 1, MAX)
|
|
29
|
+
raise ConfigurationError, 'Canonical minor-unit string in the signed 64-bit fiat range required'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def decimal_to_minor(value, currency)
|
|
36
|
+
exponent = EXPONENTS.fetch(currency) { raise ConfigurationError, 'Unsupported currency' }
|
|
37
|
+
raise ConfigurationError, 'Exact decimal text required' unless value.is_a?(String) && value.match?(/\A(?:0|[1-9][0-9]*)(?:\.[0-9]+)?\z/)
|
|
38
|
+
|
|
39
|
+
whole, fraction = value.split('.', 2)
|
|
40
|
+
fraction ||= ''
|
|
41
|
+
raise ConfigurationError, 'Fractional minor units' if fraction[exponent..]&.match?(/[1-9]/)
|
|
42
|
+
|
|
43
|
+
minor((whole + fraction[0, exponent].ljust(exponent, '0')).to_i.to_s, positive: false)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_fields(value, response: false)
|
|
47
|
+
case value
|
|
48
|
+
when Array
|
|
49
|
+
value.each { |entry| validate_fields(entry, response: response) }
|
|
50
|
+
when Hash
|
|
51
|
+
value.each do |name, entry|
|
|
52
|
+
key = name.to_s
|
|
53
|
+
next if entry.nil? || %w[metadata fields inputFields insuranceData].include?(key)
|
|
54
|
+
|
|
55
|
+
if %w[
|
|
56
|
+
amount amountMinor authorizedAmount availableBalance balanceMinor buyerTotalCharge cancelledPremiumAmount
|
|
57
|
+
capturedAmount capturedAmountMinor chargeAmount collectedAmountMinor collectedPremiumAmount compensatedAmount
|
|
58
|
+
disputedAmount feeTotal fixedFee inForcePremiumAmount insuranceValueAmount legAmount legsAmount lineTotalMinor
|
|
59
|
+
maxAmount maxAmountMinor minAmount minAmountMinor nonRefundableAmount paidAmount paidPrice pendingBalance
|
|
60
|
+
premiumAmount price refundAmount refundOpenPremiumAmount refundableAmount refundedAmount refundedAmountMinor
|
|
61
|
+
refundedPremiumAmount requestedAmount reservedBalance sourceAmountMinor targetAmountMinor taxAmountMinor taxTotal
|
|
62
|
+
totalAmount unitPriceMinor
|
|
63
|
+
].include?(key)
|
|
64
|
+
minor(entry, positive: !response && %w[amount price totalAmount].include?(key), signed: key.downcase.include?('balance'))
|
|
65
|
+
elsif %w[
|
|
66
|
+
data details items rows results payments operations balances wallets legs aggregate refund refunds totals
|
|
67
|
+
charge fees fee balance splits sellers basketItems promissoryNote schedule notes lines offers policies
|
|
68
|
+
reconciliation quotes payment transactions accounts settlements payouts limits
|
|
69
|
+
].include?(key)
|
|
70
|
+
validate_fields(entry, response: response)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/lunixi/payments.rb
CHANGED
|
@@ -5,6 +5,8 @@ module Lunixi
|
|
|
5
5
|
# documented endpoints without pretending to validate their entire DTO schema.
|
|
6
6
|
class Payments
|
|
7
7
|
BASE = '/api/v1/payments'
|
|
8
|
+
OMITTED_AMOUNT = Object.new.freeze
|
|
9
|
+
private_constant :OMITTED_AMOUNT
|
|
8
10
|
attr_reader :client
|
|
9
11
|
|
|
10
12
|
def initialize(client)
|
|
@@ -36,17 +38,17 @@ module Lunixi
|
|
|
36
38
|
client.request('GET', "#{BASE}/methods", query: query)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
#
|
|
40
|
-
def capture(payment_id, idempotency_key:, amount:
|
|
41
|
+
# Omit amount to capture the remaining authorization; explicit nil is invalid.
|
|
42
|
+
def capture(payment_id, idempotency_key:, amount: OMITTED_AMOUNT)
|
|
41
43
|
body = { 'paymentIntentId' => identifier!(payment_id) }
|
|
42
|
-
body['amount'] = minor!(amount) unless amount.
|
|
44
|
+
body['amount'] = minor!(amount) unless amount.equal?(OMITTED_AMOUNT)
|
|
43
45
|
client.request('POST', "#{BASE}/#{payment_id}/capture", body: body, idempotency_key: Client.idempotency_key!(idempotency_key))
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
# Refund captured funds; persist a separate key for each refund operation.
|
|
47
|
-
def refund(payment_id, idempotency_key:, amount:
|
|
49
|
+
def refund(payment_id, idempotency_key:, amount: OMITTED_AMOUNT, reason: nil)
|
|
48
50
|
body = { 'paymentIntentId' => identifier!(payment_id) }
|
|
49
|
-
body['amount'] = minor!(amount) unless amount.
|
|
51
|
+
body['amount'] = minor!(amount) unless amount.equal?(OMITTED_AMOUNT)
|
|
50
52
|
body['reason'] = reason!(reason) unless reason.nil?
|
|
51
53
|
client.request('POST', "#{BASE}/#{payment_id}/refund", body: body, idempotency_key: Client.idempotency_key!(idempotency_key))
|
|
52
54
|
end
|
|
@@ -68,9 +70,9 @@ module Lunixi
|
|
|
68
70
|
total = legs.sum do |leg|
|
|
69
71
|
raise ConfigurationError, 'Split legs must be objects' unless leg.is_a?(Hash)
|
|
70
72
|
|
|
71
|
-
minor!(leg['amount'])
|
|
73
|
+
minor!(leg['amount']).to_i
|
|
72
74
|
end
|
|
73
|
-
raise ConfigurationError, 'Split leg amounts must equal totalAmount' unless total == data['totalAmount']
|
|
75
|
+
raise ConfigurationError, 'Split leg amounts must equal totalAmount' unless total == data['totalAmount'].to_i
|
|
74
76
|
|
|
75
77
|
client.request('POST', "#{BASE}/split", body: data, idempotency_key: Client.idempotency_key!(idempotency_key))
|
|
76
78
|
end
|
|
@@ -97,11 +99,7 @@ module Lunixi
|
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
def minor!(value)
|
|
100
|
-
|
|
101
|
-
raise ConfigurationError, 'Amount must be a positive safe integer in minor units'
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
value
|
|
102
|
+
Money.minor(value)
|
|
105
103
|
end
|
|
106
104
|
|
|
107
105
|
def currency!(value)
|
|
@@ -155,9 +153,9 @@ module Lunixi
|
|
|
155
153
|
total = items.sum do |item|
|
|
156
154
|
raise ConfigurationError, 'Basket items must be objects' unless item.is_a?(Hash)
|
|
157
155
|
|
|
158
|
-
minor!(item['price'])
|
|
156
|
+
minor!(item['price']).to_i
|
|
159
157
|
end
|
|
160
|
-
raise ConfigurationError, 'Basket total must equal amount' unless total == amount
|
|
158
|
+
raise ConfigurationError, 'Basket total must equal amount' unless total == amount.to_i
|
|
161
159
|
end
|
|
162
160
|
end
|
|
163
161
|
end
|
data/lib/lunixi/version.rb
CHANGED
data/lib/lunixi.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lunixi-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lunixi
|
|
@@ -101,6 +101,7 @@ files:
|
|
|
101
101
|
- ".github/assets/lunixi-logo.svg"
|
|
102
102
|
- CHANGELOG.md
|
|
103
103
|
- LICENSE
|
|
104
|
+
- MIGRATION-1.0.md
|
|
104
105
|
- README.md
|
|
105
106
|
- README.tr.md
|
|
106
107
|
- SECURITY.md
|
|
@@ -115,6 +116,7 @@ files:
|
|
|
115
116
|
- lib/lunixi/errors.rb
|
|
116
117
|
- lib/lunixi/fraud.rb
|
|
117
118
|
- lib/lunixi/json_body.rb
|
|
119
|
+
- lib/lunixi/money.rb
|
|
118
120
|
- lib/lunixi/payments.rb
|
|
119
121
|
- lib/lunixi/signer.rb
|
|
120
122
|
- lib/lunixi/transport.rb
|