Opłacanie transakcji kartami płatniczymi cieszy się coraz większą popularnością na rynku e-commerce

Ten poradnik pozwoli zarówno zrozumieć mechanizm działania tego rodzaju płatności jak i pomoże w poprawnym wdrożeniu ich na własnej stronie. Ta metoda może również posłużyć do zapisania karty klienta, poprzez uzyskanie tokenu, który będzie później wiązał kartę kredytową z kontem sprzedawcy i umożliwiał płatności bez potrzeby ponownego wprowadzania danych.

Mechanizm płatności przez bramkę kartową

Ze względu na potrzebę zmaksymalizowania bezpieczeństwa wprowadzanych danych kart, ta metoda płatności wymaga wdrożenia dodatkowych zabezpieczeń na stronie sklepu i jest jedną z najbardziej zaawansowanych integracji.

  1. W pierwszej kolejności należy dodać możliwość wyboru metody płatności kartą swoim klientom, poprzez np. wyświetlenie dedykowanej opcji "Szybka płatność kartą".
  2. Po wybraniu tej metody płatności, należy wyświetlić klientowi formularz, w którym będzie mógł wprowadzić dane karty, oraz zaakceptować regulamin płatności Tpay.
  3. W trakcie wprowadzania danych, akcję sprawdzenia poprawności numeru karty wykonuje JavaScript. Każdy numer karty jest nadawany w oparciu o konkretny schemat i te informacje są publicznie dostępne, dlatego formularz Tpay rozpoznaje wystawcę karty po dwóch lub czterech cyfrach. W tym miejscu następuje również sprawdzenie, czy karta jest obsługiwana przez nasz system. Aktualnie akceptujemy wpłaty wyłączenie kartami VISA, MasterCard i Maestro:
  4. Po wprowadzeniu obsługiwanych danych i kliknięciu "Zapłać z Tpay", następuje szyfrowanie danych przed ich dalszym przesłaniem.
  5. Tpay udostępnia gotowe biblioteki programistyczne w języku PHP, które obsługują wszystkie te akcje automatycznie i nie wymagają dużego nakładu pracy deweloperów. Wystarczy tylko dostosować gotowe pliki do swoich potrzeb.
  6. Dalszy proces zależy od zabezpieczenia 3D Secure na karcie klienta:
    1. Karta z 3DS
      1. Płatność kartą zabezpieczoną będzie wymagała wykonania przekierowania klienta do tzw. bramki 3DS, gdzie będzie musiał potwierdzić płatność, w sposób uzależniony od swojego banku (np. poprzez wprowadzenie jednorazowego hasła SMS).
      2. Po udanej płatności klient zostanie przekierowany do strony sukcesu w sklepie.
      3. Powiadomienie o wpłacie zostanie wysłane na adres wynikowy sklepu, skonfigurowany w ustawieniach konta sprzedawcy.
    2. Karta bez 3DS
      1. Próba pobrania środków zostanie wykonana natychmiast po złożeniu zamówienia, a informacja o sukcesie lub niepowodzeniu zostanie zwrócona natychmiast.
      2. W tej sytuacji nie ma potrzeby wykonywania przekierowania do stron zewnętrznych, a zwrócone dane świadczą jednoznacznie o przypisaniu środków do konta sprzedawcy.
      3. Jeśli płatność się nie powiedzie należy umożliwić klientowi ponowną próbę zapłaty inną kartą.

 Techniczne wyjaśnienie płatności kartą na stronie sklepu

  1. W momencie złożenia zamówienia, serwis powinien dysponować już danymi zamawiającego, takimi jak adres email oraz imię i nazwisko, które będą wymagane do przygotowania transakcji.
    Transakcja powinna zostać utworzona zgodnie z poradnikiem Tworzenie transakcji przez API.
  2. Próba opłacenia transakcji kartą odbywa się poprzez wysłanie zapytania przez API. Do rozpoczęcia integracji będą potrzebne dane dostępowe takie jak identyfikator klienta, sekretny klucz klienta i klucz publiczny RSA, które są dostępne w Panelu Odbiorcy Płatności.
  3. Do wyświetlenia gotowego oskryptowanego formularza, zalecamy posłużyć się gotową biblioteką PHP tak jak w tym przykładzie. Wspomniany przykład wyświetli gotowy formularz do wprowadzania danych karty.
  4. Przed wysłaniem formularza z danymi, należy zaszyfrować wprowadzone dane:
    • numer karty,
    • kod CVV,
    • data ważności,
    kluczem publicznym RSA zgodnie z wytycznymi opisanymi w szczegółach metody POST /transactions/{transactionId}/pay
  5. Następnie wprowadzone dane należy wyczyścić i przesyłać jedynie zaszyfrowany jednorazowy ciąg. Ta operacja powinna odbyć się po stronie klienta, tak aby uniknąć jakiegokolwiek przesłania danych karty klienta w formie jawnej. Przykład skryptu JS obsługującego takie szyfrowanie znajduje się w bibliotece Tpay w serwisie Github.
    Bliblioteka Tpay PHP zawiera formularz, który wykonuje te czynności automatycznie!
  6. Drugi krok to wywołanie metody /transactions/{transactionId}/pay, służącej do przesłania potrzebnych danych do serwera Tpay.
    Minimalny zestaw parametrów, jakie należy wysłać w tej metodzie to:
    {
      "groupId": 103,
      "cardPaymentData": {
        "card": "SrKhV6VEPiaw4cQEM2Mr0Nj7fuVhRdAdQ9Bk+sv7iM/St12s/W4zRNyqVptN3KIVMqr6coRXNfSPF/o/N9pLy94koY255N5U8wc/ApsBVyo7tlV7xQDxM+I5ksZ3f11qhxzIsk8inmzQGJywrg9CGvZAoiHazgDSVRuy7Tj4vbk="
      }
    }

    Dodatkowy parametr "save" decyduje o zapamiętaniu karty płatniczej. Jeżeli metoda ta nie ma posłużyć do zapamiętania karty płatniczej, należy zawsze przesłać w niej parametr "save" z wartością logiczną true.

    {
      "groupId": 103,
      "cardPaymentData": {
        "card": "SrKhV6VEPiaw4cQEM2Mr0Nj7fuVhRdAdQ9Bk+sv7iM/St12s/W4zRNyqVptN3KIVMqr6coRXNfSPF/o/N9pLy94koY255N5U8wc/ApsBVyo7tlV7xQDxM+I5ksZ3f11qhxzIsk8inmzQGJywrg9CGvZAoiHazgDSVRuy7Tj4vbk=",
        "save": true
      }
    }

    Zapamiętanie karty można wykorzystać do wdrożenia płatności rekurencyjnych. Zapamiętany token będzie zwrócony w parametrze "card_token", w powiadomieniu systemowym lub odpowiedzi na wywołanie API (w zależności od posiadania zabezpieczenia 3D Secure).

  7. Dalszy proces zależy od zabezpieczenia 3D Secure na karcie klienta.
    Rozpoznanie czy karta jest zabezpieczona przez 3DS wykonuje się na podstawie kodu HTTP odpowiedzi oraz statusu transakcji.
    1. Karta z 3DS:
      1. Zwrócony zostanie kod HTTP 202 oraz parametr status będzie miał wartość "pending".
      2. Zwrócony parametr "transactionPaymentUrl" będzie zawierał link do bramki 3DS, pod który należy przekierować klienta aby mógł dokończyć walidację 3DS w swoim banku.
      3. Po udanym zatwierdzeniu płatności przez klienta, na adres wynikowy serwera (zdefiniowany w ustawieniach konta) zostanie wysłane asynchroniczne powiadomienie o wpłacie, tak samo jak w przypadku transferów bankowych.
        Przeczytaj jak odebrać powiadomienie o wpłacie.
    2. Karta bez 3DS:
      1. Zwrócony zostanie kod HTTP 200 oraz parametr status będzie miał wartość "success" lub "declined" w zależności od wyniku obciążenia.
      2. Jeśli płatność się nie powiodła, można przekierować klienta na adres z parametru 'transactionPaymentUrl' zwrócony przy tworzeniu transakcji (nie przy próbie opłacania).

