create CreateInvoice(ctx context.Context, request map[string]string) (string, errors.Error) and move rest payment to grpc

This commit is contained in:
pasha1coil 2025-06-11 18:12:44 +03:00
parent edf077736b
commit dfbff4d86b
10 changed files with 155 additions and 287 deletions

@ -81,7 +81,7 @@ func Run(ctx context.Context, config initialize.Config, logger *zap.Logger) (app
httpServer := http.NewServer(http.ServerConfig{
Logger: logger,
Controllers: []http.Controller{controllers.YandexStatusREST, controllers.PaymentREST},
Controllers: []http.Controller{controllers.YandexStatusREST},
})
grpcServer, err := grpc.NewGRPC(logger)

@ -2,28 +2,29 @@ package grpc
import (
"context"
"fmt"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"gitea.pena/PenaSide/treasurer/internal/payment_provider"
"gitea.pena/PenaSide/treasurer/internal/utils"
"strings"
"sync"
"gitea.pena/PenaSide/treasurer/internal/errors"
"gitea.pena/PenaSide/treasurer/internal/models"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"gitea.pena/PenaSide/treasurer/internal/proto/treasurer"
"gitea.pena/PenaSide/treasurer/internal/utils"
"go.uber.org/zap"
)
type PaymentService interface {
CreatePaymentBankCard(context.Context, *models.CreatePayment[yandex.Receipt]) (string, errors.Error)
CreatePayment(context.Context, *models.CreatePayment[yandex.Receipt]) (string, errors.Error)
}
type PaymentControllerDeps struct {
Logger *zap.Logger
PaymentService PaymentService
PaymentProviders []payment_provider.PaymentProvider
}
type PaymentController struct {
logger *zap.Logger
paymentService PaymentService
paymentProviders []payment_provider.PaymentProvider
providerMap map[models.PaymentType][]payment_provider.PaymentProvider
mu sync.RWMutex
}
func NewPaymentController(deps PaymentControllerDeps) (*PaymentController, errors.Error) {
@ -31,169 +32,96 @@ func NewPaymentController(deps PaymentControllerDeps) (*PaymentController, error
return nil, errors.NewWithMessage("Logger in nil on <NewPaymentController>", errors.ErrInvalidArgs)
}
if deps.PaymentService == nil {
return nil, errors.NewWithMessage("PaymentService in nil on <NewPaymentController>", errors.ErrInvalidArgs)
if len(deps.PaymentProviders) == 0 {
return nil, errors.NewWithMessage("No payment providers provided on <NewPaymentController>", errors.ErrInvalidArgs)
}
return &PaymentController{
controller := &PaymentController{
logger: deps.Logger,
paymentService: deps.PaymentService,
}, nil
paymentProviders: deps.PaymentProviders,
providerMap: make(map[models.PaymentType][]payment_provider.PaymentProvider),
}
for _, provider := range deps.PaymentProviders {
for _, method := range provider.GetSupportedPaymentMethods() {
controller.providerMap[method] = append(controller.providerMap[method], provider)
}
}
return controller, nil
}
func (receiver *PaymentController) getProviderForPaymentMethod(method models.PaymentType) (payment_provider.PaymentProvider, errors.Error) {
receiver.mu.RLock()
defer receiver.mu.RUnlock()
providers, exists := receiver.providerMap[method]
if !exists || len(providers) == 0 {
return nil, errors.NewWithMessage("no provider found for payment method", errors.ErrNotFound)
}
return providers[0], nil
}
func (receiver *PaymentController) createPayment(ctx context.Context, paymentType models.PaymentType, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
provider, err := receiver.getProviderForPaymentMethod(paymentType)
if err != nil {
receiver.logger.Error("failed to get payment provider",
zap.String("paymentMethod", string(paymentType)),
zap.Error(err))
return nil, errors.GRPC("failed to get payment provider", err)
}
request := map[string]string{
"type": string(models.PaymentTypeBankCard),
"currency": in.MainSettings.Currency,
"amount": fmt.Sprintf("%d", in.MainSettings.Amount),
"callback_host_grpc": strings.Join(in.MainSettings.CallbackHostGRPC, ","),
"return_url": in.MainSettings.ReturnURL,
"user_id": in.MainSettings.UserID,
"client_ip": in.MainSettings.ClientIP,
"requisites": utils.ToJSON(yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
INN: in.MainSettings.Customer.INN,
Email: in.MainSettings.Customer.Email,
Phone: in.MainSettings.Customer.Phone,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
}),
}
link, err := provider.CreateInvoice(ctx, request)
if err != nil {
receiver.logger.Error("failed to create payment",
zap.String("provider", provider.GetName()),
zap.Error(err))
return nil, errors.GRPC("failed to create payment", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
}
func (receiver *PaymentController) GetPaymentLinkBankCard(ctx context.Context, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
receiver.logger.Info("f<GetPaymentLinkBankCard> of <PaymentController>", zap.Any("Customer", in.MainSettings.Customer))
link, err := receiver.paymentService.CreatePaymentBankCard(ctx, &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentTypeBankCard,
Currency: in.MainSettings.Currency,
UserID: in.MainSettings.UserID,
ClientIP: in.MainSettings.ClientIP,
Amount: in.MainSettings.Amount,
CallbackHostGRPC: in.MainSettings.CallbackHostGRPC,
ReturnURL: in.MainSettings.ReturnURL,
Requisites: yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
Email: in.MainSettings.Customer.Email,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
},
})
if err != nil {
receiver.logger.Error("failed to get payment link on <GetPaymentLinkBankCard> of <PaymentController>", zap.Error(err))
return nil, errors.GRPC("failed to get payment link", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
return receiver.createPayment(ctx, models.PaymentTypeBankCard, in)
}
func (receiver *PaymentController) GetPaymentLinkYooMoney(ctx context.Context, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
link, err := receiver.paymentService.CreatePayment(ctx, &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentTypeYoomoney,
Currency: in.MainSettings.Currency,
UserID: in.MainSettings.UserID,
ClientIP: in.MainSettings.ClientIP,
Amount: in.MainSettings.Amount,
CallbackHostGRPC: in.MainSettings.CallbackHostGRPC,
ReturnURL: in.MainSettings.ReturnURL,
Requisites: yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
Email: in.MainSettings.Customer.Email,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
},
})
if err != nil {
receiver.logger.Error("failed to get payment link on <GetPaymentLinkYooMoney> of <PaymentController>", zap.Error(err))
return nil, errors.GRPC("failed to get payment link", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
return receiver.createPayment(ctx, models.PaymentTypeYoomoney, in)
}
func (receiver *PaymentController) GetPaymentLinkTinkoff(ctx context.Context, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
link, err := receiver.paymentService.CreatePayment(ctx, &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentTypeTinkoff,
Currency: in.MainSettings.Currency,
UserID: in.MainSettings.UserID,
ClientIP: in.MainSettings.ClientIP,
Amount: in.MainSettings.Amount,
CallbackHostGRPC: in.MainSettings.CallbackHostGRPC,
ReturnURL: in.MainSettings.ReturnURL,
Requisites: yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
Email: in.MainSettings.Customer.Email,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
},
})
if err != nil {
receiver.logger.Error("failed to get payment link on <GetPaymentLinkTinkoff> of <PaymentController>", zap.Error(err))
return nil, errors.GRPC("failed to get payment link", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
return receiver.createPayment(ctx, models.PaymentTypeTinkoff, in)
}
func (receiver *PaymentController) GetPaymentLinkSBP(ctx context.Context, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
link, err := receiver.paymentService.CreatePayment(ctx, &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentTypeSBP,
Currency: in.MainSettings.Currency,
UserID: in.MainSettings.UserID,
ClientIP: in.MainSettings.ClientIP,
Amount: in.MainSettings.Amount,
CallbackHostGRPC: in.MainSettings.CallbackHostGRPC,
ReturnURL: in.MainSettings.ReturnURL,
Requisites: yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
Email: in.MainSettings.Customer.Email,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
},
})
if err != nil {
receiver.logger.Error("failed to get payment link on <GetPaymentLinkSBP> of <PaymentController>", zap.Error(err))
return nil, errors.GRPC("failed to get payment link", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
return receiver.createPayment(ctx, models.PaymentTypeSBP, in)
}
func (receiver *PaymentController) GetPaymentLinkSberPay(ctx context.Context, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
link, err := receiver.paymentService.CreatePayment(ctx, &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentTypeSberPay,
Currency: in.MainSettings.Currency,
UserID: in.MainSettings.UserID,
ClientIP: in.MainSettings.ClientIP,
Amount: in.MainSettings.Amount,
CallbackHostGRPC: in.MainSettings.CallbackHostGRPC,
ReturnURL: in.MainSettings.ReturnURL,
Requisites: yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
Email: in.MainSettings.Customer.Email,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
},
})
if err != nil {
receiver.logger.Error("failed to get payment link on <GetPaymentLinkSberPay> of <PaymentController>", zap.Error(err))
return nil, errors.GRPC("failed to get payment link", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
return receiver.createPayment(ctx, models.PaymentTypeSberPay, in)
}
func (receiver *PaymentController) GetPaymentLinkSberbankB2B(ctx context.Context, in *treasurer.GetPaymentLinkRequest) (*treasurer.GetPaymentLinkResponse, error) {
link, err := receiver.paymentService.CreatePayment(ctx, &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentTypeSberB2B,
Currency: in.MainSettings.Currency,
UserID: in.MainSettings.UserID,
ClientIP: in.MainSettings.ClientIP,
Amount: in.MainSettings.Amount,
CallbackHostGRPC: in.MainSettings.CallbackHostGRPC,
ReturnURL: in.MainSettings.ReturnURL,
Requisites: yandex.Receipt{
TaxSystemCode: 2,
Customer: yandex.Customer{
FullName: in.MainSettings.Customer.FullName,
Email: in.MainSettings.Customer.Email,
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
},
})
if err != nil {
receiver.logger.Error("failed to get payment link on <GetPaymentLinkB2B> of <PaymentController>", zap.Error(err))
return nil, errors.GRPC("failed to get payment link", err)
}
return &treasurer.GetPaymentLinkResponse{RedirectURL: link}, nil
return receiver.createPayment(ctx, models.PaymentTypeSberB2B, in)
}

@ -1,85 +0,0 @@
package payment
import (
"fmt"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"gitea.pena/PenaSide/treasurer/internal/payment_provider"
"github.com/gofiber/fiber/v2"
"sync"
"gitea.pena/PenaSide/treasurer/internal/errors"
"gitea.pena/PenaSide/treasurer/internal/models"
"go.uber.org/zap"
)
type PaymentControllerDeps struct {
Logger *zap.Logger
PaymentProviders []payment_provider.PaymentProvider
}
type PaymentController struct {
logger *zap.Logger
paymentProviders []payment_provider.PaymentProvider
providerMap map[models.PaymentType][]payment_provider.PaymentProvider
mu sync.RWMutex
}
func NewPaymentController(deps PaymentControllerDeps) (*PaymentController, errors.Error) {
if deps.Logger == nil {
return nil, errors.NewWithMessage("Logger is nil on <NewPaymentController>", errors.ErrInvalidArgs)
}
if len(deps.PaymentProviders) == 0 {
return nil, errors.NewWithMessage("No payment providers provided on <NewPaymentController>", errors.ErrInvalidArgs)
}
controller := &PaymentController{
logger: deps.Logger,
paymentProviders: deps.PaymentProviders,
providerMap: make(map[models.PaymentType][]payment_provider.PaymentProvider),
}
for _, provider := range deps.PaymentProviders {
for _, method := range provider.GetSupportedPaymentMethods() {
controller.providerMap[method] = append(controller.providerMap[method], provider)
}
}
return controller, nil
}
// TODO Добавить более сложную логику выбора провайдера (приоритеты, доступность и т.д.)
func (receiver *PaymentController) getProviderForPaymentMethod(method models.PaymentType) (payment_provider.PaymentProvider, errors.Error) {
receiver.mu.RLock()
defer receiver.mu.RUnlock()
providers, exists := receiver.providerMap[method]
if !exists || len(providers) == 0 {
return nil, errors.NewWithMessage("no provider found for payment method", errors.ErrNotFound)
}
return providers[0], nil
}
// обрабатывает запрос на создание платежа
func (receiver *PaymentController) CreatePayment(ctx *fiber.Ctx) error {
var req models.CreatePayment[yandex.Receipt]
if err := ctx.BodyParser(&req); err != nil {
receiver.logger.Error("failed to parse payment request", zap.Error(err))
return errors.HTTP(ctx, errors.NewWithError(fmt.Errorf("failed to parse input body: %w", err), errors.ErrInternalError))
}
provider, err := receiver.getProviderForPaymentMethod(req.Type)
if err != nil {
receiver.logger.Error("failed to get payment provider", zap.String("paymentMethod", string(req.Type)), zap.Error(err))
return errors.HTTP(ctx, err)
}
// todo потом нужно будет сделать так чтобы работало не только с yandex.Receipt
resp, err := provider.CreateInvoice(ctx.Context(), &req)
if err != nil {
receiver.logger.Error("failed to create payment", zap.String("provider", provider.GetName()), zap.Error(err))
return errors.HTTP(ctx, err)
}
return ctx.JSON(resp)
}

@ -1,18 +0,0 @@
package payment
import "github.com/gofiber/fiber/v2"
func (receiver *PaymentController) Register(router fiber.Router) {
// вебхуки для каждого провайдера
for _, provider := range receiver.paymentProviders {
provider.RegisterWebhookHandlers(router)
}
// эндпоинт создания платежа
router.Post("/payment", receiver.CreatePayment)
}
func (receiver *PaymentController) Name() string {
return ""
}

@ -2,7 +2,6 @@ package initialize
import (
"gitea.pena/PenaSide/treasurer/internal/controller/grpc"
"gitea.pena/PenaSide/treasurer/internal/controller/http_controllers/payment"
"gitea.pena/PenaSide/treasurer/internal/controller/http_controllers/yandex"
"gitea.pena/PenaSide/treasurer/internal/errors"
"go.uber.org/zap"
@ -16,7 +15,6 @@ type ControllersDeps struct {
type Controllers struct {
YandexStatusREST *yandex.YandexStatusController
PaymentGRPC *grpc.PaymentController
PaymentREST *payment.PaymentController
}
func NewControllers(deps ControllersDeps) (*Controllers, errors.Error) {
@ -30,14 +28,6 @@ func NewControllers(deps ControllersDeps) (*Controllers, errors.Error) {
}
paymentControllerGRPC, err := grpc.NewPaymentController(grpc.PaymentControllerDeps{
Logger: deps.Logger,
PaymentService: deps.Services.Payment,
})
if err != nil {
return nil, err
}
paymentControllerREST, err := payment.NewPaymentController(payment.PaymentControllerDeps{
Logger: deps.Logger,
PaymentProviders: deps.Services.PaymentProviders,
})
@ -48,6 +38,5 @@ func NewControllers(deps ControllersDeps) (*Controllers, errors.Error) {
return &Controllers{
YandexStatusREST: yandexStatusControllerREST,
PaymentGRPC: paymentControllerGRPC,
PaymentREST: paymentControllerREST,
}, nil
}

@ -4,7 +4,6 @@ import (
"context"
"gitea.pena/PenaSide/treasurer/internal/errors"
"gitea.pena/PenaSide/treasurer/internal/models"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"github.com/gofiber/fiber/v2"
)
@ -12,6 +11,5 @@ type PaymentProvider interface {
GetName() string
RegisterWebhookHandlers(router fiber.Router)
GetSupportedPaymentMethods() []models.PaymentType
// todo потом нужно будет сделать так чтобы работало не только с yandex.Receipt
CreateInvoice(ctx context.Context, request *models.CreatePayment[yandex.Receipt]) (string, errors.Error)
CreateInvoice(ctx context.Context, request map[string]string) (string, errors.Error)
}

@ -0,0 +1,44 @@
package utils
import (
"encoding/json"
"fmt"
"gitea.pena/PenaSide/treasurer/internal/models"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"strconv"
"strings"
)
func MapToCreatePaymentYandexReceipt(data map[string]string) (*models.CreatePayment[yandex.Receipt], error) {
amount, err := strconv.ParseInt(data["amount"], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid amount: %w", err)
}
var callbackHosts []string
if val, ok := data["callback_host_grpc"]; ok && val != "" {
callbackHosts = strings.Split(val, ",")
}
var requisites yandex.Receipt
if val, ok := data["requisites"]; ok && val != "" {
if err := json.Unmarshal([]byte(val), &requisites); err != nil {
return nil, fmt.Errorf("invalid requisites JSON: %w", err)
}
} else {
return nil, fmt.Errorf("missing requisites field")
}
payment := &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentType(data["type"]),
Currency: data["currency"],
Amount: amount,
CallbackHostGRPC: callbackHosts,
ReturnURL: data["return_url"],
UserID: data["user_id"],
ClientIP: data["client_ip"],
Requisites: requisites,
}
return payment, nil
}

@ -2,10 +2,11 @@ package utils
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"gitea.pena/PenaSide/treasurer/internal/proto/treasurer"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"gitea.pena/PenaSide/treasurer/internal/proto/treasurer"
"strings"
)
func ConvertYoomoneySercetsToAuth(authType, storeID, secretKey string) string {
@ -41,3 +42,8 @@ func ProtoItems2ReceiptItems(in []*treasurer.Item) []yandex.Item {
return out
}
func ToJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}

@ -71,7 +71,13 @@ func (p *Provider) GetSupportedPaymentMethods() []models.PaymentType {
}
}
func (p *Provider) CreateInvoice(ctx context.Context, request *models.CreatePayment[yandex.Receipt]) (string, errors.Error) {
// *models.CreatePayment[yandex.Receipt]
func (p *Provider) CreateInvoice(ctx context.Context, req map[string]string) (string, errors.Error) {
request, err := utils.MapToCreatePaymentYandexReceipt(req)
if err != nil {
p.logger.Error("failed to create payment yandex receipt by parse map", zap.Error(err))
return "", errors.NewWithMessage("failed to parse input request by parse map", errors.ErrInvalidArgs)
}
idempotenceKey := uuid.New().String()
yandexPayment, err := p.httpClient.R().