dpay 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +100 -4
- data/lib/dpay/blik/blik_alias_type.rb +2 -2
- data/lib/dpay/blik/blik_service.rb +0 -9
- data/lib/dpay/card/card_service.rb +21 -5
- data/lib/dpay/client.rb +3 -1
- data/lib/dpay/errors.rb +13 -6
- data/lib/dpay/internal/checksum_calculator.rb +27 -2
- data/lib/dpay/internal/error_mapper.rb +11 -2
- data/lib/dpay/internal/php.rb +6 -0
- data/lib/dpay/ipn/ipn_event.rb +2 -0
- data/lib/dpay/ipn/ipn_type.rb +1 -0
- data/lib/dpay/payment/payment_service.rb +4 -4
- data/lib/dpay/payment/register_payment_request.rb +92 -13
- data/lib/dpay/payment/registered_payment.rb +19 -0
- data/lib/dpay/payment/return_urls.rb +3 -1
- data/lib/dpay/payment/transaction_type.rb +1 -3
- data/lib/dpay/recurring/recurring_registration.rb +162 -0
- data/lib/dpay/recurring/recurring_registration_info.rb +45 -0
- data/lib/dpay/recurring/recurring_retry_result.rb +43 -0
- data/lib/dpay/recurring/recurring_service.rb +59 -0
- data/lib/dpay/recurring/recurring_status.rb +42 -0
- data/lib/dpay/refund/refund_service.rb +10 -4
- data/lib/dpay/version.rb +1 -1
- data/lib/dpay/webhook/event_page.rb +25 -0
- data/lib/dpay/webhook/event_service.rb +76 -0
- data/lib/dpay/webhook/webhook_event.rb +42 -0
- data/lib/dpay/webhook/webhook_event_type.rb +43 -0
- data/lib/dpay/webhook/webhook_target.rb +45 -0
- data/lib/dpay/webhook/webhook_verifier.rb +110 -0
- data/lib/dpay.rb +11 -3
- data/sig/dpay/blik/blik_alias_type.rbs +0 -1
- data/sig/dpay/blik/blik_service.rbs +0 -1
- data/sig/dpay/card/card_service.rbs +1 -1
- data/sig/dpay/client.rbs +2 -0
- data/sig/dpay/errors.rbs +6 -2
- data/sig/dpay/internal/checksum_calculator.rbs +8 -1
- data/sig/dpay/internal/error_mapper.rbs +1 -0
- data/sig/dpay/internal/php.rbs +2 -0
- data/sig/dpay/ipn/ipn_event.rbs +2 -0
- data/sig/dpay/ipn/ipn_type.rbs +1 -0
- data/sig/dpay/payment/register_payment_request.rbs +13 -1
- data/sig/dpay/payment/registered_payment.rbs +6 -0
- data/sig/dpay/payment/return_urls.rbs +2 -2
- data/sig/dpay/payment/transaction_type.rbs +0 -2
- data/sig/dpay/recurring/recurring_registration.rbs +45 -0
- data/sig/dpay/{blik/blik_recurring_registration_info.rbs → recurring/recurring_registration_info.rbs} +13 -3
- data/sig/dpay/recurring/recurring_retry_result.rbs +24 -0
- data/sig/dpay/recurring/recurring_service.rbs +15 -0
- data/sig/dpay/recurring/recurring_status.rbs +27 -0
- data/sig/dpay/refund/refund_service.rbs +2 -2
- data/sig/dpay/webhook/event_page.rbs +14 -0
- data/sig/dpay/webhook/event_service.rbs +21 -0
- data/sig/dpay/webhook/webhook_event.rbs +24 -0
- data/sig/dpay/webhook/webhook_event_type.rbs +23 -0
- data/sig/dpay/webhook/webhook_target.rbs +15 -0
- data/sig/dpay/webhook/webhook_verifier.rbs +37 -0
- metadata +26 -10
- data/lib/dpay/blik/blik_recurring_registration.rb +0 -74
- data/lib/dpay/blik/blik_recurring_registration_info.rb +0 -26
- data/lib/dpay/blik/blik_recurring_status.rb +0 -28
- data/sig/dpay/blik/blik_recurring_registration.rbs +0 -24
- data/sig/dpay/blik/blik_recurring_status.rbs +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96829708b670a52f5790c2e96db83946582206a72dd1c81cbd3a1744306b7fe9
|
|
4
|
+
data.tar.gz: 9928ed9b7eb9c6b6da934ad204e6c7afb1aff3d44b12088b33382c74144cb39c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc7432ce768eeaf7c9795af7177bbdc4b4036bbbcb0c14c390c58c6ad078f40c9889eb8997580f1d44b26ae26df35d28380dba208a81172c3471f094c29977d3
|
|
7
|
+
data.tar.gz: 04d7f864c82d9f4a29600a61edebc77673c7b2429900018d514d23683f7e9e326b990950298ac17dc76e71f176d2a90e6fbb1d3d6f24a4d4dcdeeaa2455c288a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ projekt stosuje [Semantic Versioning](https://semver.org/lang/pl/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - wydanie razem z wdrożeniem API dpay
|
|
11
|
+
|
|
12
|
+
Wersja wymaga API dpay z tym samym wydaniem (wspólne API płatności cyklicznych, suma kontrolna capture
|
|
13
|
+
i anulowania kart). Zmiany łamiące zgodność są oznaczone jako **BREAKING**.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `client.recurring` (`DPay::RecurringService`): `status`, `retry` i `cancel` płatności cyklicznej
|
|
18
|
+
(`/api/v1_0/payments/recurring/*`), modele `DPay::RecurringStatus`, `DPay::RecurringRegistrationInfo`,
|
|
19
|
+
`DPay::RecurringRetryResult`.
|
|
20
|
+
- `RegisterPaymentRequest#with_recurring_registration(DPay::RecurringRegistration)` - rejestracja płatności
|
|
21
|
+
cyklicznej (modele O, A i M, `terms_url` wymagany) z kodem BLIK klienta.
|
|
22
|
+
- `RegisterPaymentRequest#with_recurring_alias` - obciążenie zapisanej płatności cyklicznej bez kodu BLIK;
|
|
23
|
+
alias wchodzi do sumy kontrolnej. `#with_client_context` - opcjonalne IP i przeglądarka klienta przy obciążeniu.
|
|
24
|
+
- Webhooki: `DPay::WebhookVerifier.construct_event` i `.verify` (Standard Webhooks, podpis `v1`, tolerancja czasu,
|
|
25
|
+
kilka podpisów i sekretów w czasie rotacji; nagłówki z `Hash` z kluczami `String` lub `Symbol` w dowolnej
|
|
26
|
+
wielkości liter albo w formacie Rack `HTTP_WEBHOOK_ID`), `DPay::WebhookEvent`, `DPay::WebhookEventType`.
|
|
27
|
+
- `client.events` (`DPay::EventService`): historia zdarzeń z filtrami, `list` i `iterate` po stronach
|
|
28
|
+
(`DPay::EventPage`).
|
|
29
|
+
- `DPay::WebhookTarget` - własny adres zdarzeń w rejestracji płatności (`#with_webhook`), zwrocie
|
|
30
|
+
(`refunds.create`) i capture karty (`cards.capture`); `RegisterPaymentRequest#with_reference`.
|
|
31
|
+
- `DPay::ApiError#reason`, `DPay::PaymentRejectedError#error_description` (kod błędu także
|
|
32
|
+
z `additionalInfo.error`), `DPay::RegisteredPayment#recurring_alias` i `#recurring_methods`.
|
|
33
|
+
- `spec/fixtures/api_vectors.json` - wspólne wektory sum kontrolnych i podpisów webhooków wszystkich SDK dpay.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **BREAKING** `cards.capture` i `cards.cancel` wysyłają `service` i sumę
|
|
38
|
+
`sha256(operacja|service|transaction_id|amount|hash)` - API odrzuca je bez sumy (401).
|
|
39
|
+
- **BREAKING** `DPay::ReturnUrls`: adres IPN jest opcjonalny (`ipn` może być `nil`); bez niego `url_ipn`
|
|
40
|
+
nie jest wysyłany, a IPN nie przychodzi (wynik przychodzi webhookiem).
|
|
41
|
+
- Kod błędu API (`DPay::ApiError#error_code`) pochodzi z pola `code` (np. `CHECKSUM_REQUIRED`,
|
|
42
|
+
`WEBHOOK_URL_INVALID`), potem z `errorcode`.
|
|
43
|
+
- `ChecksumCalculator#ordered_body` przyjmuje całe body i spłaszcza obiekty zagnieżdżone (np. `webhook`).
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
|
|
47
|
+
- **BREAKING** `BlikService#recurring_status`, `DPay::BlikRecurringRegistration`, `DPay::BlikRecurringStatus`,
|
|
48
|
+
`DPay::BlikRecurringRegistrationInfo` i `RegisterPaymentRequest#with_register_blik_recurring_alias` - API
|
|
49
|
+
usunęło te endpointy i pole; użyj `client.recurring` i `#with_recurring_registration`.
|
|
50
|
+
- **BREAKING** `BlikAliasType::PAYID` (aliasy OneClick są tylko `UID`), `TransactionType::BLIK_RECURRING`
|
|
51
|
+
i `TransactionType::BIZUM_DIRECT` (API odrzuca je kodem 422).
|
|
52
|
+
|
|
53
|
+
### Deprecated
|
|
54
|
+
|
|
55
|
+
- `IpnType::CAPTURE`, `IpnEvent#capture?` i `#capture_payment_id` - dpay nie wysyła już IPN typu `capture`;
|
|
56
|
+
użyj zdarzenia `payment.captured`.
|
|
57
|
+
|
|
10
58
|
## [0.1.0] - 2026-07-23
|
|
11
59
|
|
|
12
60
|
### Added
|
data/README.md
CHANGED
|
@@ -46,9 +46,84 @@ redirect_to payment.redirect_url if payment.redirect_url
|
|
|
46
46
|
|
|
47
47
|
`DPay::Money.pln(1050)` przyjmuje kwotę w groszach (najmniejszej jednostce), czyli 10,50 PLN.
|
|
48
48
|
`RegisterPaymentRequest` to budowniczy: każda metoda `with_*` zwraca `self`, więc wywołania można łączyć w łańcuch.
|
|
49
|
+
Adres IPN w `ReturnUrls` jest opcjonalny - bez niego IPN nie przychodzi, a wynik płatności dostaniesz webhookiem.
|
|
50
|
+
|
|
51
|
+
## Płatności cykliczne
|
|
52
|
+
|
|
53
|
+
Rejestracja idzie razem z płatnością kodem BLIK klienta (kwota `0` - sama zgoda, więcej - opłata inicjalna).
|
|
54
|
+
Kolejne obciążenia wysyła Twój serwer, bez kodu.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
urls = DPay::ReturnUrls.new("https://twojsklep.pl/sukces", "https://twojsklep.pl/blad")
|
|
58
|
+
|
|
59
|
+
registration = dpay.payments.register(
|
|
60
|
+
DPay::RegisterPaymentRequest
|
|
61
|
+
.create(DPay::Money.pln(0), DPay::TransactionType::TRANSFERS, urls)
|
|
62
|
+
.with_blik_code(kod_blik, request.user_agent, request.remote_ip)
|
|
63
|
+
.with_recurring_registration(
|
|
64
|
+
DPay::RecurringRegistration
|
|
65
|
+
.create("Abonament Premium", DPay::RecurringRegistration::MODEL_O, "https://twojsklep.pl/regulamin")
|
|
66
|
+
.with_alias("SUB-1234")
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
registration.recurring_alias # => "SUB-1234"
|
|
70
|
+
|
|
71
|
+
charge = dpay.payments.register(
|
|
72
|
+
DPay::RegisterPaymentRequest
|
|
73
|
+
.create(DPay::Money.pln(4999), DPay::TransactionType::TRANSFERS, urls)
|
|
74
|
+
.with_recurring_alias("SUB-1234")
|
|
75
|
+
.with_description("Abonament Premium 10/2026")
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
status = dpay.recurring.status("SUB-1234") # status.status: ACTIVE, INACTIVE, UNREGISTERED, EXPIRED, DECLINED
|
|
79
|
+
dpay.recurring.retry(charge.transaction_id) # po odmowie, np. INSUFFICIENT_FUNDS
|
|
80
|
+
dpay.recurring.cancel("SUB-1234", "Rezygnacja klienta")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Model `A` (stała kwota) wymaga `with_frequency`, `with_limit_amt`, `with_tot_limit_amt` (kwoty w groszach),
|
|
84
|
+
`with_expiration_date` i `with_init_date`, model `M` przyjmuje je opcjonalnie, a model `O` ich nie dopuszcza.
|
|
85
|
+
Obciążenie wiąże alias z sumą kontrolną, a anulowanie ma własną sumę - SDK liczy obie.
|
|
86
|
+
Limity API: `status` do 60, `retry` i `cancel` do 30 zapytań na minutę (licznik wspólny z resztą API
|
|
87
|
+
płatności z tego adresu IP) - nie odpytuj statusu w pętli, wynik przychodzi webhookiem.
|
|
88
|
+
|
|
89
|
+
## Webhooki
|
|
90
|
+
|
|
91
|
+
Zdarzenia (`payment.succeeded`, `refund.failed`, `recurring_payment.canceled` i inne) są podpisane
|
|
92
|
+
(Standard Webhooks). Weryfikuj je na surowym body, przed parsowaniem JSON:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
begin
|
|
96
|
+
event = DPay::WebhookVerifier.construct_event(
|
|
97
|
+
request.raw_post,
|
|
98
|
+
request.headers,
|
|
99
|
+
ENV.fetch("DPAY_WEBHOOK_SECRET") # sekret endpointu z panelu (whsec_...); w czasie rotacji tablica sekretów
|
|
100
|
+
)
|
|
101
|
+
rescue DPay::SignatureVerificationError
|
|
102
|
+
return head :bad_request
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
payment = event.object if event.type == DPay::WebhookEventType::PAYMENT_SUCCEEDED # Hash, kwoty w groszach
|
|
106
|
+
|
|
107
|
+
head :ok
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Nagłówki podajesz jako `Hash` (albo obiekt z `#each` zwracającym nazwę i wartość) z kluczami `String` lub `Symbol`
|
|
111
|
+
w dowolnej wielkości liter (`"webhook-id"`, `"Webhook-Id"`, `:webhook_id`) albo w formacie Rack (`"HTTP_WEBHOOK_ID"`),
|
|
112
|
+
więc zadziała zarówno `request.headers` w Rails, jak i `request.env` w Rack i Sinatrze. Wartość nagłówka może być
|
|
113
|
+
tablicą - liczy się pierwszy element. Znacznik czasu może odbiegać od zegara serwera najwyżej o 300 sekund
|
|
114
|
+
(`DPay::WebhookVerifier::DEFAULT_TOLERANCE`, inną wartość podasz czwartym argumentem). Sama weryfikacja bez
|
|
115
|
+
parsowania: `DPay::WebhookVerifier.verify` z tymi samymi argumentami.
|
|
116
|
+
|
|
117
|
+
Deduplikuj zdarzenia po `event.id`. Historię zdarzeń (np. po awarii endpointu) pobierzesz przez
|
|
118
|
+
`dpay.events.iterate(types: ["payment.succeeded"]) { |event| ... }` albo stronami przez `dpay.events.list`.
|
|
119
|
+
|
|
120
|
+
Własny adres zdarzeń jednej płatności: `.with_webhook(DPay::WebhookTarget.create("https://twojsklep.pl/webhooks"))`
|
|
121
|
+
(podpisywany sekretem webhooków serwisu), a Twój identyfikator zamówienia w zdarzeniach: `.with_reference("order-1234")`.
|
|
49
122
|
|
|
50
123
|
## Obsługa IPN
|
|
51
124
|
|
|
125
|
+
IPN przychodzi tylko wtedy, gdy podasz adres IPN w `ReturnUrls`.
|
|
126
|
+
|
|
52
127
|
Brama traktuje jako potwierdzenie dokładnie treść odpowiedzi, nie kod HTTP. Musi to być dokładnie napis
|
|
53
128
|
`"OK"` (dostępny jako stała `DPay::IpnEvent::ACK`) - jakikolwiek inna treść w body oznacza dla dpay.pl,
|
|
54
129
|
że powiadomienie nie zostało obsłużone i zostanie wysłane ponownie.
|
|
@@ -60,7 +135,7 @@ rescue DPay::SignatureVerificationError
|
|
|
60
135
|
return render plain: "Invalid signature", status: :bad_request
|
|
61
136
|
end
|
|
62
137
|
|
|
63
|
-
mark_order_as_paid(event.id, event.amount) if event.transfer?
|
|
138
|
+
mark_order_as_paid(event.id, event.amount) if event.transfer?
|
|
64
139
|
|
|
65
140
|
render plain: DPay::IpnEvent::ACK
|
|
66
141
|
```
|
|
@@ -75,6 +150,14 @@ zamiast ufać wyłącznie faktowi otrzymania powiadomienia.
|
|
|
75
150
|
dpay.refunds.create("identyfikator-transakcji")
|
|
76
151
|
dpay.refunds.create("identyfikator-transakcji", DPay::Money.pln(500), "reklamacja")
|
|
77
152
|
|
|
153
|
+
# Odpowiedź oznacza przyjęcie zwrotu - wynik przychodzi zdarzeniem refund.succeeded / refund.failed
|
|
154
|
+
dpay.refunds.create(
|
|
155
|
+
"identyfikator-transakcji",
|
|
156
|
+
DPay::Money.pln(500),
|
|
157
|
+
nil,
|
|
158
|
+
DPay::WebhookTarget.create("https://twojsklep.pl/webhooks/zwroty", %w[refund.succeeded refund.failed])
|
|
159
|
+
)
|
|
160
|
+
|
|
78
161
|
availability = dpay.refunds.check_availability("identyfikator-transakcji")
|
|
79
162
|
availability.available?
|
|
80
163
|
```
|
|
@@ -119,6 +202,18 @@ offer = result.dcc_offer if result.dcc_offer?
|
|
|
119
202
|
|
|
120
203
|
Klucz publiczny jest rotowany - pobieraj go przed każdą próbą płatności, nie przechowuj go u siebie.
|
|
121
204
|
|
|
205
|
+
Po preautoryzacji (`pre_auth`) pobierasz środki przez `capture` albo zwalniasz je przez `cancel`. Oba wywołania
|
|
206
|
+
SDK podpisuje sumą kontrolną operacji; `capture` przyjmuje też własny adres zdarzenia `payment.captured`:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
dpay.cards.capture(
|
|
210
|
+
"identyfikator-transakcji",
|
|
211
|
+
DPay::Money.pln(2999),
|
|
212
|
+
DPay::WebhookTarget.create("https://twojsklep.pl/webhooks", ["payment.captured"])
|
|
213
|
+
)
|
|
214
|
+
dpay.cards.cancel("identyfikator-transakcji") # bez kwoty: cała nieprzechwycona reszta
|
|
215
|
+
```
|
|
216
|
+
|
|
122
217
|
## Obsługa błędów
|
|
123
218
|
|
|
124
219
|
Wszystkie wyjątki SDK dołączają moduł `DPay::Error`, więc `rescue DPay::Error` łapie każdy z nich.
|
|
@@ -130,7 +225,8 @@ rescue DPay::InvalidRequestError => e
|
|
|
130
225
|
e.field_errors
|
|
131
226
|
rescue DPay::ApiError => e
|
|
132
227
|
e.http_status
|
|
133
|
-
e.error_code
|
|
228
|
+
e.error_code # np. CHECKSUM_REQUIRED, WEBHOOK_URL_INVALID
|
|
229
|
+
e.reason # szczegół kodu, np. "https_required" przy WEBHOOK_URL_INVALID
|
|
134
230
|
rescue DPay::TransportError
|
|
135
231
|
# błąd sieci - status płatności jest nieznany, sprawdź go przez payments.details()
|
|
136
232
|
rescue DPay::Error => e
|
|
@@ -146,9 +242,9 @@ end
|
|
|
146
242
|
| `DPay::NotFoundError` | HTTP 404 |
|
|
147
243
|
| `DPay::RateLimitError` | HTTP 429, dodatkowo `retry_after`, `limit`, `remaining` |
|
|
148
244
|
| `DPay::ApiServerError` | HTTP 5xx |
|
|
149
|
-
| `DPay::PaymentRejectedError` | rejestracja płatności odrzucona mimo HTTP 200 |
|
|
245
|
+
| `DPay::PaymentRejectedError` | rejestracja płatności odrzucona mimo HTTP 200, dodatkowo `transaction_id`, `error_description` |
|
|
150
246
|
| `DPay::CardPaymentError` | płatność kartą odrzucona mimo HTTP 200 |
|
|
151
|
-
| `DPay::SignatureVerificationError` | niepoprawny lub brakujący podpis IPN |
|
|
247
|
+
| `DPay::SignatureVerificationError` | niepoprawny lub brakujący podpis IPN albo webhooka |
|
|
152
248
|
| `DPay::TransportError` | błąd sieci lub transportu HTTP |
|
|
153
249
|
| `DPay::InvalidArgumentError` | niepoprawny argument przekazany do SDK |
|
|
154
250
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module DPay
|
|
4
4
|
module BlikAliasType
|
|
5
|
+
# BLIK OneClick alias. Recurring payments (PAYID) are handled by Client#recurring.
|
|
5
6
|
UID = "UID"
|
|
6
|
-
PAYID = "PAYID"
|
|
7
7
|
|
|
8
|
-
ALL = [UID
|
|
8
|
+
ALL = [UID].freeze
|
|
9
9
|
|
|
10
10
|
def self.valid?(value)
|
|
11
11
|
ALL.include?(value)
|
|
@@ -23,15 +23,6 @@ module DPay
|
|
|
23
23
|
nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def recurring_status(alias_value)
|
|
27
|
-
body = { "service" => @api.service, "alias_value" => alias_value }
|
|
28
|
-
body["checksum"] = @api.checksum.secret_second(@api.service, [alias_value])
|
|
29
|
-
|
|
30
|
-
data = @api.post_json(Internal::BaseUrls::API_PAYMENTS, "/api/v1_0/payments/blik/recurring/status", body)
|
|
31
|
-
|
|
32
|
-
BlikRecurringStatus.from_api(payload(data))
|
|
33
|
-
end
|
|
34
|
-
|
|
35
26
|
private
|
|
36
27
|
|
|
37
28
|
def alias_body(alias_value, alias_type, reason = nil)
|
|
@@ -18,12 +18,28 @@ module DPay
|
|
|
18
18
|
post(transaction_id, "/pay/card-pre-auth", request.to_body)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
# Captures a pre-authorised amount (partial captures allowed up to the authorisation). Signed with
|
|
22
|
+
# sha256(capture|service|transaction_id|amount|hash). Optional webhook target for "payment.captured".
|
|
23
|
+
def capture(transaction_id, amount, webhook = nil)
|
|
24
|
+
# @type var body: Hash[String, untyped]
|
|
25
|
+
body = { "service" => @api.service, "amount" => amount.to_decimal.to_f }
|
|
26
|
+
unless webhook.nil?
|
|
27
|
+
webhook.assert_events_allowed(WebhookEventType::CAPTURE, "a card capture")
|
|
28
|
+
body["webhook"] = webhook.to_h
|
|
29
|
+
end
|
|
30
|
+
body["checksum"] = @api.checksum.operation("capture", @api.service, transaction_id, amount.to_decimal)
|
|
31
|
+
|
|
32
|
+
post(transaction_id, "/capture", body)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Cancels the pre-authorisation, the whole uncaptured remainder without an amount. Signed with
|
|
36
|
+
# sha256(cancellation|service|transaction_id|amount|hash) - empty amount segment without an amount.
|
|
25
37
|
def cancel(transaction_id, amount = nil)
|
|
26
|
-
|
|
38
|
+
body = { "service" => @api.service }
|
|
39
|
+
body["amount"] = amount.to_decimal.to_f unless amount.nil?
|
|
40
|
+
body["checksum"] = @api.checksum.operation("cancellation", @api.service, transaction_id, amount&.to_decimal)
|
|
41
|
+
|
|
42
|
+
post(transaction_id, "/cancellation", body)
|
|
27
43
|
end
|
|
28
44
|
|
|
29
45
|
def google_pay(transaction_id, request)
|
data/lib/dpay/client.rb
CHANGED
|
@@ -4,7 +4,7 @@ module DPay
|
|
|
4
4
|
class Client
|
|
5
5
|
VERSION = DPay::VERSION
|
|
6
6
|
|
|
7
|
-
attr_reader :config, :payments, :refunds, :banks, :blik, :cards, :payouts
|
|
7
|
+
attr_reader :config, :payments, :refunds, :banks, :blik, :cards, :payouts, :recurring, :events
|
|
8
8
|
|
|
9
9
|
def initialize(service:, secret_hash:, timeout: Config::DEFAULT_TIMEOUT, http_client: nil, base_urls: nil)
|
|
10
10
|
@config = Config.new(
|
|
@@ -20,6 +20,8 @@ module DPay
|
|
|
20
20
|
@blik = BlikService.new(api)
|
|
21
21
|
@cards = CardService.new(api)
|
|
22
22
|
@payouts = PayoutService.new(api)
|
|
23
|
+
@recurring = RecurringService.new(api)
|
|
24
|
+
@events = EventService.new(api)
|
|
23
25
|
freeze
|
|
24
26
|
end
|
|
25
27
|
end
|
data/lib/dpay/errors.rb
CHANGED
|
@@ -22,14 +22,16 @@ module DPay
|
|
|
22
22
|
class ApiError < StandardError
|
|
23
23
|
include Error
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# reason: detail next to the error code, e.g. "https_required" for WEBHOOK_URL_INVALID.
|
|
26
|
+
attr_reader :http_status, :error_code, :field_errors, :raw_body, :reason
|
|
26
27
|
|
|
27
|
-
def initialize(message, http_status, error_code = nil, field_errors = {}, raw_body = "")
|
|
28
|
+
def initialize(message, http_status, error_code = nil, field_errors = {}, raw_body = "", reason = nil)
|
|
28
29
|
super(message)
|
|
29
30
|
@http_status = http_status
|
|
30
31
|
@error_code = error_code
|
|
31
32
|
@field_errors = field_errors
|
|
32
33
|
@raw_body = raw_body
|
|
34
|
+
@reason = reason
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -51,19 +53,24 @@ module DPay
|
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
class PaymentRejectedError < ApiError
|
|
54
|
-
|
|
56
|
+
# error_description: the provider's description of the decline, when it sent one.
|
|
57
|
+
attr_reader :transaction_id, :error_description
|
|
55
58
|
|
|
56
59
|
def self.from_api(data)
|
|
57
60
|
message = %w[message msg].filter_map { |key| data[key] if data[key].is_a?(String) }.first || "Payment rejected"
|
|
58
|
-
|
|
61
|
+
additional = data["additionalInfo"].is_a?(Hash) ? data["additionalInfo"] : {}
|
|
62
|
+
error_code = [data["errorcode"], additional["error"]].find { |code| code.is_a?(String) }
|
|
59
63
|
transaction_id = data["transactionId"].nil? ? nil : Internal::PHP.strval(data["transactionId"])
|
|
64
|
+
description = additional["error_description"].is_a?(String) ? additional["error_description"] : nil
|
|
60
65
|
|
|
61
|
-
new(message, 200, error_code, {}, Internal::PHP.json_encode(data), transaction_id)
|
|
66
|
+
new(message, 200, error_code, {}, Internal::PHP.json_encode(data), transaction_id, description)
|
|
62
67
|
end
|
|
63
68
|
|
|
64
|
-
def initialize(message, http_status, error_code = nil, field_errors = {}, raw_body = "", transaction_id = nil
|
|
69
|
+
def initialize(message, http_status, error_code = nil, field_errors = {}, raw_body = "", transaction_id = nil,
|
|
70
|
+
error_description = nil)
|
|
65
71
|
super(message, http_status, error_code, field_errors, raw_body)
|
|
66
72
|
@transaction_id = transaction_id
|
|
73
|
+
@error_description = error_description
|
|
67
74
|
end
|
|
68
75
|
end
|
|
69
76
|
|
|
@@ -5,22 +5,47 @@ require "digest"
|
|
|
5
5
|
module DPay
|
|
6
6
|
module Internal
|
|
7
7
|
class ChecksumCalculator
|
|
8
|
+
CHECKSUM_KEY = "checksum"
|
|
9
|
+
|
|
8
10
|
def initialize(secret_hash)
|
|
9
11
|
@secret_hash = secret_hash
|
|
10
12
|
freeze
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
# sha256(service|secret_hash|field1|field2|...) - payment registration, BLIK aliases, recurring payments, events.
|
|
13
16
|
def secret_second(service, fields)
|
|
14
17
|
parts = [service, @secret_hash] + fields.map { |field| PHP.strval(field) }
|
|
15
18
|
|
|
16
19
|
Digest::SHA256.hexdigest(parts.join("|"))
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
# sha256(value1|value2|...|secret_hash) over the request body in the order it is sent (PBL API: refunds,
|
|
23
|
+
# transaction details, banks, payouts). Takes the whole body (Hash) or its values (Array). The "checksum" key
|
|
24
|
+
# is skipped, nested objects (e.g. "webhook") contribute their leaf values in order, nil and false give an empty
|
|
25
|
+
# segment and true gives "1" - the way the API casts JSON values to strings.
|
|
26
|
+
def ordered_body(body)
|
|
27
|
+
values = body.is_a?(Hash) ? body.reject { |key, _| key.to_s == CHECKSUM_KEY }.values : body
|
|
28
|
+
joined = values.flat_map { |value| leaves(value) }.map { |leaf| PHP.strval(leaf) }.join("|")
|
|
21
29
|
|
|
22
30
|
Digest::SHA256.hexdigest("#{joined}|#{@secret_hash}")
|
|
23
31
|
end
|
|
32
|
+
|
|
33
|
+
# sha256(operation|service|transaction_id|amount|secret_hash) - Cards API capture and cancellation. The
|
|
34
|
+
# operation name keeps a capture checksum from authorising a cancellation; without an amount the segment
|
|
35
|
+
# stays empty.
|
|
36
|
+
def operation(operation, service, transaction_id, amount)
|
|
37
|
+
Digest::SHA256.hexdigest([operation, service, transaction_id, amount || "", @secret_hash].join("|"))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def leaves(value)
|
|
43
|
+
case value
|
|
44
|
+
when Hash then value.values.flat_map { |item| leaves(item) }
|
|
45
|
+
when Array then value.flat_map { |item| leaves(item) }
|
|
46
|
+
else [value]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
24
49
|
end
|
|
25
50
|
end
|
|
26
51
|
end
|
|
@@ -17,12 +17,21 @@ module DPay
|
|
|
17
17
|
error_class(response.status).new(
|
|
18
18
|
message,
|
|
19
19
|
response.status,
|
|
20
|
-
data
|
|
20
|
+
extract_code(data),
|
|
21
21
|
normalize_field_errors(data["errors"]),
|
|
22
|
-
response.body
|
|
22
|
+
response.body,
|
|
23
|
+
data["reason"].is_a?(String) ? data["reason"] : nil
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
# "code" (Cards API, webhooks, Connect: CHECKSUM_REQUIRED, WEBHOOK_URL_INVALID, ...), then legacy "errorcode".
|
|
28
|
+
def extract_code(data)
|
|
29
|
+
return data["code"] if data["code"].is_a?(String)
|
|
30
|
+
return data["errorcode"] if data["errorcode"].is_a?(String)
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
def error_class(status)
|
|
27
36
|
case status
|
|
28
37
|
when 401 then AuthenticationError
|
data/lib/dpay/internal/php.rb
CHANGED
|
@@ -7,9 +7,15 @@ module DPay
|
|
|
7
7
|
module PHP
|
|
8
8
|
SAFE_INTEGRAL_FLOAT = 2**53
|
|
9
9
|
NON_UNRESERVED = /[^A-Za-z0-9\-_.~]/n
|
|
10
|
+
TRIM_EDGES = /\A[ \t\n\r\x00\x0B]+|[ \t\n\r\x00\x0B]+\z/
|
|
10
11
|
|
|
11
12
|
module_function
|
|
12
13
|
|
|
14
|
+
# PHP trim(): strips " \t\n\r\0\x0B" from both ends (String#strip also strips "\f").
|
|
15
|
+
def trim(value)
|
|
16
|
+
value.gsub(TRIM_EDGES, "")
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def strval(value)
|
|
14
20
|
case value
|
|
15
21
|
when nil, false then ""
|
data/lib/dpay/ipn/ipn_event.rb
CHANGED
|
@@ -39,6 +39,7 @@ module DPay
|
|
|
39
39
|
scalar("custom")
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# @deprecated dpay no longer sends capture IPNs - use the "payment.captured" webhook event.
|
|
42
43
|
def capture_payment_id
|
|
43
44
|
scalar("capture_payment_id")
|
|
44
45
|
end
|
|
@@ -55,6 +56,7 @@ module DPay
|
|
|
55
56
|
type == IpnType::TRANSFER
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
# @deprecated dpay no longer sends capture IPNs - use the "payment.captured" webhook event.
|
|
58
60
|
def capture?
|
|
59
61
|
type == IpnType::CAPTURE
|
|
60
62
|
end
|
data/lib/dpay/ipn/ipn_type.rb
CHANGED
|
@@ -8,10 +8,10 @@ module DPay
|
|
|
8
8
|
|
|
9
9
|
def register(request)
|
|
10
10
|
body = request.to_body(@api.service)
|
|
11
|
-
body["
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
11
|
+
fields = [body["value"], body["url_success"], body["url_fail"], body["url_ipn"]]
|
|
12
|
+
# A recurring charge binds the checksum to the customer's alias
|
|
13
|
+
fields << body["recurring_alias"] unless body["recurring_alias"].nil?
|
|
14
|
+
body["checksum"] = @api.checksum.secret_second(@api.service, fields)
|
|
15
15
|
|
|
16
16
|
data = @api.post_json(Internal::BaseUrls::API_PAYMENTS, "/api/v1_0/payments/register", body)
|
|
17
17
|
raise PaymentRejectedError.from_api(data) if rejected?(data)
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "resolv"
|
|
4
|
+
|
|
3
5
|
module DPay
|
|
4
6
|
class RegisterPaymentRequest
|
|
5
7
|
BLIK_CODE = /\A\d{6}\z/
|
|
6
8
|
PARTNER_PLATFORM = /\A[A-Z0-9]{1,64}\z/
|
|
7
9
|
HTTP_URL = %r{\Ahttps?://[^\s]+\z}
|
|
10
|
+
CONTROL_CHARACTERS = /[\x00-\x1F\x7F]/
|
|
11
|
+
RECURRING_ALIAS_MAX = 128
|
|
12
|
+
REFERENCE_MAX = 64
|
|
8
13
|
TOGGLES = { "creditcard" => :@credit_card, "paysafecard" => :@paysafecard, "blik" => :@blik,
|
|
9
14
|
"installment" => :@installment, "paypal" => :@paypal, "nobanks" => :@no_banks }.freeze
|
|
15
|
+
RECURRING_CONFLICTS = %w[blik_alias register_blik_alias register_card_recurring card_recurring_alias].freeze
|
|
10
16
|
|
|
11
17
|
attr_reader :amount
|
|
12
18
|
|
|
@@ -21,6 +27,7 @@ module DPay
|
|
|
21
27
|
@transaction_type = transaction_type
|
|
22
28
|
@urls = urls
|
|
23
29
|
@optional = {}
|
|
30
|
+
@recurring_registration = nil
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
def with_description(description)
|
|
@@ -103,9 +110,10 @@ module DPay
|
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def with_blik_alias(alias_value, user_agent, user_ip)
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
raise InvalidArgumentError,
|
|
113
|
+
if %w[blik_code register_blik_alias recurring_alias].any? { |key| @optional.key?(key) } ||
|
|
114
|
+
!@recurring_registration.nil?
|
|
115
|
+
raise InvalidArgumentError,
|
|
116
|
+
"blik_alias cannot be combined with blik_code, alias registration or recurring payments"
|
|
109
117
|
end
|
|
110
118
|
|
|
111
119
|
set("blik_alias", alias_value)
|
|
@@ -121,12 +129,47 @@ module DPay
|
|
|
121
129
|
set("register_blik_alias", registration.to_h)
|
|
122
130
|
end
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
# Registers a recurring payment together with this payment. Requires the customer's BLIK code (with_blik_code)
|
|
133
|
+
# and transactionType "transfers"; the amount may be 0 (consent only) or an initial fee.
|
|
134
|
+
def with_recurring_registration(registration)
|
|
135
|
+
@recurring_registration = registration
|
|
136
|
+
self
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Charges a registered recurring payment server-to-server (no BLIK code): transactionType "transfers", amount
|
|
140
|
+
# above 0. The alias is appended to the checksum, binding the charge to that customer.
|
|
141
|
+
def with_recurring_alias(alias_value)
|
|
142
|
+
unless alias_value.is_a?(String) && !alias_value.empty? && alias_value.bytesize <= RECURRING_ALIAS_MAX
|
|
143
|
+
raise InvalidArgumentError, "Recurring alias must be 1-128 characters"
|
|
127
144
|
end
|
|
128
145
|
|
|
129
|
-
set("
|
|
146
|
+
set("recurring_alias", alias_value)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Payer's user agent and IP for a recurring charge (optional there; BLIK code and alias payments set them in
|
|
150
|
+
# with_blik_code / with_blik_alias).
|
|
151
|
+
def with_client_context(user_agent, user_ip)
|
|
152
|
+
raise InvalidArgumentError, %(Invalid user IP "#{user_ip}") unless ip_address?(user_ip)
|
|
153
|
+
|
|
154
|
+
set("user_agent", user_agent)
|
|
155
|
+
set("user_ip", user_ip)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Sends this payment's events (and later events of its refunds and recurring payment) also to this URL, signed
|
|
159
|
+
# with the service's webhook secret. Not part of the checksum.
|
|
160
|
+
def with_webhook(webhook)
|
|
161
|
+
webhook.assert_events_allowed(WebhookEventType::PAYMENT_REGISTRATION, "a payment registration")
|
|
162
|
+
set("webhook", webhook.to_h)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Your reference of the payment (max 64 characters), returned as references.merchant in webhooks.
|
|
166
|
+
def with_reference(reference)
|
|
167
|
+
trimmed = reference.is_a?(String) ? Internal::PHP.trim(reference) : ""
|
|
168
|
+
if trimmed.empty? || trimmed.length > REFERENCE_MAX || CONTROL_CHARACTERS.match?(trimmed)
|
|
169
|
+
raise InvalidArgumentError, "Reference must be 1-64 characters without control characters"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
set("reference", trimmed)
|
|
130
173
|
end
|
|
131
174
|
|
|
132
175
|
def with_alias_ipn_url(url)
|
|
@@ -194,14 +237,17 @@ module DPay
|
|
|
194
237
|
end
|
|
195
238
|
|
|
196
239
|
def to_body(service)
|
|
240
|
+
assert_recurring_combination
|
|
241
|
+
|
|
242
|
+
# @type var body: Hash[String, untyped]
|
|
197
243
|
body = {
|
|
198
244
|
"service" => service,
|
|
199
245
|
"value" => @amount.to_decimal,
|
|
200
246
|
"transactionType" => @transaction_type,
|
|
201
247
|
"url_success" => @urls.success,
|
|
202
|
-
"url_fail" => @urls.fail
|
|
203
|
-
"url_ipn" => @urls.ipn
|
|
248
|
+
"url_fail" => @urls.fail
|
|
204
249
|
}
|
|
250
|
+
body["url_ipn"] = @urls.ipn unless @urls.ipn.nil?
|
|
205
251
|
|
|
206
252
|
append(body, "description")
|
|
207
253
|
append(body, "custom")
|
|
@@ -209,10 +255,12 @@ module DPay
|
|
|
209
255
|
append(body, "accept_tos")
|
|
210
256
|
append(body, "channel")
|
|
211
257
|
append_toggles(body)
|
|
212
|
-
%w[phone_number currency_code partner_platform user_agent user_ip blik_code blik_alias
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
258
|
+
%w[phone_number currency_code partner_platform user_agent user_ip blik_code blik_alias
|
|
259
|
+
register_blik_alias].each { |key| append(body, key) }
|
|
260
|
+
body["recurring_registration"] = @recurring_registration.to_h unless @recurring_registration.nil?
|
|
261
|
+
%w[recurring_alias alias_ipn_url no_delay register_card_recurring card_recurring_alias authorize_only
|
|
262
|
+
card_recurring_operation payout billing_address shipping_address device_info products efaktura invoice
|
|
263
|
+
webhook reference].each { |key| append(body, key) }
|
|
216
264
|
|
|
217
265
|
body
|
|
218
266
|
end
|
|
@@ -242,5 +290,36 @@ module DPay
|
|
|
242
290
|
body[key] = flag ? 1 : 0 unless flag.nil?
|
|
243
291
|
end
|
|
244
292
|
end
|
|
293
|
+
|
|
294
|
+
def ip_address?(value)
|
|
295
|
+
value.is_a?(String) && !value.include?("%") &&
|
|
296
|
+
(Resolv::IPv4::Regex.match?(value) || Resolv::IPv6::Regex.match?(value))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Checked when the body is built, like the API: a registration needs the BLIK code and excludes "channel",
|
|
300
|
+
# a charge needs an amount above 0 and excludes "blik_code"; both exclude the other alias kinds.
|
|
301
|
+
def assert_recurring_combination
|
|
302
|
+
charge = !@optional["recurring_alias"].nil?
|
|
303
|
+
return if @recurring_registration.nil? && !charge
|
|
304
|
+
|
|
305
|
+
if !@recurring_registration.nil? && charge
|
|
306
|
+
raise InvalidArgumentError, "recurring_registration cannot be combined with recurring_alias"
|
|
307
|
+
end
|
|
308
|
+
unless @transaction_type == TransactionType::TRANSFERS
|
|
309
|
+
raise InvalidArgumentError, %(Recurring payments require transactionType "transfers")
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
assert_recurring_requirements(charge)
|
|
313
|
+
conflict = (RECURRING_CONFLICTS + [charge ? "blik_code" : "channel"]).find { |key| !@optional[key].nil? }
|
|
314
|
+
raise InvalidArgumentError, "#{conflict} cannot be combined with a recurring payment" unless conflict.nil?
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def assert_recurring_requirements(charge)
|
|
318
|
+
if charge
|
|
319
|
+
raise InvalidArgumentError, "A recurring charge requires an amount above 0" unless @amount.minor.positive?
|
|
320
|
+
elsif @optional["blik_code"].nil?
|
|
321
|
+
raise InvalidArgumentError, "recurring_registration requires the customer's BLIK code (with_blik_code)"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
245
324
|
end
|
|
246
325
|
end
|