Przykład wdrożenia w oparciu o biblioteki PHP Tpay

  1. Pobierz biblioteki Tpay z serwisu Github.
  2. W folderze Examples znajduje się plik ExamplesConfig.php, w którym podaj swoje dane dostępowe jako wartości stałych:
    const MERCHANT_CLIENT_ID = '1010-e5736adfd4bc5d8c';
    
    const MERCHANT_CLIENT_SECRET = '493e01af815383a687b747675010f65d1eefaeb42f63cfe197e7b30f14a556b7';
    
    const MERCHANT_RSA_KEY = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2NLRTVZNU1Wemd5a1Z5ODNMS1NTTFlEMEVrU2xadTRVZm1STS8NCmM5L0NtMENuVDM2ekU0L2dMRzBSYzQwODRHNmIzU3l5NVpvZ1kwQXFOVU5vUEptUUZGVyswdXJacU8yNFRCQkxCcU10TTVYSllDaVQNCmVpNkx3RUIyNnpPOFZocW9SK0tiRS92K1l1YlFhNGQ0cWtHU0IzeHBhSUJncllrT2o0aFJDOXk0WXdJREFRQUINCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==';

    Dane dostępowe nie muszą być podane w pliku ExamplesConfig, można wykorzystać własne źródło danych. Plik ten jest wykorzystywany tylko na potrzeby przykładu.

  3. W folderze Examples\TransactionsApi znajduje się plik CardGate.php zawierający klasę CardGate.
    Klasa metodę, która sprawdza czy należy wyświetlić formularz płatności kartą, czy procesować płatność. Na potrzeby przykładu jest to jeden plik.
    public function init()
    {
        if (empty($_POST)) {
            $PaymentForms = new PaymentForms('en');
            //Show new payment form
            echo $PaymentForms->getOnSiteCardForm(static::MERCHANT_RSA_KEY, 'CardGate.php', true, false);
        } else {
            $this->processNewCardPayment();
        }
    }​

    Jeśli nie zostaną przesłane dane karty, klasa wyświetli formularz płatności.
    Metoda getOnSiteCardForm pozwala na wygenerowanie formularza w kilku wariantach:
    1. Podanie danych karty:
    2. Podanie danych karty oraz nazwy i adresu email właściciela:
    3. Dodatkowo można zdecydować czy wyświetlany ma być przycisk zezwalający na zapisanie karty oraz lista zapamiętanych wcześniej kart:
  4. Klasa zapewnia metodę tworzącą transakcję na podstawie danych klienta i zamówienia. Jeśli wybrany został wariant z podawaniem danych klienta w formularzu, należy zamienić linie odpowiedzialne za pobieranie danych $clientEmail i $clientName na te, które są w komentarzu:
    private function getNewCardTransaction()
    {
        //If you set the fourth getOnSiteCardForm() parameter true, you can get client name and email here.
        //Otherwise, you must get those values from your DB.
    //    $clientName = Util::cast($_POST['client_name'], FieldTypes::STRING);
    //    $clientEmail = Util::cast($_POST['client_email'], FieldTypes::STRING);
        $clientEmail = '[email protected]';
        $clientName = 'John Doe';
        $request = [
            'amount' => 0.10,
            'description' => 'test transaction',
            'hiddenDescription' => 'order_213',
            'payer' => [
                'email' => $clientEmail,
                'name' => $clientName,
            ],
            'lang' => 'pl',
            'callbacks' => [
                'notification' => [
                    'url' => 'https://example.com/notification',
                ],
                'payerUrls' => [
                    'success' => 'https://example.com/success',
                    'error' => 'https://example.com/error',
                ],
            ],
            'pay' => [
                'groupId' => 103,
            ],
        ];
    
        return $this->TpayApi->Transactions->createTransaction($request);
    }

    Niektóre dane należy pobrać z bazy danych (kwotę, walutę itp.), w tym przykładzie wprowadziliśmy dane przykładowe.

  5. Klasa zapewnia metodę procesującą płatność kartą:
    private function makeCardPayment($transaction)
    {
        if (isset($_POST['card_save'])) {
            $saveCard = Util::cast($_POST['card_save'], FieldTypes::STRING);
        } else {
            $saveCard = false;
        }
        $cardData = Util::cast($_POST['carddata'], FieldTypes::STRING);
        $request = [
            'groupId' => 103,
            'cardPaymentData' => [
                'card' => $cardData,
                'save' => $saveCard === 'on',
            ],
            'method' => 'sale',
        ];
    
        return $this->TpayApi->Transactions->createPaymentByTransactionId($request, $transaction['transactionId']);
    }

    oraz metodę sterującą logiką w zależności od wyniku próby płatności:

    private function processNewCardPayment()
    {
        if (isset($_POST['card_vendor']) && in_array($_POST['card_vendor'], static::SUPPORTED_CARD_VENDORS)) {
            $this->saveUserCardVendor($_POST['card_vendor']);
        }
        $transaction = $this->getNewCardTransaction();
        if (!isset($transaction['transactionId'])) {
            //Code error handling @see POST /transactions HTTP 400 response details
            throw new TpayException('Unable to create transaction. Response: '.json_encode($transaction));
        } else {
            //Try to sale with provided card data
            $response = $this->makeCardPayment($transaction);
            if (!isset($response['result']) || $response['result'] === 'failure') {
                header("Location: ".$transaction['transactionPaymentUrl']);
            }
            if (isset($response['status']) && $response['status'] === 'correct') {
                //Successful payment by card not protected by 3DS
                $this->setOrderAsComplete($response);
            } elseif (isset($response['transactionPaymentUrl'])) {
                //Successfully generated 3DS link for payment authorization
                header("Location: ".$response['transactionPaymentUrl']);
            } else {
                //Invalid credit card data
                header("Location: ".$transaction['transactionPaymentUrl']);
            }
        }
    }
  6. Jeżeli płatność się powiedzie, można korzystając ze zwróconych danych oznaczyć zamówienie w sklepie jako opłacone. W tym celu należy dopisać funkcję setOrderAsComplete, która w oparciu o tablicę danych w $response, wywoła mechanizm sklepowy zmieniający status zamówienia i zapisujący przydatne dane.
    W przypadku karty z 3DS zostanie wykonane przekierowanie na adres bramki 3DS.
    Jeśli podane dane karty będą nieprawidłowe i próba płatności zwróci błąd, klient zostanie przekierowany na adres płatności za transakcję, gdzie będzie mógł ponownie wprowadzić dane karty i ponowić próbę opłacenia.
  7. Na końcu pliku następuje wywołanie klasy i metody init:
    (new CardGate)->init();​

