added support auto paid

This commit is contained in:
pasha1coil 2025-06-14 05:38:14 +03:00
parent 05089cab41
commit e30209c739
7 changed files with 176 additions and 32 deletions

@ -6,6 +6,7 @@ import (
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"gitea.pena/PenaSide/treasurer/internal/payment_provider"
"gitea.pena/PenaSide/treasurer/internal/utils"
"strconv"
"strings"
"sync"
@ -89,6 +90,7 @@ func (r *PaymentController) createPayment(ctx context.Context, paymentType model
},
Items: utils.ProtoItems2ReceiptItems(in.MainSettings.Items),
}),
"save_payment_method": strconv.FormatBool(in.MainSettings.Auto),
}
link, err := provider.CreateInvoice(ctx, request)
if err != nil {

@ -58,6 +58,7 @@ type CreatePayment[T any] struct {
UserID string
ClientIP string
Requisites T
Auto bool
}
type BankCard struct {

@ -1,5 +1,7 @@
package yandex
import "time"
// Payment description https://yookassa.ru/developers/api#payment_object
type Payment struct {
ID string `json:"id" bson:"id"`
@ -8,7 +10,7 @@ type Payment struct {
Confirmation *ConfirmationRedirect `json:"confirmation" bson:"confirmation"`
IncomeAmount *Amount `json:"income_amount,omitempty" bson:"income_amount,omitempty"`
Description string `json:"description,omitempty" bson:"description,omitempty"`
PaymentMethod any `json:"payment_method,omitempty" bson:"payment_method,omitempty"`
PaymentMethod *PaymentMethod `json:"payment_method,omitempty" bson:"payment_method,omitempty"`
Recipient Recipient `json:"recipient" bson:"recipient"`
CapturedAt string `json:"captured_at,omitempty" bson:"captured_at,omitempty"`
ExpiresAt string `json:"expires_at,omitempty" bson:"expires_at,omitempty"`
@ -42,3 +44,31 @@ const (
PaymentStatusSuccessfully PaymentStatus = "succeeded"
PaymentStatusCanceled PaymentStatus = "canceled"
)
type PaymentMethod struct {
ID string `json:"_id" bson:"_id,omitempty"`
UserID string `json:"userId" bson:"userId"`
Type string `json:"type" bson:"type"`
MethodID string `json:"id" bson:"id"`
Saved bool `json:"saved" bson:"saved"`
Card *Card `json:"card,omitempty" bson:"card,omitempty"`
Title string `json:"title" bson:"title"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
}
type Card struct {
First6 string `json:"first6" bson:"first6"`
Last4 string `json:"last4" bson:"last4"`
ExpiryMonth string `json:"expiry_month" bson:"expiry_month"`
ExpiryYear string `json:"expiry_year" bson:"expiry_year"`
CardType string `json:"card_type" bson:"card_type"`
CardProduct *CardProduct `json:"card_product,omitempty" bson:"card_product,omitempty"`
IssuerCountry string `json:"issuer_country" bson:"issuer_country"`
IssuerName string `json:"issuer_name" bson:"issuer_name"`
}
type CardProduct struct {
Code string `json:"code" bson:"code"`
Name string `json:"name" bson:"name"`
}

@ -10,33 +10,34 @@ type CreatePaymentRequest[T any] struct {
ClientIP string `json:"client_ip,omitempty"`
Deal any `json:"deal,omitempty"`
MerchantCustomerID any `json:"merchant_customer_id,omitempty"`
Receipt Receipt `json:"receipt,omitempty"`
Receipt Receipt `json:"receipt,omitempty"`
SavePaymentMethod bool `json:"save_payment_method"`
}
type Receipt struct {
Customer Customer `json:"customer,omitempty"`
Items []Item `json:"items,omitempty"`
TaxSystemCode int `json:"tax_system_code,omitempty"`
Customer Customer `json:"customer,omitempty"`
Items []Item `json:"items,omitempty"`
TaxSystemCode int `json:"tax_system_code,omitempty"`
}
type Item struct {
Description string `json:"description,omitempty"`
Amount ReceiptAmount `json:"amount,omitempty"`
VatCode int `json:"vat_code,omitempty"`
Quantity int64 `json:"quantity,omitempty"`
Measure string `json:"measure,omitempty"`
PaymentSubject string `json:"payment_subject,omitempty"`
PaymentMode string `json:"payment_mode,omitempty"`
Description string `json:"description,omitempty"`
Amount ReceiptAmount `json:"amount,omitempty"`
VatCode int `json:"vat_code,omitempty"`
Quantity int64 `json:"quantity,omitempty"`
Measure string `json:"measure,omitempty"`
PaymentSubject string `json:"payment_subject,omitempty"`
PaymentMode string `json:"payment_mode,omitempty"`
}
type ReceiptAmount struct {
Value string `json:"value,omitempty"`
Value string `json:"value,omitempty"`
Currency string `json:"currency,omitempty"`
}
type Customer struct {
FullName string `json:"full_name,omitempty"`
INN string `json:"inn,omitempty"`
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
INN string `json:"inn,omitempty"`
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
}

@ -29,27 +29,30 @@ type Config struct {
}
type Provider struct {
logger *zap.Logger
config *Config
httpClient *resty.Client
repository *repository.PaymentRepository
callbackService *callback.Service
logger *zap.Logger
config *Config
httpClient *resty.Client
repository *repository.PaymentRepository
callbackService *callback.Service
paymentMethodRepository *repository.PaymentMethodRepository
}
type Deps struct {
Logger *zap.Logger
Config *Config
Repository *repository.PaymentRepository
CallbackService *callback.Service
Logger *zap.Logger
Config *Config
Repository *repository.PaymentRepository
CallbackService *callback.Service
PaymentMethodRepository *repository.PaymentMethodRepository
}
func New(deps Deps) (*Provider, errors.Error) {
return &Provider{
logger: deps.Logger,
config: deps.Config,
httpClient: resty.New(),
repository: deps.Repository,
callbackService: deps.CallbackService,
logger: deps.Logger,
config: deps.Config,
httpClient: resty.New(),
repository: deps.Repository,
callbackService: deps.CallbackService,
paymentMethodRepository: deps.PaymentMethodRepository,
}, nil
}
@ -100,8 +103,9 @@ func (p *Provider) CreateInvoice(ctx context.Context, req map[string]string) (st
ReturnURL: request.ReturnURL,
Enforce: true,
},
Capture: true,
ClientIP: request.ClientIP,
Capture: true,
ClientIP: request.ClientIP,
SavePaymentMethod: request.Auto,
}).
Post(p.config.PaymentsURL)
@ -163,6 +167,42 @@ func (p *Provider) handleWebhook(ctx *fiber.Ctx) error {
p.logger.Error("failed to set payment complete", zap.Error(err))
return errors.HTTP(ctx, err)
}
if notification.Object.PaymentMethod != nil && notification.Object.PaymentMethod.Saved {
method := &yandex.PaymentMethod{
UserID: payment.UserID,
MethodID: notification.Object.PaymentMethod.ID,
Type: notification.Object.PaymentMethod.Type,
Title: notification.Object.PaymentMethod.Title,
Saved: notification.Object.PaymentMethod.Saved,
}
if notification.Object.PaymentMethod.Card != nil {
method.Card = &yandex.Card{
First6: notification.Object.PaymentMethod.Card.First6,
Last4: notification.Object.PaymentMethod.Card.Last4,
ExpiryMonth: notification.Object.PaymentMethod.Card.ExpiryMonth,
ExpiryYear: notification.Object.PaymentMethod.Card.ExpiryYear,
CardType: notification.Object.PaymentMethod.Card.CardType,
IssuerName: notification.Object.PaymentMethod.Card.IssuerName,
IssuerCountry: notification.Object.PaymentMethod.Card.IssuerCountry,
}
}
if notification.Object.PaymentMethod.Card.CardProduct != nil {
method.Card.CardProduct = &yandex.CardProduct{
Code: notification.Object.PaymentMethod.Card.CardProduct.Code,
Name: notification.Object.PaymentMethod.Card.CardProduct.Name,
}
}
if err := p.paymentMethodRepository.Save(ctx.Context(), method); err != nil {
p.logger.Error("failed to save payment method", zap.Error(err),
zap.String("userId", payment.UserID), zap.String("methodId", method.MethodID))
// todo стоит ли возвращать ошибку?
}
}
case yandex.WebhookEventPaymentCanceled:
payment, err = p.repository.SetPaymentStatus(ctx.Context(), notification.Object.ID, models.PaymentStatusCanceled)
if err != nil {

@ -0,0 +1,61 @@
package repository
import (
"context"
"gitea.pena/PenaSide/treasurer/internal/models/yandex"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
type PaymentMethodRepositoryDeps struct {
Logger *zap.Logger
Collection *mongo.Collection
}
type PaymentMethodRepository struct {
logger *zap.Logger
collection *mongo.Collection
}
func NewPaymentMethodRepository(deps PaymentMethodRepositoryDeps) *PaymentMethodRepository {
return &PaymentMethodRepository{
logger: deps.Logger,
collection: deps.Collection,
}
}
func (r *PaymentMethodRepository) Save(ctx context.Context, method *yandex.PaymentMethod) error {
_, err := r.collection.InsertOne(ctx, method)
if err != nil {
r.logger.Error("failed to save payment method", zap.Error(err), zap.String("userId", method.UserID))
return err
}
return nil
}
func (r *PaymentMethodRepository) GetByUserID(ctx context.Context, userID string) ([]*yandex.PaymentMethod, error) {
cursor, err := r.collection.Find(ctx, bson.M{"userId": userID})
if err != nil {
r.logger.Error("failed to find payment methods", zap.Error(err), zap.String("userId", userID))
return nil, err
}
defer cursor.Close(ctx)
var methods []*yandex.PaymentMethod
if err := cursor.All(ctx, &methods); err != nil {
r.logger.Error("failed to decode payment methods", zap.Error(err), zap.String("userId", userID))
return nil, err
}
return methods, nil
}
func (r *PaymentMethodRepository) DeleteByUserID(ctx context.Context, userID string) error {
_, err := r.collection.DeleteMany(ctx, bson.M{"userId": userID})
if err != nil {
r.logger.Error("failed to delete payment methods", zap.Error(err), zap.String("userId", userID))
return err
}
return nil
}

@ -29,6 +29,14 @@ func MapToCreatePaymentYandexReceipt(data map[string]string) (*models.CreatePaym
return nil, fmt.Errorf("missing requisites field")
}
auto := false
if val, ok := data["save_payment_method"]; ok && val != "" {
auto, err = strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf("invalid save_payment_method field: %w", err)
}
}
payment := &models.CreatePayment[yandex.Receipt]{
Type: models.PaymentType(data["type"]),
Currency: data["currency"],
@ -38,6 +46,7 @@ func MapToCreatePaymentYandexReceipt(data map[string]string) (*models.CreatePaym
UserID: data["user_id"],
ClientIP: data["client_ip"],
Requisites: requisites,
Auto: auto,
}
return payment, nil