DEV Community

Maksim
Maksim

Posted on

Будуємо надійні платіжні системи на Go: Глибоке занурення в Deposits, Withdrawals та безпеку

Будуємо надійні платіжні системи на Go: Глибоке занурення в Deposits, Withdrawals та безпеку

Платіжні системи — це серце будь-якого онлайн-бізнесу. Вони є складними, критично важливими для безпеки та вимагають високої надійності. Для Go-розробника робота з платіжним шлюзом (Payment Gateway) відкриває безліч можливостей для застосування потужності мови: від конкурентності до суворого типового контролю та продуктивності.

У цій статті ми зануримося в ключові аспекти розробки платіжної системи на Go, охоплюючи як депозити, так і виплати, а також не забуваючи про критично важливі питання безпеки та відповідності вимогам.

PSP Інтеграції (Stripe, PayPal, Crypto)

Першим кроком у будь-якій платіжній системі є інтеграція з одним або кількома провайдерами платіжних послуг (PSP) або крипто-шлюзами. Це можуть бути традиційні фіатні PSP, такі як Stripe і PayPal, або провайдери криптоплатежів.

Чому це важливо?

Кожен PSP має свій власний API, вимоги до автентифікації та формати даних. Наша система повинна бути гнучкою, щоб підтримувати їх усі.

Go підхід:

  • Абстракція за допомогою інтерфейсів: Створіть спільний інтерфейс для обробки платежів, щоб ваша бізнес-логіка не була жорстко прив'язана до конкретного PSP.
  • Специфічні клієнти: Реалізуйте окремі клієнти для кожного PSP, які відповідають інтерфейсу.
  • Використання net/http та encoding/json: Для взаємодії з RESTful API PSPs. Багато PSP також мають офіційні Go SDK (наприклад, Stripe), що значно спрощує інтеграцію.
package payments

import (
    "context"
    "errors"
)

// PaymentProcessor - спільний інтерфейс для різних PSP
type PaymentProcessor interface {
    ProcessDeposit(ctx context.Context, request *DepositRequest) (*DepositResponse, error)
    ProcessWithdrawal(ctx context.Context, request *WithdrawalRequest) (*WithdrawalResponse, error)
    // Додайте інші методи, наприклад, для повернень або запитів статусу
}

// DepositRequest / DepositResponse та інші структури даних
type DepositRequest struct {
    Amount       float64
    Currency     string
    CustomerID   string
    PaymentToken string // Токен, отриманий з фронтенду (наприклад, Stripe Token)
}

type DepositResponse struct {
    TransactionID string
    Status        string // "pending", "success", "failed"
    RedirectURL   string // Для 3DS
}

// StripeProcessor - реалізація для Stripe
type StripeProcessor struct {
    // ... конфігурація Stripe API ...
}

func NewStripeProcessor(apiKey string) *StripeProcessor {
    return &StripeProcessor{/* ... */}
}

func (s *StripeProcessor) ProcessDeposit(ctx context.Context, request *DepositRequest) (*DepositResponse, error) {
    // Логіка виклику Stripe API для депозиту
    // Наприклад, using stripe-go SDK:
    // chargeParams := &stripe.ChargeParams{Amount: stripe.Int64(int64(request.Amount*100)), Currency: stripe.String(request.Currency)}
    // chargeParams.SetSource(request.PaymentToken)
    // charge, err := charge.New(chargeParams)
    // ...
    return &DepositResponse{
        TransactionID: "stripe_txn_123",
        Status:        "success",
    }, nil // Повертаємо справжній результат від Stripe
}

func (s *StripeProcessor) ProcessWithdrawal(ctx context.Context, request *WithdrawalRequest) (*WithdrawalResponse, error) {
    return nil, errors.New("not implemented")
}

// PayPalProcessor - реалізація для PayPal (аналогічно)
type PayPalProcessor struct {}
// ...
Enter fullscreen mode Exit fullscreen mode

Deposit Flow з 3DS

Процес депозиту вимагає особливої уваги до безпеки, особливо із запровадженням 3D Secure (3DS) для підтвердження карткових операцій.

Чому це важливо?

3DS додає додатковий рівень безпеки, перевіряючи, що держатель картки є законним користувачем, що допомагає запобігти шахрайству та зменшує відповідальність за чарджбеки.