Rozszerzenie klasy o obsługę płatności zapisaną kartą

Jeśli sklep umożliwia klientom zapisywanie kart, można połączyć formularz płatności nową kartą z formularzem płatności zapisaną kartą. Dzięki temu powracający klient ma zawsze możliwość wyboru, którą kartą chce zapłacić za zakupy.
Biblioteka Tpay zapewnia gotowy formularz obsługujący wszystkie te akcje i obsługująca je w sposób analogiczny do opisanego w poprzedniej sekcji.
W celu zaprezentowania obsługi takiego przypadku w PHP udostępniliśmy dodatkowy rozszerzony przykład "CardGateExtended.php"

Przykład opisany w poprzedniej sekcji zostaje rozszerzony o:

  1. Zmianę w metodzie init() uwzględniającą możliwość przesłania numeru zapisanej karty zamiast danych nowej karty:
    public function init()
    {
        if (isset($_POST['carddata'])) {
            $this->processNewCardPayment();
        } elseif (isset($_POST['savedId'])) {
            //Payment by saved card
            $this->processSavedCardPayment($_POST['savedId']);
        } else {
            $userCards = $this->getUserSavedCards($this->getCurrentUserId());
            //Show new payment form
            echo (new PaymentForms('en'))
                    ->getOnSiteCardForm(static::MERCHANT_RSA_KEY, 'CardGateExtended.php', true, false, $userCards);
        }
    }​
  2. Nową metodę processSavedCardPayment() sprawdzającą czy przesłany numer karty jest prawidłowy i zalogowany użytkownik jest właścicielem oraz wykonująca płatność:
    private function processSavedCardPayment($savedCardId)
    {
        $transaction = $this->getNewCardTransaction();
        $exampleCurrentUserId = $this->getCurrentUserId();
        if (!is_numeric($savedCardId)) {
             Logger::log('Invalid saved cardId', 'CardId: '.$savedCardId);
    
            return header("Location: ".$transaction['transactionPaymentUrl']);
        }
        $requestedCardId = (int)$savedCardId;
        $currentUserCards = $this->getUserSavedCards($exampleCurrentUserId);
        $isValid = false;
        $cardToken = '';
        foreach ($currentUserCards as $card) {
            if ($card['cardId'] === $requestedCardId) {
                $isValid = true;
                $cardToken = $card['cli_auth'];
            }
        }
        if ($isValid === false) {
            Logger::log(
                'Unauthorized payment try',
                sprintf('User %s has tried to pay by not owned cardId: %s', $exampleCurrentUserId, $requestedCardId)
            );
    
            //Reject current payment try and redirect user to tpay payment panel new card form
            return header("Location: ".$transaction['transactionPaymentUrl']);
        } else {
            return $this->payBySavedCard($cardToken, $transaction);
        }
    }​
  3. Metodę payBySavedCard() wykonującą płatność zapisaną kartą:
    private function payBySavedCard($cardToken, $transaction)
    {
        $request = [
            'groupId' => 103,
            'cardPaymentData' => [
                'token' => $cardToken,
            ],
            'method' => 'sale',
        ];
        $result = $this->TpayApi->Transactions->createPaymentByTransactionId($request, $transaction['transactionId']);
        if (isset($result['result'], $result['status']) && $result['status'] === 'correct') {
            return $this->setOrderAsComplete($result);
        } else {
            return header("Location: ".$transaction['transactionPaymentUrl']);
        }
    }​
  4. Metodę getUserSavedCards($userId) imitującą tabelę w bazi danych sklepu z zapisanymi kartami użytkowników
    private function getUserSavedCards($userId = 0)
    {
        //Code getting current logged user cards from your DB. This is only an example of DB.
        $exampleDbUsersIdsWithCards = [
            0 => [],
            2 => [
                [
                    'cardId' => 1,
                    'vendor' => 'visa',
                    'shortCode' => '****1111',
                    'cli_auth' => 't5ca63654a3c44a8fac1dea7f1227b9f5d8dc4af',
                ],
                [
                    'cardId' => 2,
                    'vendor' => 'mastercard',
                    'shortCode' => '****4444',
                    'cli_auth' => 't5ca636697eebe24b5c2cf02f5d7723f1297f825',
                ],
            ],
            3 => [
                [
                   'cardId' => 3,
                   'vendor' => 'visa',
                   'shortCode' => '****3321',
                   'cli_auth' => 't5ca6367039f480aa9df557798b47748681f1f05',
                ],
            ],
        ];
         if (isset($exampleDbUsersIdsWithCards[$userId])) {
            return $exampleDbUsersIdsWithCards[$userId];
        }
    
        return [];
    }
    ​​