Przejdź do głównej zawartości

Java SDK

Dostępne - wersja 0.1.0

Oficjalna biblioteka Javy 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 long), a odpowiedzi API mapowane na typowane obiekty. Wyjątki są typu unchecked - kompilator nie zmusza do try/catch.


Wymagania

  • Java 11 lub nowsza (testowane do Javy 25)
  • Maven lub Gradle

Biblioteka nie ma żadnych zależności runtime - transport HTTP korzysta z java.net.http, a kryptografia z javax.crypto. SDK jest przeznaczone dla backendu (serwera); nie jest wspierane na Androidzie.


Instalacja

Gradle:

implementation 'pl.dpay:dpay-java-sdk:0.1.0'

Maven:

<dependency>
<groupId>pl.dpay</groupId>
<artifactId>dpay-java-sdk</artifactId>
<version>0.1.0</version>
</dependency>

Pakiet jest publikowany na Maven Central, a kod źródłowy dostępny na GitHub.


Konfiguracja

Najprościej podając dane z Panelu dpay.pl:

import pl.dpay.sdk.DPayClient;

DPayClient dpay = new DPayClient("nazwa_serwisu", "twoj_secret_hash");

Pełną kontrolę daje DPayConfig:

import java.time.Duration;
import pl.dpay.sdk.ApiHost;
import pl.dpay.sdk.DPayClient;
import pl.dpay.sdk.DPayConfig;

DPayClient dpay = new DPayClient(
DPayConfig.create("nazwa_serwisu", "twoj_secret_hash")
.withTimeout(Duration.ofSeconds(30))
.withBaseUrl(ApiHost.PANEL, "https://panel.dpay.pl"));
OpcjaTypOpis
serviceStringNazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane)
secretHashStringKlucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane)
withTimeoutDurationTimeout żądań HTTP (domyślnie 30 s)
withHttpClientHttpClientWłasny transport (proxy, retry, testy)
withBaseUrlApiHost, StringNadpisanie hosta API (API_PAYMENTS, PANEL, GATEWAY)

Klient jest niemutowalny i bezpieczny wątkowo. Serwisy udostępnia jako metody:

SerwisZakres
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 pl.dpay.sdk.Money, wewnętrznie przechowujący grosze jako long. 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.

import pl.dpay.sdk.Currency;
import pl.dpay.sdk.Money;

Money.pln(1050); // 10.50 PLN
Money.of(500, Currency.EUR); // 5.00 EUR
Money.fromDecimal("10.50", Currency.PLN);

money.getMinor(); // 1050 (grosze)
money.toDecimal(); // "10.50"

Rejestracja płatności

Żądanie budujesz obiektem RegisterPaymentRequest - pola wymagane w create(), opcjonalne przez settery withX(). Suma kontrolna i pole transactionType są dokładane automatycznie:

import pl.dpay.sdk.DPayClient;
import pl.dpay.sdk.Money;
import pl.dpay.sdk.payment.Payer;
import pl.dpay.sdk.payment.RegisterPaymentRequest;
import pl.dpay.sdk.payment.RegisteredPayment;
import pl.dpay.sdk.payment.ReturnUrls;
import pl.dpay.sdk.payment.TransactionType;

DPayClient dpay = new DPayClient("nazwa_serwisu", "twoj_secret_hash");

RegisteredPayment payment = dpay.payments().register(
RegisterPaymentRequest.create(
Money.pln(1050),
TransactionType.TRANSFERS,
new ReturnUrls(
"https://twojsklep.pl/sukces",
"https://twojsklep.pl/blad",
"https://twojsklep.pl/ipn"))
.withDescription("Zamówienie #1234")
.withCustom("order-1234")
.withPayer(Payer.create().withEmail("klient@example.com")));

if (payment.getRedirectUrl() != null) {
response.sendRedirect(payment.getRedirectUrl());
return;
}

if (payment.isPaid()) {
// płatność rozliczona inline (np. BLIK Level 0)
}

Typy transakcji (TransactionType): TRANSFERS, DCB_GATEWAY, CARD_AUTH, MB_WAY_DIRECT, BIZUM_DIRECT, BLIK_RECURRING, CARD_RECURRING.

Ważniejsze settery RegisterPaymentRequest

