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/payment_provider" "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 PaymentRepository callbackService CallbackService } type PaymentRepository interface { Insert(context.Context, *models.Payment) (*models.Payment, errors.Error) SetPaymentStatus(ctx context.Context, paymentID string, status models.PaymentStatus) (*models.Payment, errors.Error) SetPaymentComplete(ctx context.Context, paymentID string) (*models.Payment, errors.Error) } type CallbackService interface { OnSuccess(context.Context, *models.Event) errors.Error OnFailure(context.Context, *models.Event) errors.Error } func New(logger *zap.Logger, config *Config, repository PaymentRepository, callbackService CallbackService) (*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, } } // todo func (p *Provider) CreateInvoice(ctx context.Context, request *payment_provider.PaymentRequest) (*payment_provider.PaymentResponse, errors.Error) { 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.PaymentMethod]}, 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.PaymentMethod))) return nil, 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 nil, 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 nil, errors.NewWithError(fmt.Errorf("failed to unmarshal payment response: %w", err), errors.ErrInternalError) } dbPayment, 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.PaymentMethod, 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 nil, errors.NewWithError(fmt.Errorf("failed to save payment to database: %w", err), errors.ErrInternalError) } return &payment_provider.PaymentResponse{ RedirectURL: payment.Confirmation.ConfirmationURL, Payment: dbPayment, }, 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) }