Rename config vars. Add widget holder template and endpoint to serve it. Add dockerfile

This commit is contained in:
2026-03-31 22:18:41 +03:00
parent 33da1338bb
commit 6d67e969e0
16 changed files with 207 additions and 28 deletions

View File

@@ -15,6 +15,7 @@ import (
"payouts/internal/api/payout"
"payouts/internal/api/user"
"payouts/internal/api/version"
"payouts/internal/api/widget"
appConfig "payouts/internal/config"
"payouts/internal/service/monitoring"
)
@@ -25,6 +26,7 @@ var Module = fx.Options(
health.Module,
payout.Module,
version.Module,
widget.Module,
monitoring.Module,
fx.Invoke(RegisterRoutes),
@@ -41,8 +43,9 @@ type Params struct {
PayoutHandler payout.Handler
UserHandler user.Handler
Version version.Handler
HealthHandler health.Handler
Version version.Handler
Widget widget.Handler
Metrics monitoring.Metrics
}
@@ -77,6 +80,9 @@ func RegisterRoutes(p Params, lc fx.Lifecycle) {
payoutRouter.HandleFunc(payout.CreateRoute, p.PayoutHandler.PayoutCreate).Methods(http.MethodPost)
payoutRouter.HandleFunc(payout.CallbackRoute, p.PayoutHandler.PayoutCallback).Methods(http.MethodPost)
// Widget endpoint
router.HandleFunc(widget.WidgetPage, p.Widget.WidgetHandler).Methods(http.MethodGet)
// collect api metrics
apiRouter.Use(p.Metrics.GetMiddleware())

View File

@@ -122,7 +122,7 @@ func (p *payoutHandler) GetSbpBanks(w http.ResponseWriter, r *http.Request) {
}
}
// PaymentCreate implements [Handler].
// PayoutCreate implements [Handler].
func (p *payoutHandler) PayoutCreate(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
@@ -203,7 +203,7 @@ func (p *payoutHandler) delayedPayoutUpdate(ctx context.Context, payoutData *yoo
}
}
// PaymentCallback implements [Handler].
// PayoutCallback implements [Handler].
func (p *payoutHandler) PayoutCallback(w http.ResponseWriter, r *http.Request) {
// todo: check also the X-real-ip and/or X-Forwarded-For
if p.yookassaConf.CheckAllowedCallbackAddress && !p.checkAllowedIpCallback(r.RemoteAddr) {

View File

@@ -0,0 +1,9 @@
package widget
import (
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(NewWidgetHandler),
)

View File

@@ -0,0 +1,55 @@
package widget
import (
"html/template"
"net/http"
"go.uber.org/fx"
"payouts/internal/service/yookassa"
yookassaConf "payouts/internal/service/yookassa/config"
"payouts/internal/templates"
)
const WidgetPage = "/payout/widget"
type Handler interface {
WidgetHandler(http.ResponseWriter, *http.Request)
}
type widgetHandler struct {
template *template.Template
config yookassaConf.YooKassa
}
// Params represents the module input params
type Params struct {
fx.In
YookassaService yookassa.Service
}
func NewWidgetHandler(p Params) (Handler, error) {
return &widgetHandler{
template: templates.Templates,
config: p.YookassaService.GetConfig(),
}, nil
}
// WidgetHandler renders the payouts widget page
func (h *widgetHandler) WidgetHandler(w http.ResponseWriter, r *http.Request) {
data := struct {
ApiPayoutKey string
WidgetVersion string
}{
ApiPayoutKey: h.config.ApiPayoutKey,
WidgetVersion: h.config.WidgetVersion,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := h.template.ExecuteTemplate(w, "payouts-widget.html", data)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}

View File

@@ -10,6 +10,8 @@ type PayoutType int64
const (
TypeSBP PayoutType = iota
TypeYooMoney
TypeCard
TypeCardWidget
)
func (r PayoutType) String() string {
@@ -18,6 +20,10 @@ func (r PayoutType) String() string {
return "spb"
case TypeYooMoney:
return "yoo_money"
case TypeCard:
return "bank_card"
case TypeCardWidget:
return "widget"
}
return "unknown"
}
@@ -33,8 +39,12 @@ func (r *PayoutType) UnmarshalText(text []byte) (err error) {
*r = TypeSBP
case "yoo_money":
*r = TypeYooMoney
case "bank_card":
*r = TypeCard
case "widget":
*r = TypeCardWidget
default:
err = fmt.Errorf("invalid payment type: %s", s)
err = fmt.Errorf("invalid payout type: %s", s)
}
return err
}
@@ -83,7 +93,7 @@ func (r *PayoutStatus) UnmarshalText(text []byte) (err error) {
case "failed":
*r = StatusFailed
default:
err = fmt.Errorf("invalid payment type: %s", s)
err = fmt.Errorf("invalid payout type: %s", s)
}
return err
}
@@ -96,8 +106,10 @@ type SBPBank struct {
type PayoutReq struct {
PayoutType PayoutType `json:"payout_type"`
PayoutToken string `json:"payout_token"`
AccountNumber string `json:"account_number"`
BankID string `json:"bank_id"`
CardNumber string `json:"card_number"`
Amount float32 `json:"amount"`
}

View File

@@ -13,8 +13,9 @@ type YooKassa struct {
ApiBaseKey string
ApiBaseSecret string
ApiPaymentKey string
ApiPaymentSecret string
ApiPayoutKey string
ApiPayoutSecret string
WidgetVersion string
CallbackProcessTimeout time.Duration
}

View File

@@ -39,7 +39,8 @@ type Metadata map[string]any
type PayoutRequest struct {
Amount Amount `json:"amount"`
PayoutDestinationData PayoutDestination `json:"payout_destination_data"`
PayoutToken string `json:"payout_token,omitempty"`
PayoutDestinationData PayoutDestination `json:"payout_destination_data,omitzero"`
Description string `json:"description"`
Metadata Metadata `json:"metadata"`
Test bool `json:"test"`

View File

@@ -26,7 +26,7 @@ type yookassaService struct {
func NewYookassaService(conf config.YooKassa) (Service, error) {
client := resty.New()
client.SetBaseURL(conf.BaseUrl)
client.SetBasicAuth(conf.ApiPaymentKey, conf.ApiPaymentSecret)
client.SetBasicAuth(conf.ApiPayoutKey, conf.ApiPayoutSecret)
client.SetTimeout(conf.Timeout)
if conf.Retry.Enabled {
@@ -110,10 +110,23 @@ func (y *yookassaService) CreatePayout(req models.PayoutReq, userSession *orm.Us
switch req.PayoutType {
case models.TypeSBP:
yReq.PayoutDestinationData.Phone = userSession.Phone
yReq.PayoutDestinationData.BankID = req.BankID
yReq.PayoutDestinationData = PayoutDestination{
Phone: userSession.Phone,
BankID: req.BankID,
}
case models.TypeYooMoney:
yReq.PayoutDestinationData.AccountNumber = req.AccountNumber
case models.TypeCard:
yReq.PayoutDestinationData.Card = Card{
Number: req.CardNumber,
}
case models.TypeCardWidget:
yReq.PayoutToken = req.PayoutToken
yReq.PayoutDestinationData = PayoutDestination{}
default:
return nil, errors.New("unsupported payout type")
}

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payouts Page</title>
<script src="https://yookassa.ru/payouts-data/{{ .WidgetVersion }}/widget.js"></script>
<style>
</style>
</head>
<body>
<div id="payout-form"></div>
<script>
// Инициализация виджета. Все параметры обязательные.
const payoutsData = new window.PayoutsData({
type: 'payout',
account_id: '{{ .ApiPayoutKey }}', // Идентификатор шлюза (agentId в личном кабинете)
success_callback: function(data) {
// https://yookassa.ru/developers/payouts/making-payouts/bank-card/using-payout-widget/implementing-widget#reference-output-parameters
if (window.AndroidCallback) {
window.AndroidCallback.onWidgetData(JSON.stringify(data));
} else if (window.webkit && window.webkit.messageHandlers.iosCallback) {
window.webkit.messageHandlers.iosCallback.onWidgetData(JSON.stringify(data));
}
},
error_callback: function(error) {
// https://yookassa.ru/developers/payouts/making-payouts/bank-card/using-payout-widget/implementing-widget#reference-output-parameters-error
if (window.AndroidCallback) {
window.AndroidCallback.onWidgetError(JSON.stringify(error));
} else if (window.webkit && window.webkit.messageHandlers.iosCallback) {
window.webkit.messageHandlers.iosCallback.onWidgetError(JSON.stringify(error));
}
}
});
//Отображение формы в контейнере
payoutsData.render('payout-form')
//Метод возвращает Promise, исполнение которого говорит о полной загрузке формы сбора данных (можно не использовать).
.then(() => {
//Код, который нужно выполнить после отображения формы.
});
</script>
</body>
</html>

View File

@@ -0,0 +1,11 @@
package templates
import (
"embed"
"html/template"
)
//go:embed *.html
var FS embed.FS
var Templates = template.Must(template.ParseFS(FS, "*.html"))