Go підхід:

  1. Ініціація 3DS: Коли PSP потребує 3DS, він поверне URL для перенаправлення користувача. Ваш Go-backend повинен отримати цей URL і передати його фронтенду.
  2. Обробка Callback: Після успішного (або невдалого) проходження 3DS, PSP надішле callback (зазвичай webhook) на ваш сервер. Ваш Go-сервер повинен мати ендпоінт для прийому та обробки цих callback-ів.
  3. Стани транзакцій: Використовуйте чіткі стани транзакцій (наприклад, pending_3ds, 3ds_successful, 3ds_failed, completed) для відстеження процесу.
// Приклад відповіді з PSP, що вимагає 3DS
// func (s *StripeProcessor) ProcessDeposit(...) {
//    // ... виклик Stripe API ...
//    if charge.Status == "requires_action" && charge.NextAction.Type == "use_stripe_sdk" {
//        return &DepositResponse{
//            Status:      "pending_3ds",
//            RedirectURL: charge.NextAction.UseStripeSDK.StripeJS, // Або інший URL для перенаправлення
//        }, nil
//    }
//    // ...
// }

// В HTTP-обробнику вашого Go-сервера:
// func HandleDepositRequest(w http.ResponseWriter, r *http.Request) {
//    // ... отримання даних від фронтенду ...
//    resp, err := stripeProcessor.ProcessDeposit(r.Context(), depositReq)
//    if err != nil { /* ... */ }
//
//    if resp.Status == "pending_3ds" {
//        // Повернути фронтенду URL для перенаправлення користувача
//        json.NewEncoder(w).Encode(map[string]string{"status": "pending_3ds", "redirect_url": resp.RedirectURL})
//        return
//    }
//    // ... обробка інших статусів ...
// }
Enter fullscreen mode Exit fullscreen mode

Withdrawal Approval Pipeline

Виплати, або виведення коштів, часто є більш складними, ніж депозити, оскільки вони пов'язані з ризиками шахрайства та регуляторними вимогами.

Чому це важливо?

Потрібен механізм для перевірки запитів на виплату, що може включати ручні перевірки, перевірки лімітів та перевірки на шахрайство.

Go підхід:

  • Система станів: Використовуйте модель станів (наприклад, requested, pending_approval, approved, rejected, processing, completed, failed) для відстеження кожної виплати.
  • Конкурентне виконання: Використовуйте goroutines та channels для асинхронної обробки перевірок (наприклад, перевірка AML/KYC, лімітів) або для взаємодії з зовнішніми системами (наприклад, банківськими переказами).
  • Система черг: Інтеграція з чергами повідомлень (Kafka, RabbitMQ) дозволить надійно обробляти запити на виплату у фоновому режимі та масштабувати процес.
package payments

import (
    "context"
    "time"
)

type WithdrawalStatus string

const (
    WithdrawalRequested    WithdrawalStatus = "requested"
    WithdrawalPendingAML   WithdrawalStatus = "pending_aml"
    WithdrawalPendingKYC   WithdrawalStatus = "pending_kyc"
    WithdrawalPendingAdmin WithdrawalStatus = "pending_admin_approval"
    WithdrawalApproved     WithdrawalStatus = "approved"
    WithdrawalProcessing   WithdrawalStatus = "processing"
    WithdrawalCompleted    WithdrawalStatus = "completed"
    WithdrawalFailed       WithdrawalStatus = "failed"
    WithdrawalRejected     WithdrawalStatus = "rejected"
)

type Withdrawal struct {
    ID         string
    UserID     string
    Amount     float64
    Currency   string
    Method     string // "bank_transfer", "paypal", "crypto"
    Status     WithdrawalStatus
    CreatedAt  time.Time
    ApprovedBy string // Хто схвалив (якщо вручну)
    Reason     string // Причина відмови
}

// Заглушка для сервісу, що обробляє виплати
type WithdrawalService struct {
    // ... залежності, наприклад, сховище даних, PSP клієнти ...
}

func (s *WithdrawalService) ProcessWithdrawalRequest(ctx context.Context, withdrawal *Withdrawal) error {
    withdrawal.Status = WithdrawalPendingAML
    // Зберегти в базу даних
    // ...

    // Відправити в чергу для AML-перевірки
    // go func() {
    //    err := s.amlChecker.Check(withdrawal.UserID)
    //    if err != nil { /* оновити статус на failed/rejected */ }
    //    // Відправити в наступну чергу або оновити статус
    // }()
    return nil
}
Enter fullscreen mode Exit fullscreen mode

