Implement yookassa client
This commit is contained in:
@@ -98,8 +98,14 @@ func (d *dbService) CreatePayout(payoutModel *orm.Payout, opts ...Optional) erro
|
||||
return gorm.G[orm.Payout](d.db).Create(p.ctx, payoutModel)
|
||||
}
|
||||
|
||||
// UpdatePayout implements [Service].
|
||||
func (d *dbService) UpdatePayout(payoutModel *orm.Payout, opts ...Optional) error {
|
||||
// p := d.getParams(opts...)
|
||||
panic("unimplemented")
|
||||
// UpdatePayoutById implements [Service].
|
||||
func (d *dbService) UpdatePayoutById(id uint, updateModel orm.Payout, opts ...Optional) (int, error) {
|
||||
p := d.getParams(opts...)
|
||||
return gorm.G[orm.Payout](d.db).Where("id = ?", id).Updates(p.ctx, updateModel)
|
||||
}
|
||||
|
||||
// UpdatePayoutByPayoutID implements [Service].
|
||||
func (d *dbService) UpdatePayoutByPayoutID(payoutId string, updateModel orm.Payout, opts ...Optional) (int, error) {
|
||||
p := d.getParams(opts...)
|
||||
return gorm.G[orm.Payout](d.db).Where("payout_id = ?", payoutId).Updates(p.ctx, updateModel)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ type Service interface {
|
||||
GetUser(user *orm.User, opts ...Optional) (orm.User, error)
|
||||
GetPayout(payoutModel *orm.Payout, opts ...Optional) (orm.Payout, error)
|
||||
CreatePayout(payoutModel *orm.Payout, opts ...Optional) error
|
||||
UpdatePayout(payoutModel *orm.Payout, opts ...Optional) error
|
||||
UpdatePayoutById(id uint, updateModel orm.Payout, opts ...Optional) (int, error)
|
||||
UpdatePayoutByPayoutID(payoutId string, updateModel orm.Payout, opts ...Optional) (int, error)
|
||||
}
|
||||
|
||||
// Params represents the module input params
|
||||
|
||||
@@ -1,61 +1,9 @@
|
||||
package orm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PayoutStatus int64
|
||||
|
||||
const (
|
||||
StatusCreated PayoutStatus = iota
|
||||
StatusCanceled
|
||||
StatusPending
|
||||
StatusSucceeded
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
func (r PayoutStatus) String() string {
|
||||
switch r {
|
||||
case StatusCreated:
|
||||
return "created"
|
||||
case StatusCanceled:
|
||||
return "canceled"
|
||||
case StatusPending:
|
||||
return "pending"
|
||||
case StatusSucceeded:
|
||||
return "succeeded"
|
||||
case StatusFailed:
|
||||
return "failed"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (r PayoutStatus) MarshalText() (text []byte, err error) {
|
||||
return []byte(r.String()), nil
|
||||
}
|
||||
|
||||
func (r *PayoutStatus) UnmarshalText(text []byte) (err error) {
|
||||
s := strings.ToLower(string(text))
|
||||
switch s {
|
||||
case "canceled":
|
||||
*r = StatusCanceled
|
||||
case "created":
|
||||
*r = StatusCreated
|
||||
case "pending":
|
||||
*r = StatusPending
|
||||
case "succeeded":
|
||||
*r = StatusSucceeded
|
||||
case "failed":
|
||||
*r = StatusFailed
|
||||
default:
|
||||
err = fmt.Errorf("invalid payment type: %s", s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type Payout struct {
|
||||
gorm.Model
|
||||
|
||||
@@ -69,6 +17,6 @@ type Payout struct {
|
||||
AccountNumber string
|
||||
Amount float32
|
||||
Currency string
|
||||
Status PayoutStatus
|
||||
Status string
|
||||
Test bool
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
71
internal/service/yookassa/yookassa_api.go
Normal file
71
internal/service/yookassa/yookassa_api.go
Normal 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)
|
||||
}
|
||||
@@ -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].
|
||||
|
||||
Reference in New Issue
Block a user