package yoomoney import ( "context" "encoding/json" "fmt" "gitea.pena/PenaSide/treasurer/internal/errors" "gitea.pena/PenaSide/treasurer/internal/models" "gitea.pena/PenaSide/treasurer/internal/models/yandex" "gitea.pena/PenaSide/treasurer/internal/repository" "gitea.pena/PenaSide/treasurer/internal/service/callback" "gitea.pena/PenaSide/treasurer/internal/utils" "github.com/go-resty/resty/v2" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "go.uber.org/zap" "net/http" ) const ( ProviderName = "yoomoney" ) type Config struct { StoreID string `env:"YOOMONEY_STORE_ID"` SecretKey string `env:"YOOMONEY_SECRET_KEY"` WebhooksURL string `env:"YOOMONEY_WEBHOOKS_URL"` PaymentsURL string `env:"YOOMONEY_PAYMENTS_URL"` } type Provider struct { logger *zap.Logger config *Config httpClient *resty.Client repository *repository.PaymentRepository callbackService *callback.Service } func New(logger *zap.Logger, config *Config, repository *repository.PaymentRepository, callbackService *callback.Service) (*Provider, errors.Error) { return &Provider{ logger: logger, config: config, httpClient: resty.New(), repository: repository, callbackService: callbackService, }, nil } func (p *Provider) GetName() string { return ProviderName } func (p *Provider) GetSupportedPaymentMethods() []models.PaymentType { return []models.PaymentType{ models.PaymentTypeBankCard, models.PaymentTypeTinkoff, models.PaymentTypeSberPay, models.PaymentTypeYoomoney, models.PaymentTypeMobile, models.PaymentTypeSBP, models.PaymentTypeSberB2B, } } // *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(). SetContext(ctx). SetHeader("Content-Type", "application/json"). SetHeader("Idempotence-Key", idempotenceKey). SetHeader("Authorization", utils.ConvertYoomoneySercetsToAuth("Basic", p.config.StoreID, p.config.SecretKey)). SetBody(&yandex.CreatePaymentRequest[yandex.PaymentMethodType]{ Amount: yandex.Amount{ Value: utils.ConvertAmountToStringFloat(request.Amount), Currency: request.Currency, }, Receipt: yandex.Receipt{ TaxSystemCode: 2, //https://yookassa.ru/developers/payment-acceptance/receipts/54fz/other-services/parameters-values#tax-systems Customer: request.Requisites.Customer, Items: request.Requisites.Items, }, PaymentMethodData: &yandex.PaymentMethodType{Type: models.YandexPaymentTypeMap[request.Type]}, Confirmation: &yandex.CreateConfirmationRedirect{ Type: yandex.ConfirmationTypeRedirect, Locale: "ru_RU", ReturnURL: request.ReturnURL, Enforce: true, }, Capture: true, ClientIP: request.ClientIP, }). Post(p.config.PaymentsURL) if err != nil { p.logger.Error("failed to create payment", zap.Error(err), zap.String("payment_method", string(request.Type))) return "", errors.NewWithError(fmt.Errorf("failed to create payment: %w", err), errors.ErrInternalError) } if yandexPayment.StatusCode() != http.StatusOK { p.logger.Error("failed to create payment", zap.Int("status_code", yandexPayment.StatusCode()), zap.String("response", yandexPayment.String())) return "", errors.NewWithMessage("failed to create payment", errors.ErrInternalError) } var payment yandex.Payment if err := json.Unmarshal(yandexPayment.Body(), &payment); err != nil { p.logger.Error("failed to unmarshal payment response", zap.Error(err)) return "", errors.NewWithError(fmt.Errorf("failed to unmarshal payment response: %w", err), errors.ErrInternalError) } _, err = p.repository.Insert(ctx, &models.Payment{ UserID: request.UserID, PaymentID: payment.ID, IdempotencePaymentID: idempotenceKey, ClientIP: request.ClientIP, Currency: request.Currency, Amount: request.Amount, Type: request.Type, Status: models.PaymentStatusMap[string(payment.Status)], Completed: false, RawPaymentBody: payment, CallbackHostGRPC: request.CallbackHostGRPC, }) if err != nil { p.logger.Error("failed to save payment to database", zap.Error(err)) return "", errors.NewWithError(fmt.Errorf("failed to save payment to database: %w", err), errors.ErrInternalError) } return payment.Confirmation.ConfirmationURL, nil } func (p *Provider) RegisterWebhookHandlers(router fiber.Router) { router.Post(p.config.WebhooksURL, p.handleWebhook) } func (p *Provider) handleWebhook(ctx *fiber.Ctx) error { var notification yandex.WebhookNotification[yandex.Payment] if err := ctx.BodyParser(¬ification); err != nil { p.logger.Error("failed to parse webhook notification", zap.Error(err)) return errors.HTTP(ctx, errors.NewWithError(fmt.Errorf("failed to parse webhook notification: %w", err), errors.ErrInternalError)) } var payment *models.Payment var err errors.Error switch notification.Event { case yandex.WebhookEventPaymentSucceeded: payment, err = p.repository.SetPaymentStatus(ctx.Context(), notification.Object.ID, models.PaymentStatusSuccessfully) if err != nil { p.logger.Error("failed to set payment complete", zap.Error(err)) return errors.HTTP(ctx, err) } case yandex.WebhookEventPaymentCanceled: payment, err = p.repository.SetPaymentStatus(ctx.Context(), notification.Object.ID, models.PaymentStatusCanceled) if err != nil { p.logger.Error("failed to set payment status canceled", zap.Error(err)) return errors.HTTP(ctx, err) } case yandex.WebhookEventPaymentWaiting: payment, err = p.repository.SetPaymentStatus(ctx.Context(), notification.Object.ID, models.PaymentStatusWaiting) if err != nil { p.logger.Error("failed to set payment status waiting", zap.Error(err)) return errors.HTTP(ctx, err) } case yandex.WebhookEventRefundSucceeded: payment, err = p.repository.SetPaymentStatus(ctx.Context(), notification.Object.ID, models.PaymentStatusRefund) if err != nil { p.logger.Error("failed to set payment status refund", zap.Error(err)) return errors.HTTP(ctx, err) } default: p.logger.Warn("unknown webhook event type", zap.String("event", string(notification.Event))) return errors.HTTP(ctx, errors.NewWithMessage(fmt.Sprintf("unknown webhook event type: %s", notification.Event), errors.ErrInvalidArgs)) } if payment != nil { event := &models.Event{ Key: string(notification.Event), Message: fmt.Sprintf("yoomoney send event: %s", notification.Event), Payment: payment, } if notification.Event == yandex.WebhookEventPaymentSucceeded { if err := p.callbackService.OnSuccess(ctx.Context(), event); err != nil { p.logger.Error("failed to send success callback", zap.Error(err)) return errors.HTTP(ctx, err) } } if notification.Event == yandex.WebhookEventPaymentCanceled { if err := p.callbackService.OnFailure(ctx.Context(), event); err != nil { p.logger.Error("failed to send failure callback", zap.Error(err)) return errors.HTTP(ctx, err) } } } return ctx.SendStatus(http.StatusOK) }