Ruby SDK
✅ Dostępne - wersja 0.1.0
Oficjalna biblioteka Ruby do integracji z API dpay.pl. Automatyzuje generowanie sum kontrolnych, weryfikację powiadomień IPN oraz wywołania API: rejestrację płatności, szczegóły transakcji, zwroty, listę banków, aliasy BLIK, płatności kartowe server-to-server i wypłaty 1:1. Kwoty są reprezentowane obiektem Money (grosze jako int), a odpowiedzi API mapowane na typowane obiekty. Zero zależności runtime, sygnatury typów RBS w komplecie.
Wymagania
- Ruby 3.1 lub nowszy (testowane do Ruby 4.0)
- Zero zależności runtime - SDK korzysta wyłącznie z biblioteki standardowej (
net/http,openssl,json) - W komplecie sygnatury typów RBS (
sig/) do weryfikacji typów (Steep) i podpowiedzi w edytorze
Instalacja
gem install dpay
Albo w pliku Gemfile:
gem "dpay"
Gem jest publikowany na RubyGems, a kod źródłowy dostępny na GitHub.
Konfiguracja
Utwórz instancję klienta DPay::Client, podając dane z Panelu dpay.pl:
require "dpay"
dpay = DPay::Client.new(
service: "nazwa_serwisu",
secret_hash: "twoj_secret_hash",
)
| Opcja | Typ | Opis |
|---|---|---|
service | String | Nazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane) |
secret_hash | String | Klucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane) |
timeout | Integer | Timeout żądań HTTP w sekundach (domyślnie 30) |
http_client | DPay::HTTP::Client | Własny klient HTTP (testy, proxy) |
base_urls | Hash | Nadpisanie hostów API (klucze: :api_payments, :panel, :gateway) |
Klient udostępnia serwisy jako metody:
| Serwis | Zakres |
|---|---|
dpay.payments | Rejestracja płatności, szczegóły transakcji |
dpay.refunds | Zwroty i sprawdzanie dostępności zwrotu |
dpay.banks | Lista banków pay-by-link |
dpay.blik | Aliasy BLIK OneClick i Recurring |
dpay.cards | Płatności kartowe server-to-server |
dpay.payouts | Szczegóły wypłat 1:1 |
Kwoty - obiekt Money
Wszystkie kwoty w SDK to obiekt DPay::Money, wewnętrznie przechowujący grosze jako int. SDK sam dba o właściwy format kwoty dla każdego endpointu (kwota dziesiętna albo grosze), więc nie musisz pamiętać, który endpoint oczekuje którego formatu.
DPay::Money.pln(1050) # 10.50 PLN
DPay::Money.of(500, "EUR") # 5.00 EUR
DPay::Money.from_decimal("10.50", "PLN")
money.minor # 1050 (grosze)
money.to_decimal # "10.50"
Money jest niemutowalny i porównywalny - dwa obiekty o tej samej kwocie i walucie są sobie równe.
Rejestracja płatności
Żądanie budujesz obiektem RegisterPaymentRequest - pola wymagane w create, opcjonalne przez łańcuch setterów with_*. Suma kontrolna i pole transactionType są dokładane automatycznie:
require "dpay"
dpay = DPay::Client.new(
service: "nazwa_serwisu",
secret_hash: "twoj_secret_hash",
)
payment = dpay.payments.register(
DPay::RegisterPaymentRequest.create(
DPay::Money.pln(1050),
DPay::TransactionType::TRANSFERS,
DPay::ReturnUrls.new(
"https://twojsklep.pl/sukces",
"https://twojsklep.pl/blad",
"https://twojsklep.pl/ipn",
),
)
.with_description("Zamówienie #1234")
.with_custom("order-1234")
.with_payer(DPay::Payer.create.with_email("klient@example.com")),
)
redirect_to payment.redirect_url if payment.redirect_url
# płatność rozliczona inline (np. BLIK Level 0)
mark_order_as_paid if payment.paid?
Typy transakcji (TransactionType): TRANSFERS, DCB_GATEWAY, CARD_AUTH, MB_WAY_DIRECT, BIZUM_DIRECT, BLIK_RECURRING, CARD_RECURRING.
Ważniejsze settery RegisterPaymentRequest
| Metoda | Opis |
|---|---|
with_description(String) | Opis transakcji widoczny dla klienta |
with_custom(String) | Własne dane identyfikujące zamówienie (wracają w IPN) |
with_payer(Payer) | E-mail oraz imię i nazwisko klienta |
with_channel(String) | Płatność bezpośrednia wskazanym kanałem banku |
with_credit_card(bool) / with_paypal(bool) / with_paysafecard(bool) / with_installment(bool) / with_blik(bool) | Włączenie lub wyłączenie metod na bramce |
with_no_banks(bool) | Ukrycie listy banków |
with_blik_code(code, user_agent, user_ip) | Płatność BLIK Level 0 (kod 6-cyfrowy) |
with_blik_alias(alias_value, user_agent, user_ip) | Płatność aliasem BLIK OneClick |
with_register_blik_alias(BlikAliasRegistration) | Rejestracja aliasu OneClick przy płatności |
with_card_recurring(CardRecurringRegistration) | Rejestracja mandatu card recurring |
with_card_recurring_alias(String) | Obciążenie zapisanej karty (MIT) |
with_payout(PayoutInstruction) | Instrukcja wypłaty 1:1 |
with_efaktura(InvoiceDetails) | Płatność za eFakturę (KSeF) |
with_phone_number(phone, currency) | MB WAY |
Obsługa IPN
Powiadomienia IPN weryfikujesz przez IpnVerifier.construct_event - nieprawidłowy podpis rzuca SignatureVerificationError:
begin
event = DPay::IpnVerifier.construct_event(request.raw_post, "twoj_secret_hash")
rescue DPay::SignatureVerificationError
return render plain: "Invalid signature", status: :bad_request
end
mark_order_as_paid(event.id, event.amount) if event.transfer?
mark_order_as_captured(event.id, event.amount, event.capture_payment_id) if event.capture?
render plain: DPay::IpnEvent::ACK
OKdpay.pl uznaje IPN za dostarczony wyłącznie, gdy treść odpowiedzi to dokładnie OK (stała DPay::IpnEvent::ACK). Kod HTTP nie jest sprawdzany. Upewnij się, że framework nie dokleja niczego do body.
Zawsze porównaj event.amount (string, np. "10.00") z kwotą zamówienia w Twojej bazie i przetwarzaj każdą transakcję tylko raz (IPN może przyjść wielokrotnie).
Szczegóły transakcji
transaction = dpay.payments.details("identyfikator-transakcji")
transaction.status # "paid", "created", "processing", "expired", "captured"
transaction.paid? # true dla paid i captured
transaction.value.to_decimal # "29.99"
transaction.refunded_amount # DPay::Money
transaction.available_refund_amount # DPay::Money
transaction.fully_refunded? # bool
transaction.refunds # Array<TransactionRefund>
Zwroty
# zwrot pełny
refund = dpay.refunds.create("identyfikator-transakcji")
# zwrot częściowy z powodem
refund = dpay.refunds.create("identyfikator-transakcji", DPay::Money.pln(500), "reklamacja")
refund.accepted?
Przed zwrotem możesz sprawdzić jego dostępność - metoda zwraca wynik biznesowy (nie rzuca wyjątku), także gdy zwrot jest niemożliwy:
availability = dpay.refunds.check_availability("identyfikator-transakcji", DPay::Money.pln(500))
unless availability.available?
availability.message # np. "Transakcja nie została opłacona"
availability.http_status # stabilny kod przyczyny, patrz tabela
end
| Kod HTTP | Przyczyna odmowy |
|---|---|
400 | Kwota przekracza dostępną kwotę zwrotu |
401 | Kanał płatności nie obsługuje zwrotów |
402 | Transakcja nie została opłacona |
406 | Niewystarczające saldo na pokrycie zwrotu |
409 | Wniosek o zwrot został już złożony |
410 | Transakcja została już zwrócona |
411 | Transakcje obciążeniowe nie podlegają zwrotom |
Banki
banks = dpay.banks.all # wszystkie banki dpay.pl
banks = dpay.banks.for_service # banki dostępne dla Twojego serwisu
banks.first.id
banks.first.name
BLIK - aliasy OneClick i Recurring
blik_alias = dpay.blik.alias("DPAY.UID.123456.abc12345")
blik_alias.active?
blik_alias.apps
dpay.blik.unregister_alias("DPAY.UID.123456.abc12345")
status = dpay.blik.recurring_status("PAYID-...")
status.registration
Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (with_blik_code + with_register_blik_alias, płatność przez with_blik_alias).
Karty server-to-server
Dane karty szyfrujesz kluczem RSA pobieranym przed każdą próbą płatności:
public_key = dpay.cards.public_key
encrypted = DPay::CardEncryptor.new.encrypt(
DPay::CardData.new("4111111111111111", "123", "12/30"),
transaction_id,
public_key,
)
device_info = DPay::DeviceInfo.create(...) # dane przeglądarki płatnika
result = dpay.cards.pay_otp(
transaction_id,
DPay::CardPaymentRequest.create(device_info)
.with_encrypted_card_data(encrypted)
.with_card_holder("Jan", "Kowalski")
.with_channel_id(31),
)
if result.success?
# płatność przechwycona
elsif result.three_ds_form?
html = result.three_ds_form_html # wyrenderuj w przeglądarce płatnika
elsif result.dcc_offer?
offer = result.dcc_offer # pokaż ofertę DCC płatnikowi
# decyzję odeślij ponownym pay_otp z .with_dcc_decision(DPay::DccDecision::ACCEPT)
end
Dostępne są także: pre_auth (pre-autoryzacja), capture i cancel (przechwycenie i anulowanie), google_pay oraz apple_pay. Odrzucenie płatności przy HTTP 200 rzuca CardPaymentError z kodem błędu.
Szyfrowanie RSA korzysta ze standardowej biblioteki OpenSSL, więc karty S2S nie wymagają żadnej dodatkowej zależności kryptograficznej.
Karty S2S wymagają zgodności PCI-DSS po stronie merchanta. Szczegóły przepływu znajdziesz w dokumentacji kart S2S.
Wypłaty 1:1
details = dpay.payouts.details(12345)
details.processed?
details.net.to_decimal
details.receiver
Obsługa błędów
Wszystkie wyjątki SDK można złapać przez marker DPay::Error:
| Wyjątek | Kiedy |
|---|---|
DPay::TransportError | Błąd sieci - status płatności nieznany, użyj payments.details |
DPay::AuthenticationError | 401 - błędny checksum lub secret hash |
DPay::InvalidRequestError | 400/422 - błędy walidacji (field_errors) |
DPay::AccessDeniedError | 403 - brak uprawnień do operacji |
DPay::NotFoundError | 404 - zasób nie istnieje |
DPay::RateLimitError | 429 - limit żądań (retry_after) |
DPay::PaymentRejectedError | Rejestracja odrzucona (np. błędny kod BLIK; transaction_id) |
DPay::CardPaymentError | Odrzucenie płatności kartowej (error_code) |
DPay::SignatureVerificationError | Nieprawidłowy podpis IPN |
DPay::ApiServerError | 5xx lub niepoprawna odpowiedź API |
DPay::InvalidArgumentError | Niepoprawny argument (dziedziczy także po ArgumentError) |
begin
payment = dpay.payments.register(request)
rescue DPay::InvalidRequestError => error
error.field_errors # Hash z polami i komunikatami walidacji
rescue DPay::ApiError => error
error.http_status
error.error_code
rescue DPay::TransportError
# nie wiadomo, czy żądanie dotarło - zweryfikuj przez payments.details
end
Testowanie integracji
SDK zawiera transport testowy, który kolejkuje odpowiedzi i nagrywa wysłane żądania, więc testy nie wymagają dostępu do sieci:
require "dpay"
require "dpay/testing"
transport = DPay::Testing::MockHttpClient.new
transport.queue_json(200, { "transactionId" => "tx-1", "msg" => "https://secure.dpay.pl/pay/1" })
dpay = DPay::Client.new(service: "test", secret_hash: "test", http_client: transport)
payment = dpay.payments.register(request)
transport.last_request_body["value"] # "10.50"