Implement yookassa client

This commit is contained in:
2026-03-19 00:09:25 +03:00
parent 075a53f6ef
commit dd2c360cf6
15 changed files with 411 additions and 114 deletions

View File

@@ -5,12 +5,23 @@ import "time"
type YooKassa struct {
BaseUrl string
Timeout time.Duration
Retry Retry
Test bool
AllowedCallbackSubnets []string
CheckAllowedCallbackAddress bool
AllowedCallbackSubnets []string
ApiBaseKey string
ApiBaseSecret string
ApiPaymentKey string
ApiPaymentSecret string
CallbackProcessTimeout time.Duration
}
type Retry struct {
Enabled bool
Count int
WaitTime time.Duration
MaxWaitTime time.Duration
}

View File

@@ -27,7 +27,8 @@ func WithContext(ctx context.Context) Optional {
}
type Service interface {
CreatePayout(models.PayoutReq, *orm.User, string, ...Optional) (models.PayoutResp, error)
GetSbpBanks(...Optional) ([]models.SBPBank, error)
CreatePayout(models.PayoutReq, *orm.User, string, ...Optional) (*models.PayoutResp, error)
GetConfig() yookassaConf.YooKassa
}

View File

@@ -0,0 +1,71 @@
package yookassa
import (
"fmt"
"time"
"payouts/internal/models"
)
type SBPBankResponse struct {
Type string `json:"type"`
Items []models.SBPBank `json:"items"`
}
type Amount struct {
Value string `json:"value"`
Currency string `json:"currency"`
}
type Card struct {
Number string `json:"number"`
First6 string `json:"first6"`
Last4 string `json:"last4"`
CardType string `json:"card_type"`
IssuerCountry string `json:"issuer_country"`
IssuerName string `json:"issuer_name"`
}
type PayoutDestination struct {
Type models.PayoutType `json:"type"`
AccountNumber string `json:"account_number"`
Phone string `json:"phone"`
BankID string `json:"bank_id"`
RecipientChecked bool `json:"recipient_checked,omitempty"`
Card Card `json:"card,omitzero"`
}
type Metadata map[string]any
type PayoutRequest struct {
Amount Amount `json:"amount"`
PayoutDestinationData PayoutDestination `json:"payout_destination_data"`
Description string `json:"description"`
Metadata Metadata `json:"metadata"`
Test bool `json:"test"`
}
type PayoutResponse struct {
ID string `json:"id"`
Status models.PayoutStatus `json:"status"`
Amount Amount `json:"amount"`
PayoutDestination PayoutDestination `json:"payout_destination"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
SucceededAt time.Time `json:"succeeded_at"`
Metadata Metadata `json:"metadata"`
Test bool `json:"test"`
}
type Error struct {
Type string `json:"type"`
ID string `json:"id"`
Description string `json:"description"`
Parameter string `json:"parameter"`
Code string `json:"code"`
Status int `json:"status"`
}
func (e *Error) Error() string {
return fmt.Sprintf("yookassa error. status %d (%s). %s %s", e.Status, e.Code, e.Description, e.Parameter)
}

View File

@@ -3,21 +3,54 @@ package yookassa
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
resty "github.com/go-resty/resty/v2"
"payouts/internal/models"
"payouts/internal/service/database/orm"
"payouts/internal/service/yookassa/config"
)
const IdempotenceHeader = "Idempotence-Key"
type yookassaService struct {
conf config.YooKassa
ctx context.Context
client *resty.Client
}
func NewYookassaService(conf config.YooKassa) (Service, error) {
client := resty.New()
client.SetBaseURL(conf.BaseUrl)
client.SetBasicAuth(conf.ApiPaymentKey, conf.ApiPaymentSecret)
client.SetTimeout(conf.Timeout)
if conf.Retry.Enabled {
client.
SetRetryCount(conf.Retry.Count).
SetRetryWaitTime(conf.Retry.WaitTime).
SetRetryMaxWaitTime(conf.Retry.MaxWaitTime).
AddRetryCondition(
func(r *resty.Response, err error) bool {
// Retry on network errors
if err != nil {
return true
}
// Retry on specific status codes
return r.StatusCode() == 429 || // Too Many Requests
r.StatusCode() >= 500 // Server errors
},
)
}
svc := &yookassaService{
conf: conf,
ctx: context.Background(),
conf: conf,
client: client,
ctx: context.Background(),
}
return svc, nil
}
@@ -32,17 +65,79 @@ func (y *yookassaService) getParams(options ...Optional) *params {
return params
}
// GetSbpBanks implements [Service].
func (y *yookassaService) GetSbpBanks(opts ...Optional) ([]models.SBPBank, error) {
params := y.getParams(opts...)
yResp := &SBPBankResponse{}
yError := &Error{}
restyResp, err := y.client.R().
SetContext(params.ctx).
SetResult(yResp).
SetError(yError).
Get("/sbp_banks")
slog.Debug(fmt.Sprintf("Got response from yookassa: %v", restyResp))
if err != nil {
return nil, errors.Join(errors.New("failed to perform yookassa api post"), err)
}
if restyResp.StatusCode() != http.StatusOK {
yError.Status = restyResp.StatusCode()
return nil, yError
}
return yResp.Items, nil
}
// CreatePayout implements [Service].
func (y *yookassaService) CreatePayout(req models.PayoutReq, userSession *orm.User, idempotenceKey string, opts ...Optional) (models.PayoutResp, error) {
// params := y.getParams(opts...)
func (y *yookassaService) CreatePayout(req models.PayoutReq, userSession *orm.User, idempotenceKey string, opts ...Optional) (*models.PayoutResp, error) {
params := y.getParams(opts...)
yReq := &PayoutRequest{
Amount: Amount{
Value: fmt.Sprintf("%.2f", req.Amount),
Currency: "RUB",
},
PayoutDestinationData: PayoutDestination{
Type: req.PayoutType,
},
}
yResp := &PayoutResponse{}
yError := &Error{}
switch req.PayoutType {
case models.TypeSBP:
yReq.PayoutDestinationData.Phone = userSession.Phone
yReq.PayoutDestinationData.BankID = req.BankID
case models.TypeYooMoney:
yReq.PayoutDestinationData.AccountNumber = req.AccountNumber
default:
return models.PayoutResp{Result: "failed", ErrorReason: "unsupported payout type"}, errors.New("unsupported payout type")
return nil, errors.New("unsupported payout type")
}
return models.PayoutResp{}, nil
restyResp, err := y.client.R().
SetContext(params.ctx).
SetHeader(IdempotenceHeader, idempotenceKey).
SetBody(yReq).
SetResult(yResp).
SetError(yError).
Post("/payouts")
slog.Debug(fmt.Sprintf("Got response from yookassa: %v", restyResp))
if err != nil {
return nil, errors.Join(errors.New("failed to perform yookassa api post"), err)
}
if restyResp.StatusCode() != http.StatusOK {
yError.Status = restyResp.StatusCode()
return nil, yError
}
return &models.PayoutResp{ID: yResp.ID, Status: yResp.Status}, nil
}
// GetConfig implements [Service].