KYC Verification Integration

Know Your Customer (KYC) – це обов'язковий процес перевірки особистості користувача для відповідності нормативним вимогам.

Чому це важливо?

Запобігання відмиванню грошей (AML), фінансуванню тероризму та іншим незаконним діям, а також дотримання місцевих і міжнародних фінансових регуляцій.

Go підхід:

  • Інтеграція з постачальниками KYC: Використовуйте net/http для взаємодії з API сторонніх сервісів (наприклад, Veriff, Persona, SumSub).
  • Асинхронна обробка: Процес KYC може займати час. Використовуйте webhook-и від провайдера KYC для отримання результатів, щоб не блокувати запити.
  • Кешвання результатів: Зберігайте статус KYC користувача в базі даних, щоб уникнути повторних перевірок.
package identity

import (
    "context"
    "errors"
    "time"
)

type KYCStatus string

const (
    KYCPending    KYCStatus = "pending"
    KYCApproved   KYCStatus = "approved"
    KYCRejected   KYCStatus = "rejected"
    KYCInProgress KYCStatus = "in_progress"
)

type UserKYC struct {
    UserID    string
    Status    KYCStatus
    LastCheck time.Time
    Details   map[string]interface{} // Зберігати дані від провайдера KYC
}

type KYCProvider interface {
    InitiateVerification(ctx context.Context, userID string, userData map[string]string) (string, error) // Повернути URL для редиректу
    ProcessWebhook(ctx context.Context, payload []byte) (*UserKYC, error)
    GetStatus(ctx context.Context, userID string) (*UserKYC, error)
}

type ThirdPartyKYCClient struct {
    APIKey string
    // ... інші конфігурації
}

func (c *ThirdPartyKYCClient) InitiateVerification(ctx context.Context, userID string, userData map[string]string) (string, error) {
    // Виклик API стороннього провайдера KYC
    // ...
    return "https://thirdpartykyc.com/verify?token=abc", nil // Повернути URL для клієнта
}