MetodaOpis
withDescription(String)Opis transakcji widoczny dla klienta
withCustom(String)Własne dane identyfikujące zamówienie (wracają w IPN)
withPayer(Payer)E-mail oraz imię i nazwisko klienta
withChannel(String)Płatność bezpośrednia wskazanym kanałem banku
withCreditCard(boolean) / withPaypal(boolean) / withPaysafecard(boolean) / withInstallment(boolean) / withBlik(boolean)Włączenie lub wyłączenie metod na bramce
withNoBanks(boolean)Ukrycie listy banków
withBlikCode(String code, String userAgent, String userIp)Płatność BLIK Level 0 (kod 6-cyfrowy)
withBlikAlias(String alias, String userAgent, String userIp)Płatność aliasem BLIK OneClick
withRegisterBlikAlias(BlikAliasRegistration)Rejestracja aliasu OneClick przy płatności
withCardRecurring(CardRecurringRegistration)Rejestracja mandatu card recurring
withCardRecurringAlias(String)Obciążenie zapisanej karty (MIT)
withPayout(PayoutInstruction)Instrukcja wypłaty 1:1
withEfaktura(InvoiceDetails)Płatność za eFakturę (KSeF)
withPhoneNumber(String phone, Currency currency)MB WAY

Obsługa IPN

Powiadomienia IPN weryfikujesz przez IpnVerifier.constructEvent() - nieprawidłowy podpis rzuca SignatureVerificationException:

import pl.dpay.sdk.exception.SignatureVerificationException;
import pl.dpay.sdk.ipn.IpnEvent;
import pl.dpay.sdk.ipn.IpnVerifier;

try {
IpnEvent event = IpnVerifier.constructEvent(requestBody, "twoj_secret_hash");

if (event.isTransfer()) {
markOrderAsPaid(event.getId(), event.getAmount());
}

if (event.isCapture()) {
markOrderAsCaptured(event.getId(), event.getAmount(), event.getCapturePaymentId());
}

response.setStatus(200);
response.getWriter().print(IpnEvent.ACK);
} catch (SignatureVerificationException e) {
response.setStatus(400);
response.getWriter().print("Invalid signature");
}
Odpowiedź musi być dokładnie OK

dpay.pl uznaje IPN za dostarczony wyłącznie, gdy treść odpowiedzi to dokładnie OK (stała IpnEvent.ACK). Kod HTTP nie jest sprawdzany. Upewnij się, że framework nie dokleja niczego do body.

informacja

