Przejdź do głównej zawartości

Go SDK

Dostępne - wersja 0.1.0

Oficjalna biblioteka Go 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 reprezentuje typ Money (grosze jako int64), a odpowiedzi API mapowane są na typowane struktury. Każda metoda przyjmuje context.Context, więc anulowanie i limity czasu działają tak, jak w reszcie Twojego kodu.


Wymagania

  • Go 1.22 lub nowszy (testowane do Go 1.26)
  • Zero zależności runtime - SDK korzysta wyłącznie z biblioteki standardowej

Instalacja

go get github.com/dpayglobal/dpay-go-sdk
import dpay "github.com/dpayglobal/dpay-go-sdk"

Kod źródłowy dostępny na GitHub, dokumentacja API na pkg.go.dev.


Konfiguracja

Utwórz klienta, podając dane z Panelu dpay.pl:

client, err := dpay.New("nazwa_serwisu", "twoj_secret_hash")
if err != nil {
return err
}

Nazwa serwisu i secret hash są wymagane pozycyjnie, reszta to opcje:

OpcjaTypOpis
dpay.New(service, secretHash)string, stringNazwa serwisu (Punktu Płatności) i klucz Hash z panel.dpay.pl (wymagane)
dpay.WithTimeouttime.DurationTimeout domyślnego klienta HTTP (domyślnie 30s)
dpay.WithHTTPClientdpay.HTTPDoerWłasny transport - proxy, retry, instrumentacja, testy
dpay.WithBaseURLsdpay.BaseURLsNadpisanie hostów API (pola APIPayments, Panel, Gateway)

Klient udostępnia serwisy jako pola:

SerwisZakres
client.PaymentsRejestracja płatności, szczegóły transakcji
client.RefundsZwroty i sprawdzanie dostępności zwrotu
client.BanksLista banków pay-by-link
client.BlikAliasy BLIK OneClick i Recurring
client.CardsPłatności kartowe server-to-server
client.PayoutsSzczegóły wypłat 1:1

Klient jest bezpieczny do współdzielenia między goroutine'ami - utwórz go raz przy starcie aplikacji.

Przekierowania

Domyślny klient HTTP nie podąża za przekierowaniami - odpowiedź 302 wraca jako wynik, a nie jako strona, pod którą prowadzi. Jeśli podajesz własny *http.Client przez WithHTTPClient, ustaw CheckRedirect tak samo:

&http.Client{
Timeout: 30 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}

Pola opcjonalne - wskaźniki

Pola opcjonalne w żądaniach są wskaźnikami. nil pomija pole, a wskaźnik na wartość wysyła ją jawnie - również wtedy, gdy to false lub zero. To rozróżnienie ma znaczenie dla API, dlatego SDK nigdzie nie używa omitempty.

Description: dpay.String("Zamówienie #1234") // wysyła pole
AcceptTos: dpay.Bool(false) // wysyła false
Channel: nil // pomija pole

Do budowania wskaźników służą dpay.String, dpay.Bool, dpay.Int i dpay.Int64.


Kwoty - typ Money

Wszystkie kwoty w SDK to typ dpay.Money, wewnętrznie przechowujący grosze jako int64. SDK sam dba o właściwy format kwoty dla każdego endpointu (kwota dziesiętna, liczba zmiennoprzecinkowa albo grosze), więc nie musisz pamiętać, który endpoint oczekuje którego formatu.

dpay.PLN(1050) // 10.50 PLN
money, err := dpay.NewMoney(500, dpay.CurrencyEUR)
money, err := dpay.ParseMoney("10.50", dpay.CurrencyPLN)

money.Minor() // 1050 (grosze)
money.String() // "10.50"
money.Currency() // dpay.CurrencyPLN

Money jest typem wartościowym i porównywalnym - Equal zwraca true tylko dla tej samej kwoty i waluty. Kwoty nie są liczone na liczbach zmiennoprzecinkowych.


Rejestracja płatności

Żądanie to zwykła struktura inicjalizowana literałem. Suma kontrolna dokładana jest automatycznie:

payment, err := client.Payments.Register(ctx, &dpay.RegisterPaymentRequest{
Amount: dpay.PLN(1050),
TransactionType: dpay.TransactionTypeTransfers,
URLs: dpay.ReturnURLs{
Success: "https://twojsklep.pl/sukces",
Fail: "https://twojsklep.pl/blad",
IPN: "https://twojsklep.pl/ipn",
},
Description: dpay.String("Zamówienie #1234"),
Custom: dpay.String("order-1234"),
Payer: &dpay.Payer{
Email: dpay.String("klient@example.com"),
},
})
if err != nil {
return err
}