func (c *ThirdPartyKYCClient) ProcessWebhook(ctx context.Context, payload []byte) (*UserKYC, error) {
    // Парсинг payload, перевірка підпису, оновлення статусу KYC
    // ...
    return &UserKYC{
        UserID: "some_user_id",
        Status: KYCApproved,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

AML Screening (PEP, Sanctions)

Anti-Money Laundering (AML) перевірки включають скринінг клієнтів на предмет приналежності до політично значущих осіб (PEP) та наявність у санкційних списках.

Чому це важливо?

Юридичні зобов'язання та запобігання співпраці з особами або організаціями, залученими до незаконної діяльності.

Go підхід:

  • Інтеграція з AML-постачальниками: Схоже на KYC, використовуючи net/http для взаємодії з API сервісів AML (наприклад, Refinitiv, Dow Jones, ComplyAdvantage).
  • Фонова перевірка: Виконуйте AML-перевірки у фоновому режимі, особливо для вже існуючих користувачів, та регулярно оновлюйте їхні статуси.
  • Прийняття рішень: На основі результатів скринінгу ваша система повинна приймати автоматизовані або ручні рішення щодо ризику клієнта.
package identity

import "context"

type AMLStatus string

const (
    AMLNone       AMLStatus = "none"
    AMLScreened   AMLStatus = "screened"
    AMLHit        AMLStatus = "hit" // Знайдено збіг
    AMLFalsePositive AMLStatus = "false_positive"
)

type UserAML struct {
    UserID    string
    Status    AMLStatus
    LastCheck string
    ScreeningResults map[string]interface{}
}

type AMLChecker interface {
    ScreenUser(ctx context.Context, userID string, name, dob, country string) (*UserAML, error)
}

type ThirdPartyAMLClient struct {
    // ... конфігурація
}

func (c *ThirdPartyAMLClient) ScreenUser(ctx context.Context, userID string, name, dob, country string) (*UserAML, error) {
    // Виклик API стороннього провайдера AML
    // ...
    return &UserAML{
        UserID: userID,
        Status: AMLScreened,
        ScreeningResults: map[string]interface{}{
            "pep":       false,
            "sanctions": false,
        },
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

Payment Method Limits

Обмеження платіжних методів (ліміти) застосовуються для контролю ризиків, запобігання шахрайству та дотримання регулятивних вимог.

Чому це важливо?

Захист від великих шахрайських транзакцій, відповідність "законам про грошові перекази" та внутрішня політика ризиків.

Go підхід:

  • Централізований сервіс лімітів: Створіть окремий сервіс або модуль, який зберігає та застосовує ліміти (наприклад, максимальна сума транзакції, кількість транзакцій за період, загальна сума за період).
  • Конфігурація: Зберігайте ліміти в конфігурації (файли YAML, env vars) або в базі даних для динамічного оновлення.
  • Атомарні операції: При перевірці лімітів, особливо тих, що стосуються часу, використовуйте транзакції бази даних або розподілені м'ютекси, щоб уникнути race conditions при одночасних запитах.
package limits

import (
    "context"
    "time"
)

type LimitType string

const (
    DailyDepositLimit LimitType = "daily_deposit"
    MaxTxnAmount      LimitType = "max_transaction_amount"
)

type LimitConfig struct {
    Type     LimitType
    Value    float64
    Currency string
    Period   time.Duration // Для добових, тижневих лімітів
}

type LimitService struct {
    // Зберігання конфігурації лімітів (можна завантажити з DB/конфіг-файлу)
    configs map[LimitType]LimitConfig
    // ... сховище для поточних показників користувачів (наприклад, Redis, DB)
}

func (s *LimitService) CheckAndApplyLimits(ctx context.Context, userID string, txnAmount float64, txnType LimitType) error {
    limit, ok := s.configs[txnType]
    if !ok {
        return nil // Без обмежень, якщо не налаштовано
    }

    if txnAmount > limit.Value {
        return errors.New("transaction amount exceeds maximum limit")
    }

    // Для добових лімітів:
    // 1. Отримати поточну суму транзакцій користувача за період.
    // 2. Додати поточну транзакцію.
    // 3. Перевірити, чи не перевищено ліміт.
    // 4. Оновити суму в сховищі (важливо: атомарно!).
    // ...

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Webhook Reconciliation

Webhook-и є життєво важливими для асинхронної комунікації між вашою системою та PSP або іншими сторонніми сервісами. Вони повідомляють про події (наприклад, успішне завершення платежу, оновлення статусу KYC).

Чому це важливо?

Забезпечує актуальність даних та реакцію на події в реальному часі без постійного опитування сторонніх API.

Go підхід:

  • Виділений HTTP-ендпоінт: Створіть http.Handler для прийому webhook-ів.
  • Верифікація підпису: Критично важливо перевіряти цифрові підписи webhook-ів, щоб переконатися, що вони надходять від легітимного відправника та не були змінені. Go має чудові бібліотеки для криптографії (crypto/hmac, crypto/sha256).
  • Ідемпотентність: Ваш обробник повинен вміти обробляти дублікати webhook-ів без побічних ефектів, оскільки вони можуть бути надіслані кілька разів.
  • Асинхронна обробка: Після верифікації, помістіть payload webhook-а в чергу повідомлень (наприклад, RabbitMQ, Kafka) або обробіть його в окремій goroutine, щоб не блокувати HTTP-відповідь.
package webhooks

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io/ioutil"
    "net/http"
    "strconv"
    "time"
)

const (
    StripeWebhookSecret = "whsec_..." // Отримайте з Stripe
)

func VerifyStripeSignature(payload []byte, signatureHeader string) error {
    // Приклад для Stripe: "t=1678886400,v1=..."
    // Розпарсити хедер, знайти timestamp та сигнатуру
    // ... (складна логіка, краще використовувати stripe-go.Webhook.ConstructEvent)

    // Заглушка для демонстрації:
    expectedMAC := hmac.New(sha256.New, []byte(StripeWebhookSecret))
    expectedMAC.Write(payload)
    expectedSignature := hex.EncodeToString(expectedMAC.Sum(nil))

    // Порівняти expectedSignature з сигнатурою з хедера
    // ...
    if expectedSignature != "demo_signature" { // Замініть на реальну перевірку
        return errors.New("invalid webhook signature")
    }
    return nil
}

func HandleStripeWebhook(w http.ResponseWriter, r *http.Request) {
    payload, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading request body", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    // 1. Верифікація підпису
    signature := r.Header.Get("Stripe-Signature") // Залежить від PSP
    if err := VerifyStripeSignature(payload, signature); err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }

    // 2. Асинхронна обробка
    go func(p []byte) {
        // Розпарсити payload, визначити тип події (charge.succeeded, payout.paid тощо)
        // Оновити базу даних, сповістити користувача
        // ...
        log.Printf("Processed Stripe webhook: %s", string(p))
    }(payload)

    w.WriteHeader(http.StatusOK)
}
Enter fullscreen mode Exit fullscreen mode

Failed Payment Handling

Неминуче, платежі іноді будуть завершуватися невдачею. Наша система повинна бути готова до цього.

Чому це важливо?

Мінімізація втрат, покращення користувацького досвіду, надання чітких повідомлень про помилки.

Go підхід:

  • Деталізоване логування: Використовуйте структуроване логування (наприклад, Zap, Zerolog) для запису всіх деталей невдалого платежу, включаючи коди помилок PSP.
  • Кастомні типи помилок: Визначте кастомні типи помилок у Go для різних сценаріїв невдачі (наприклад, ErrInsufficientFunds, ErrCardDeclined, ErrFraudDetected).
  • Механізми повторних спроб (Retries): Для тимчасових помилок (наприклад, мережеві проблеми PSP) реалізуйте експоненційну затримку повторних спроб з обмеженням кількості спроб. Go time.Sleep та context.WithTimeout дуже корисні для цього.
  • Сповіщення користувачів: Чітко інформуйте користувачів про причину невдачі та можливі кроки для виправлення.
  • Адміністративні сповіщення: Надсилайте сповіщення команді підтримки/операцій для ручного втручання, якщо це необхідно.
package payments

import (
    "context"
    "errors"
    "time"
)

var (
    ErrPaymentDeclined    = errors.New("payment declined by issuer")
    ErrInsufficientFunds  = errors.New("insufficient funds")
    ErrNetworkTimeout     = errors.New("payment network timeout")
    ErrProcessorTemporary = errors.New("payment processor temporary error")
)

// ProcessDepositWithRetries спробує обробити депозит кілька разів з експоненційною затримкою
func ProcessDepositWithRetries(ctx context.Context, processor PaymentProcessor, request *DepositRequest, maxRetries int) (*DepositResponse, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        resp, err := processor.ProcessDeposit(ctx, request)
        if err == nil {
            return resp, nil // Успіх!
        }

        lastErr = err
        // Перевірка на тимчасові помилки, для яких є сенс повторювати
        if errors.Is(err, ErrNetworkTimeout) || errors.Is(err, ErrProcessorTemporary) {
            delay := time.Duration(1<<i) * time.Second // Експоненційна затримка: 1s, 2s, 4s, 8s...
            log.Printf("Deposit failed (attempt %d/%d): %v. Retrying in %v...", i+1, maxRetries, err, delay)
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                return nil, ctx.Err() // Контекст скасовано, не повторюємо
            }
        } else {
            // Не тимчасова помилка, не повторюємо
            log.Printf("Deposit failed (non-retriable): %v", err)
            return nil, err
        }
    }
    log.Printf("Deposit failed after %d retries: %v", maxRetries, lastErr)
    return nil, fmt.Errorf("deposit failed after multiple retries: %w", lastErr)
}
Enter fullscreen mode Exit fullscreen mode

Висновок

Побудова надійної платіжної системи на Go — це складне, але захоплююче завдання. Go пропонує потужні інструменти для вирішення цих викликів: конкурентність для асинхронної обробки, чітка система типів для стабільних інтеграцій та потужна стандартна бібліотека для мережевих взаємодій та криптографії.

Пам'ятайте, що безпека, відповідність нормативам та надійність повинні бути в центрі вашої архітектури. Дотримуючись найкращих практик, ви можете створити платіжну систему, яка буде ефективно обслуговувати ваш бізнес і користувачів.


Сподіваюся, ця стаття дала вам цінний огляд та натхнення для роботи з платіжними шлюзами в Go!

Tags

#Go #Golang #Payments #PaymentGateway #FinTech #Stripe #PayPal #Crypto #KYC #AML #3DS #Webhooks #SoftwareArchitecture #BackendDevelopment

Top comments (0)