Rename config vars. Add widget holder template and endpoint to serve it. Add dockerfile
This commit is contained in:
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM golang:1.26-alpine3.23 AS build
|
||||
|
||||
WORKDIR /payoutsbuild
|
||||
|
||||
RUN apk add --update --no-cache ca-certificates git
|
||||
|
||||
ENV GOBIN=/payoutsbuild/bin
|
||||
|
||||
ADD . /payoutsbuild
|
||||
|
||||
RUN cd /payoutsbuild && \
|
||||
go mod download && \
|
||||
go build ./cmd/...
|
||||
|
||||
FROM alpine:3.23
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /payoutsbuild/payouts /app/
|
||||
COPY --from=build /payoutsbuild/config/payouts.properties /app/
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT [ "/app/payouts" ]
|
||||
@@ -150,8 +150,8 @@
|
||||
| YooKassa.Test | Enable test mode | false |
|
||||
| YooKassa.ApiBaseKey | YooKassa base API key | (empty) |
|
||||
| YooKassa.ApiBaseSecret | YooKassa base API secret | (empty) |
|
||||
| YooKassa.ApiPaymentKey | YooKassa payment API key | (empty) |
|
||||
| YooKassa.ApiPaymentSecret | YooKassa payment API secret | (empty) |
|
||||
| YooKassa.ApiPayoutKey | YooKassa payout API key | (empty) |
|
||||
| YooKassa.ApiPayoutSecret | YooKassa payout API secret | (empty) |
|
||||
| YooKassa.BaseUrl | YooKassa API base URL | https://api.yookassa.ru/v3 |
|
||||
| YooKassa.Timeout | Request timeout | 2s |
|
||||
| YooKassa.CheckAllowedCallbackAddress | Check callback IP addresses | true |
|
||||
|
||||
@@ -55,8 +55,10 @@ YooKassa.AllowedCallbackSubnets = 185.71.76.0/27,185.71.77.0/27,77.75.153.0/25,7
|
||||
# Base API key/secret
|
||||
YooKassa.ApiBaseKey =
|
||||
YooKassa.ApiBaseSecret =
|
||||
# Payments API key/secret
|
||||
YooKassa.ApiPaymentKey =
|
||||
YooKassa.ApiPaymentSecret =
|
||||
# Payouts API key/secret
|
||||
YooKassa.ApiPayoutKey =
|
||||
YooKassa.ApiPayoutSecret =
|
||||
# Timeout to process yookassa callback
|
||||
YooKassa.CallbackProcessTimeout = 1s
|
||||
# Widget version
|
||||
YooKassa.WidgetVersion = 3.1.0
|
||||
|
||||
@@ -156,8 +156,8 @@ variables. Variable names are the uppercased property key with `.` replaced by `
|
||||
| `DATABASE_CONNECTION` | `Database.Connection` |
|
||||
| `YOOKASSA_APIBASEKEY` | `YooKassa.ApiBaseKey` |
|
||||
| `YOOKASSA_APIBASESECRET` | `YooKassa.ApiBaseSecret` |
|
||||
| `YOOKASSA_APIPAYMENTKEY` | `YooKassa.ApiPaymentKey` |
|
||||
| `YOOKASSA_APIPAYMENTSECRET` | `YooKassa.ApiPaymentSecret` |
|
||||
| `YOOKASSA_APIPAYOUTKEY` | `YooKassa.ApiPayoutKey` |
|
||||
| `YOOKASSA_APIPAYOUTSECRET` | `YooKassa.ApiPayoutSecret` |
|
||||
|
||||
Provide secrets at install/upgrade time:
|
||||
|
||||
@@ -166,8 +166,8 @@ helm install payouts ./helm \
|
||||
--set secrets.DATABASE_CONNECTION="host=127.0.0.1 user=app password=s3cr3t dbname=payouts port=5432 sslmode=disable" \
|
||||
--set secrets.YOOKASSA_APIBASEKEY="<key>" \
|
||||
--set secrets.YOOKASSA_APIBASESECRET="<secret>" \
|
||||
--set secrets.YOOKASSA_APIPAYMENTKEY="<key>" \
|
||||
--set secrets.YOOKASSA_APIPAYMENTSECRET="<secret>"
|
||||
--set secrets.YOOKASSA_APIPAYOUTKEY="<key>" \
|
||||
--set secrets.YOOKASSA_APIPAYOUTSECRET="<secret>"
|
||||
```
|
||||
|
||||
Or keep them in a separate values file that is **not committed to version control**:
|
||||
@@ -180,11 +180,11 @@ Example `secrets.values.yaml`:
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
DATABASE_CONNECTION: "host=127.0.0.1 user=app password=s3cr3t dbname=payouts port=5432 sslmode=disable"
|
||||
DATABASE_CONNECTION: "host=127.0.0.1 user=payouts password=password dbname=payouts port=5432 sslmode=disable"
|
||||
YOOKASSA_APIBASEKEY: "<key>"
|
||||
YOOKASSA_APIBASESECRET: "<secret>"
|
||||
YOOKASSA_APIPAYMENTKEY: "<key>"
|
||||
YOOKASSA_APIPAYMENTSECRET: "<secret>"
|
||||
YOOKASSA_APIPAYOUTKEY: "<key>"
|
||||
YOOKASSA_APIPAYOUTSECRET: "<secret>"
|
||||
```
|
||||
|
||||
### Ingress example
|
||||
|
||||
@@ -31,15 +31,15 @@
|
||||
3. Secret environment variables are injected from Secret {{ include "payouts.fullname" . }}:
|
||||
{{- end }}
|
||||
DATABASE_CONNECTION, YOOKASSA_APIBASEKEY, YOOKASSA_APIBASESECRET,
|
||||
YOOKASSA_APIPAYMENTKEY, YOOKASSA_APIPAYMENTSECRET
|
||||
YOOKASSA_APIPAYOUTKEY, YOOKASSA_APIPAYOUTSECRET
|
||||
|
||||
Before deploying to production, populate these values:
|
||||
helm upgrade {{ .Release.Name }} ./helm \
|
||||
--set secrets.DATABASE_CONNECTION="host=... dbname=..." \
|
||||
--set secrets.YOOKASSA_APIBASEKEY="<key>" \
|
||||
--set secrets.YOOKASSA_APIBASESECRET="<secret>" \
|
||||
--set secrets.YOOKASSA_APIPAYMENTKEY="<key>" \
|
||||
--set secrets.YOOKASSA_APIPAYMENTSECRET="<secret>"
|
||||
--set secrets.YOOKASSA_APIPAYOUTKEY="<key>" \
|
||||
--set secrets.YOOKASSA_APIPAYOUTSECRET="<secret>"
|
||||
|
||||
Or use a separate values file that is not committed to version control:
|
||||
helm upgrade {{ .Release.Name }} ./helm -f secrets.values.yaml
|
||||
|
||||
@@ -101,5 +101,5 @@ secrets:
|
||||
DATABASE_CONNECTION: ""
|
||||
YOOKASSA_APIBASEKEY: ""
|
||||
YOOKASSA_APIBASESECRET: ""
|
||||
YOOKASSA_APIPAYMENTKEY: ""
|
||||
YOOKASSA_APIPAYMENTSECRET: ""
|
||||
YOOKASSA_APIPAYOUTKEY: ""
|
||||
YOOKASSA_APIPAYOUTSECRET: ""
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
9
internal/api/widget/module.go
Normal file
9
internal/api/widget/module.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
var Module = fx.Options(
|
||||
fx.Provide(NewWidgetHandler),
|
||||
)
|
||||
55
internal/api/widget/widget_handler.go
Normal file
55
internal/api/widget/widget_handler.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ type YooKassa struct {
|
||||
|
||||
ApiBaseKey string
|
||||
ApiBaseSecret string
|
||||
ApiPaymentKey string
|
||||
ApiPaymentSecret string
|
||||
ApiPayoutKey string
|
||||
ApiPayoutSecret string
|
||||
WidgetVersion string
|
||||
|
||||
CallbackProcessTimeout time.Duration
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
45
internal/templates/payouts-widget.html
Normal file
45
internal/templates/payouts-widget.html
Normal 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>
|
||||
11
internal/templates/templates.go
Normal file
11
internal/templates/templates.go
Normal 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"))
|
||||
Reference in New Issue
Block a user