if url := payment.RedirectURL(); url != "" {
http.Redirect(w, r, url, http.StatusSeeOther)
return nil
}

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

Typy transakcji: TransactionTypeTransfers, TransactionTypeDCBGateway, TransactionTypeCardAuth, TransactionTypeMBWayDirect, TransactionTypeBizumDirect, TransactionTypeBlikRecurring, TransactionTypeCardRecurring.

Żądanie jest walidowane automatycznie przed wysłaniem. Możesz też sprawdzić je wcześniej - request.Validate() zwraca ten sam błąd, na przykład przy walidacji formularza.

Ważniejsze pola RegisterPaymentRequest

PoleOpis
DescriptionOpis transakcji widoczny dla klienta
CustomWłasne dane identyfikujące zamówienie (wracają w IPN)
PayerE-mail oraz imię i nazwisko klienta
ChannelPłatność bezpośrednia wskazanym kanałem banku
CreditCard, PayPal, Paysafecard, Installment, BlikWłączenie lub wyłączenie metod na bramce
NoBanksUkrycie listy banków
BlikCode + UserAgent + UserIPPłatność BLIK Level 0 (kod 6-cyfrowy)
BlikAlias + UserAgent + UserIPPłatność aliasem BLIK OneClick
RegisterBlikAliasRejestracja aliasu OneClick przy płatności
CardRecurringRejestracja mandatu card recurring
CardRecurringAliasObciążenie zapisanej karty (MIT)
PayoutInstrukcja wypłaty 1:1
Efaktura + InvoicePłatność za eFakturę (KSeF)
PhoneNumber + CurrencyCodeMB WAY
BillingAddress, ShippingAddress, ProductsObiekty wolnej formy - użyj dpay.NewFields(), aby zachować kolejność pól

Obsługa IPN

Powiadomienia weryfikujesz przez dpay.VerifyIPN - nieprawidłowy podpis zwraca błąd pasujący do dpay.ErrSignature:

func ipnHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}

event, err := dpay.VerifyIPN(body, "twoj_secret_hash")
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}

if event.IsTransfer() {
markOrderAsPaid(event.ID(), event.Amount())
}

if event.IsCapture() {
markOrderAsCaptured(event.ID(), event.Amount(), event.CapturePaymentID())
}

fmt.Fprint(w, dpay.IPNAck)
}
Odpowiedź musi być dokładnie OK

dpay.pl uznaje IPN za dostarczony wyłącznie, gdy treść odpowiedzi to dokładnie OK (stała dpay.IPNAck). Kod HTTP nie jest sprawdzany. Upewnij się, że framework nie dokleja niczego do body - fmt.Fprint nie dodaje znaku nowej linii, w przeciwieństwie do fmt.Fprintln.

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). Payload IPN nie niesie waluty, dlatego kwota jest stringiem, a nie obiektem Money.


Szczegóły transakcji

transaction, err := client.Payments.Details(ctx, "identyfikator-transakcji")

transaction.Status() // "paid", "created", "processing", "expired", "captured"
transaction.IsPaid() // true dla paid i captured
transaction.Value().String() // "29.99"
transaction.RefundedAmount() // Money
transaction.AvailableRefundAmount() // Money
transaction.IsFullyRefunded() // bool
transaction.Refunds() // []*dpay.TransactionRefund

Nieznane pola z odpowiedzi nie psują parsowania - cały payload dostępny jest przez transaction.Raw().


Zwroty

// zwrot pełny
refund, err := client.Refunds.Create(ctx, "identyfikator-transakcji")

// zwrot częściowy z powodem
refund, err := client.Refunds.Create(ctx, "identyfikator-transakcji",
dpay.WithRefundAmount(dpay.PLN(500)),
dpay.WithRefundReason("reklamacja"))

refund.IsAccepted()

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

availability, err := client.Refunds.CheckAvailability(ctx, "identyfikator-transakcji",
dpay.WithRefundAmount(dpay.PLN(500)))

