Przejdź do głównej zawartości

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",
)
OpcjaTypOpis
serviceStringNazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane)
secret_hashStringKlucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane)
timeoutIntegerTimeout żądań HTTP w sekundach (domyślnie 30)
http_clientDPay::HTTP::ClientWłasny klient HTTP (testy, proxy)
base_urlsHashNadpisanie hostów API (klucze: :api_payments, :panel, :gateway)

Klient udostępnia serwisy jako metody:

SerwisZakres
dpay.paymentsRejestracja płatności, szczegóły transakcji
dpay.refundsZwroty i sprawdzanie dostępności zwrotu
dpay.banksLista banków pay-by-link
dpay.blikAliasy BLIK OneClick i Recurring
dpay.cardsPłatności kartowe server-to-server
dpay.payoutsSzczegół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

MetodaOpis
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
Odpowiedź musi być dokładnie OK

dpay.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.

informacja

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 HTTPPrzyczyna odmowy
400Kwota przekracza dostępną kwotę zwrotu
401Kanał płatności nie obsługuje zwrotów
402Transakcja nie została opłacona
406Niewystarczające saldo na pokrycie zwrotu
409Wniosek o zwrot został już złożony
410Transakcja została już zwrócona
411Transakcje 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.

informacja

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ątekKiedy
DPay::TransportErrorBłąd sieci - status płatności nieznany, użyj payments.details
DPay::AuthenticationError401 - błędny checksum lub secret hash
DPay::InvalidRequestError400/422 - błędy walidacji (field_errors)
DPay::AccessDeniedError403 - brak uprawnień do operacji
DPay::NotFoundError404 - zasób nie istnieje
DPay::RateLimitError429 - limit żądań (retry_after)
DPay::PaymentRejectedErrorRejestracja odrzucona (np. błędny kod BLIK; transaction_id)
DPay::CardPaymentErrorOdrzucenie płatności kartowej (error_code)
DPay::SignatureVerificationErrorNieprawidłowy podpis IPN
DPay::ApiServerError5xx lub niepoprawna odpowiedź API
DPay::InvalidArgumentErrorNiepoprawny 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"

Więcej informacji