Zawsze porównaj event.getAmount() (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

import pl.dpay.sdk.payment.Transaction;

Transaction transaction = dpay.payments().details("identyfikator-transakcji");

transaction.getStatus(); // TransactionStatus: PAID, CREATED, PROCESSING, EXPIRED, CAPTURED, UNKNOWN
transaction.getStatusValue(); // surowy string z API
transaction.isPaid(); // true dla paid i captured
transaction.getValue().toDecimal(); // "29.99"
transaction.getRefundedAmount(); // Money
transaction.getAvailableRefundAmount(); // Money
transaction.isFullyRefunded(); // boolean
transaction.getRefunds(); // List<TransactionRefund>

Statusy z odpowiedzi API są enumami z wartością UNKNOWN - nieznana wartość nie psuje deserializacji, a oryginalny string zawsze dostępny przez getStatusValue().


Zwroty

import pl.dpay.sdk.Money;
import pl.dpay.sdk.refund.Refund;

// zwrot pełny
Refund refund = dpay.refunds().create("identyfikator-transakcji");

// zwrot częściowy z powodem
Refund refund = dpay.refunds().create("identyfikator-transakcji", Money.pln(500), "reklamacja");

refund.isAccepted();

Przed zwrotem możesz sprawdzić jego dostępność - metoda zwraca wynik biznesowy (nie rzuca wyjątku), także gdy zwrot jest niemożliwy:

import pl.dpay.sdk.refund.RefundAvailability;

RefundAvailability availability =
dpay.refunds().checkAvailability("identyfikator-transakcji", Money.pln(500));

if (!availability.isAvailable()) {
availability.getMessage(); // np. "Transakcja nie została opłacona"
availability.getHttpStatus(); // stabilny kod przyczyny, patrz tabela
}
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

import java.util.List;
import pl.dpay.sdk.bank.Bank;

List<Bank> banks = dpay.banks().all(); // wszystkie banki dpay.pl
List<Bank> banks = dpay.banks().forService(); // banki dostępne dla Twojego serwisu

banks.get(0).getId();
banks.get(0).getName();

BLIK - aliasy OneClick i Recurring

import pl.dpay.sdk.blik.BlikAlias;
import pl.dpay.sdk.blik.BlikRecurringStatus;

BlikAlias alias = dpay.blik().alias("DPAY.UID.123456.abc12345");
alias.isActive();
alias.getApps();

dpay.blik().unregisterAlias("DPAY.UID.123456.abc12345");

BlikRecurringStatus status = dpay.blik().recurringStatus("PAYID-...");
status.getRegistration();

Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (withBlikCode + withRegisterBlikAlias, płatność przez withBlikAlias).


Karty server-to-server

Dane karty szyfrujesz kluczem RSA pobieranym przed każdą próbą płatności:

import pl.dpay.sdk.card.CardData;
import pl.dpay.sdk.card.CardEncryptor;
import pl.dpay.sdk.card.CardPaymentRequest;
import pl.dpay.sdk.card.CardPaymentResult;
import pl.dpay.sdk.card.DccDecision;
import pl.dpay.sdk.payment.DeviceInfo;

String publicKey = dpay.cards().publicKey();

String encrypted = new CardEncryptor().encrypt(
new CardData("4111111111111111", "123", "12/30"),
transactionId,
publicKey);

DeviceInfo deviceInfo = DeviceInfo.create(/* dane przeglądarki płatnika */);

CardPaymentResult result = dpay.cards().payOtp(transactionId,
CardPaymentRequest.create(deviceInfo)
.withEncryptedCardData(encrypted)
.withCardHolder("Jan", "Kowalski")
.withChannelId(31));

if (result.isSuccess()) {
// płatność przechwycona
} else if (result.requiresThreeDsForm()) {
String html = result.getThreeDsFormHtml(); // wyrenderuj w przeglądarce płatnika
} else if (result.hasDccOffer()) {
result.getDccOffer(); // pokaż ofertę DCC płatnikowi
// decyzję odeślij ponownym payOtp z .withDccDecision(DccDecision.ACCEPT)
}

Dostępne są także: preAuth() (pre-autoryzacja), capture() i cancel() (przechwycenie i anulowanie), googlePay() oraz applePay(). Odrzucenie płatności przy HTTP 200 rzuca CardPaymentException z kodem błędu.

informacja

Karty S2S wymagają zgodności PCI-DSS po stronie merchanta. Szczegóły przepływu znajdziesz w dokumentacji kart S2S.


Wypłaty 1:1

import pl.dpay.sdk.payout.PayoutDetails;

PayoutDetails details = dpay.payouts().details(12345);

details.isProcessed();
details.getNet().toDecimal();
details.getReceiver();

Obsługa błędów

Wszystkie wyjątki SDK dziedziczą po pl.dpay.sdk.exception.DPayException i są typu unchecked - kompilator nie wymusza try/catch, łapiesz tam, gdzie faktycznie obsłużysz błąd:

WyjątekKiedy
TransportExceptionBłąd sieci - status płatności nieznany, użyj payments().details()
AuthenticationException401 - błędny checksum lub secret hash
InvalidRequestException400/422 - błędy walidacji (getFieldErrors())
NotFoundException404 - zasób nie istnieje
RateLimitException429 - limit żądań (getRetryAfter())
PaymentRejectedExceptionRejestracja odrzucona (np. błędny kod BLIK; getTransactionId())
CardPaymentExceptionOdrzucenie płatności kartowej (getErrorCode())
SignatureVerificationExceptionNieprawidłowy podpis IPN
CardEncryptionExceptionBłąd szyfrowania danych karty
ApiServerException5xx lub niepoprawna odpowiedź API
import pl.dpay.sdk.exception.ApiException;
import pl.dpay.sdk.exception.InvalidRequestException;
import pl.dpay.sdk.exception.TransportException;

try {
RegisteredPayment payment = dpay.payments().register(request);
} catch (InvalidRequestException e) {
e.getFieldErrors(); // Map<String, List<String>>
} catch (ApiException e) {
e.getHttpStatus();
e.getErrorCode();
} catch (TransportException e) {
// nie wiadomo, czy żądanie dotarło - zweryfikuj przez payments().details()
}

Niepoprawny argument przekazany do SDK (np. błędny format kwoty) rzuca IllegalArgumentException.


Testowanie integracji

MockHttpClient z pakietu pl.dpay.sdk.testing pozwala testować integrację bez sieci:

import pl.dpay.sdk.DPayClient;
import pl.dpay.sdk.DPayConfig;
import pl.dpay.sdk.testing.MockHttpClient;

MockHttpClient transport = new MockHttpClient();
transport.queueJson(200, "{\"transactionId\":\"tx-1\",\"msg\":\"https://secure.dpay.pl/pay/1\"}");

DPayClient dpay = new DPayClient(
DPayConfig.create("test", "test").withHttpClient(transport));

RegisteredPayment payment = dpay.payments().register(request);
// transport.getLastRequestBody() zawiera dokładne wysłane body

Więcej informacji