if err == nil && !availability.IsAvailable() {
availability.Message() // np. "Transakcja nie została opłacona"
availability.HTTPStatus() // 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

banks, err := client.Banks.All(ctx) // wszystkie banki dpay.pl
banks, err := client.Banks.ForService(ctx) // banki dostępne dla Twojego serwisu

banks[0].ID()
banks[0].Name()

BLIK - aliasy OneClick i Recurring

alias, err := client.Blik.Alias(ctx, "DPAY.UID.123456.abc12345", dpay.BlikAliasTypeUID)
alias.IsActive()
alias.Apps()

err = client.Blik.UnregisterAlias(ctx, "DPAY.UID.123456.abc12345", dpay.BlikAliasTypeUID)

status, err := client.Blik.RecurringStatus(ctx, "PAYID-...")
status.Registration()

Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (pola BlikCode + RegisterBlikAlias, płatność przez BlikAlias).


Karty server-to-server

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

publicKey, err := client.Cards.PublicKey(ctx)

encrypted, err := dpay.EncryptCard(
dpay.CardData{PAN: "4111111111111111", CVV: "123", Expiry: "12/30"},
transactionID,
publicKey,
)

result, err := client.Cards.PayOTP(ctx, transactionID, &dpay.CardPaymentRequest{
DeviceInfo: deviceInfo, // dane przeglądarki płatnika
EncryptedCardData: dpay.String(encrypted),
CardHolderFirstName: dpay.String("Jan"),
CardHolderLastName: dpay.String("Kowalski"),
ChannelID: dpay.Int(31),
})

switch {
case result.IsSuccess():
// płatność przechwycona
case result.RequiresThreeDSForm():
html := result.ThreeDSFormHTML() // wyrenderuj w przeglądarce płatnika
case result.RequiresRedirect():
http.Redirect(w, r, result.RedirectURL(), http.StatusSeeOther)
case result.HasDCCOffer():
offer := result.DCCOffer() // pokaż ofertę DCC płatnikowi
// decyzję odeślij ponownym PayOTP z polem DCCDecision: dpay.DCCDecisionAccept
}

Dostępne są także: PreAuth (pre-autoryzacja), Capture i Cancel (przechwycenie i anulowanie), GooglePay oraz ApplePay. Odrzucenie płatności przy HTTP 200 zwraca błąd pasujący do dpay.ErrCardPayment, z kodem błędu w ErrorCode.

Przechwycenie pełnej kwoty wykonuje się przekazując nil zamiast kwoty:

client.Cards.Capture(ctx, transactionID, nil) // pełna kwota
client.Cards.Capture(ctx, transactionID, &amount) // kwota częściowa

Klucz publiczny jest rotowany - pobieraj go przed każdą próbą płatności, nie buforuj. Szyfrowanie RSA korzysta z pakietu crypto/rsa z biblioteki standardowej, więc karty S2S nie wymagają żadnej dodatkowej zależności.

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, err := client.Payouts.Details(ctx, 12345)

details.IsProcessed()
details.Net().String()
details.Receiver()

Obsługa błędów

SDK nie ma hierarchii wyjątków - rodzaj błędu rozpoznajesz przez errors.Is, a szczegóły odczytujesz przez errors.As:

payment, err := client.Payments.Register(ctx, request)

switch {
case errors.Is(err, dpay.ErrInvalidRequest):
var apiErr *dpay.APIError
errors.As(err, &apiErr)
log.Print(apiErr.FieldErrors) // map[string][]string

case errors.Is(err, dpay.ErrPaymentRejected):
var rejected *dpay.PaymentRejectedError
errors.As(err, &rejected)
log.Print(rejected.TransactionID, rejected.ErrorCode)

case errors.Is(err, dpay.ErrTransport):
// nie wiadomo, czy żądanie dotarło - zweryfikuj przez Payments.Details
}
SentinelKiedy
ErrTransportBłąd sieci - status płatności nieznany, użyj Payments.Details
ErrAuthentication401 - błędny checksum lub secret hash
ErrInvalidRequest400/422 - błędy walidacji (FieldErrors)
ErrAccessDenied403 - brak uprawnień do operacji
ErrNotFound404 - zasób nie istnieje
ErrRateLimit429 - limit żądań (*RateLimitError niesie RetryAfter)
ErrPaymentRejectedRejestracja odrzucona (np. błędny kod BLIK; TransactionID)
ErrCardPaymentOdrzucenie płatności kartowej (ErrorCode)
ErrSignatureNieprawidłowy podpis IPN
ErrServer5xx lub niepoprawna odpowiedź API
ErrInvalidArgumentNiepoprawny argument lub pole żądania
ErrAPIPasuje do każdego błędu zwróconego przez API

*APIError niesie HTTPStatus, ErrorCode, FieldErrors i RawBody - surową treść odpowiedzi, przydatną przy diagnostyce.


Testowanie integracji

SDK nie narzuca własnego mocka - wystarczy httptest z biblioteki standardowej:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"transactionId":"tx-1","msg":"https://secure.dpay.pl/pay/1"}`))
}))
defer server.Close()

client, _ := dpay.New("test", "test", dpay.WithBaseURLs(dpay.BaseURLs{
APIPayments: server.URL,
Panel: server.URL,
}))

Jeśli wolisz sprawdzać wysłane żądania bez podnoszenia serwera, zaimplementuj dpay.HTTPDoer - to jednometodowy interfejs Do(*http.Request) (*http.Response, error), który spełnia także *http.Client.


Więcej informacji