package swagger import ( "fmt" "math" "net/http" "sync" "time" "github.com/labstack/echo/v4" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/broker/tariff" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/client" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/repository" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/proto/discount" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/service/history" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/utils" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/utils/transfer" "penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/echotools" "penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/validate" ) // TODO update echo errors const defaultCurrency = "RUB" // TODO move type API2 struct { logger *zap.Logger history repository.HistoryRepository account repository.AccountRepository currency repository.CurrencyRepository producer *tariff.Producer consumer *tariff.Consumer clients clients grpc models.ConfigurationGRPC } type clients struct { auth *client.AuthClient hubadmin *client.HubadminClient currency *client.CurrencyClient discount *client.DiscountClient payment *client.PaymentClient } var _ ServerInterface = (*API2)(nil) func NewAPI2(logger *zap.Logger, db *mongo.Database, config *models.Config, consumer *tariff.Consumer, producer *tariff.Producer) API2 { return API2{ logger: logger, history: repository.NewHistoryRepository2(logger, db.Collection("histories")), currency: repository.NewCurrencyRepository2(logger, db.Collection("currency_lists")), account: repository.NewAccountRepository2(logger, db.Collection("accounts")), consumer: consumer, producer: producer, grpc: config.GRPC, clients: clients{ auth: client.NewAuthClient(client.AuthClientDeps{Logger: logger, URLs: &config.Service.AuthMicroservice.URL}), hubadmin: client.NewHubadminClient(client.HubadminClientDeps{Logger: logger, URLs: &config.Service.HubadminMicroservice.URL}), currency: client.NewCurrencyClient(client.CurrencyClientDeps{Logger: logger, URLs: &config.Service.CurrencyMicroservice.URL}), discount: client.NewDiscountClient(client.DiscountClientDeps{Logger: logger, DiscountServiceHost: config.Service.DiscountMicroservice.HostGRPC}), payment: client.NewPaymentClient(client.PaymentClientDeps{Logger: logger, PaymentServiceHost: config.Service.PaymentMicroservice.HostGRPC}), }, } } func (api *API2) error(ctx echo.Context, status int, message string, rest ...any) error { if len(rest) > 0 { message = fmt.Sprintf(message, rest...) } api.logger.Error(message) return ctx.JSON(status, models.ResponseErrorHTTP{ StatusCode: status, Message: message, }) } func (api *API2) noauth(ctx echo.Context) error { return api.error(ctx, http.StatusUnauthorized, "failed to get jwt payload") } // Health func (api *API2) GetHealth(ctx echo.Context) error { return ctx.String(http.StatusOK, "OK") } // Account func (api *API2) DeleteAccount(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } account, err := api.account.Remove(ctx.Request().Context(), userID) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) ChangeAccount(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } request, bindErr := echotools.Bind[models.Name](ctx) if bindErr != nil { return api.error(ctx, http.StatusBadRequest, "failed to bind json: %w", bindErr) } account, err := api.account.UpdateName(ctx.Request().Context(), userID, request) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) SetAccountVerificationStatus(ctx echo.Context, userID string) error { request, bindErr := echotools.Bind[models.SetAccountStatus](ctx) if bindErr != nil { return api.error(ctx, http.StatusBadRequest, "failed to bind json: %w", bindErr) } account, err := api.account.SetStatus(ctx.Request().Context(), userID, request.Status) if err != nil { api.logger.Error("failed to set status on of ", zap.Error(err)) return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) GetAccount(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } account, err := api.account.FindByUserID(ctx.Request().Context(), userID) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) AddAccount(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } account, err := api.account.FindByUserID(ctx.Request().Context(), userID) if err != nil && err.Type() != errors.ErrNotFound { return errors.HTTP(ctx, err) } if account != nil { return api.error(ctx, http.StatusBadRequest, "account exists") } user, err := api.clients.auth.GetUser(ctx.Request().Context(), userID) if err != nil { return errors.HTTP(ctx, err) } account, err = api.account.Insert(ctx.Request().Context(), &models.Account{UserID: user.ID, Wallet: models.Wallet{Currency: defaultCurrency}}) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) DeleteDirectAccount(ctx echo.Context, userID string) error { account, err := api.account.Remove(ctx.Request().Context(), userID) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) GetDirectAccount(ctx echo.Context, userID string) error { account, err := api.account.FindByUserID(ctx.Request().Context(), userID) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, account) } func (api *API2) PaginationAccounts(ctx echo.Context, params PaginationAccountsParams) error { if params.Page == nil || params.Limit == nil { return api.error(ctx, http.StatusInternalServerError, "default values missing for PaginationAccounts") } page := int64(max(*params.Page, 1)) limit := min(int64(max(*params.Limit, 1)), models.DefaultLimit) count, err := api.account.CountAll(ctx.Request().Context()) if err != nil { return errors.HTTP(ctx, err) } if count == 0 { response := models.PaginationResponse[models.Account]{TotalPages: 0, Records: []models.Account{}} return ctx.JSON(http.StatusOK, response) } totalPages := int64(math.Ceil(float64(count) / float64(limit))) accounts, err := api.account.FindMany(ctx.Request().Context(), page, limit) if err != nil { return errors.HTTP(ctx, err) } response := models.PaginationResponse[models.Account]{ TotalPages: totalPages, Records: accounts, } return ctx.JSON(http.StatusOK, response) } // Cart func (api *API2) RemoveFromCart(ctx echo.Context, params RemoveFromCartParams) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } if validate.IsStringEmpty(params.Id) { return api.error(ctx, http.StatusBadRequest, "empty item id") } cartItems, err := api.account.RemoveItemFromCart(ctx.Request().Context(), userID, params.Id) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, cartItems) } func (api *API2) Add2cart(ctx echo.Context, params Add2cartParams) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } token, ok := ctx.Get(models.AuthJWTDecodedAccessTokenKey).(string) if !ok { return api.noauth(ctx) } if validate.IsStringEmpty(params.Id) { return api.error(ctx, http.StatusBadRequest, "empty item id") } tariffID := params.Id tariff, err := api.clients.hubadmin.GetTariff(ctx.Request().Context(), token, tariffID) if err != nil { return errors.HTTP(ctx, err) } if tariff == nil { return api.error(ctx, http.StatusNotFound, "tariff not found") } cartItems, err := api.account.AddItemToCart(ctx.Request().Context(), userID, tariffID) if err != nil { return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, cartItems) } func (api *API2) PayCart(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { return api.noauth(ctx) } accessToken, ok := ctx.Get(models.AuthJWTDecodedAccessTokenKey).(string) if !ok { return api.noauth(ctx) } account, err := api.account.FindByUserID(ctx.Request().Context(), userID) if err != nil { return errors.HTTP(ctx, err) } api.logger.Info("account for pay", zap.Any("acc", account)) tariffs, err := api.clients.hubadmin.GetTariffs(ctx.Request().Context(), accessToken, account.Cart) if err != nil { return errors.HTTP(ctx, err) } api.logger.Info("tariffs for pay", zap.Any("acc", tariffs)) tariffsAmount := utils.CalculateCartPurchasesAmount(tariffs) discountResponse, err := api.clients.discount.Apply(ctx.Request().Context(), &discount.ApplyDiscountRequest{ UserInformation: &discount.UserInformation{ ID: account.UserID, Type: string(account.Status), PurchasesAmount: uint64(account.Wallet.PurchasesAmount), CartPurchasesAmount: tariffsAmount, }, Products: transfer.TariffsToProductInformations(tariffs), Date: timestamppb.New(time.Now()), }) if err != nil { return errors.HTTP(ctx, err) } api.logger.Info("discountResponse for pay", zap.Any("acc", discount.ApplyDiscountRequest{ UserInformation: &discount.UserInformation{ ID: account.UserID, Type: string(account.Status), PurchasesAmount: uint64(account.Wallet.PurchasesAmount), CartPurchasesAmount: tariffsAmount, }, Products: transfer.TariffsToProductInformations(tariffs), Date: timestamppb.New(time.Now()), })) if account.Wallet.Money < int64(discountResponse.Price) { return api.error(ctx, http.StatusPaymentRequired, "insufficient funds: %d", int64(discountResponse.Price)-account.Wallet.Money) } // WithdrawAccountWalletMoney request := models.WithdrawAccountWallet{ Money: int64(discountResponse.Price), Account: account, } if validate.IsStringEmpty(request.Account.Wallet.Currency) { request.Account.Wallet.Currency = models.InternalCurrencyKey } var updatedAccount *models.Account if request.Account.Wallet.Currency == models.InternalCurrencyKey { accountx, err := api.account.ChangeWallet(ctx.Request().Context(), request.Account.UserID, &models.Wallet{ Cash: request.Account.Wallet.Cash - request.Money, Money: request.Account.Wallet.Money - request.Money, Spent: request.Account.Wallet.Spent + request.Money, PurchasesAmount: request.Account.Wallet.PurchasesAmount, Currency: request.Account.Wallet.Currency, }) if err != nil { return errors.HTTP(ctx, err) } updatedAccount = accountx } else { cash, err := api.clients.currency.Translate(ctx.Request().Context(), &models.TranslateCurrency{ Money: request.Money, From: models.InternalCurrencyKey, To: request.Account.Wallet.Currency, }) if err != nil { return errors.HTTP(ctx, err) } accountx, err := api.account.ChangeWallet(ctx.Request().Context(), request.Account.UserID, &models.Wallet{ Cash: request.Account.Wallet.Cash - cash, Money: request.Account.Wallet.Money - request.Money, Spent: request.Account.Wallet.Spent + request.Money, PurchasesAmount: request.Account.Wallet.PurchasesAmount, Currency: request.Account.Wallet.Currency, }) if err != nil { return errors.HTTP(ctx, err) } updatedAccount = accountx } if _, err := api.history.Insert(ctx.Request().Context(), &models.History{ Key: models.CustomerHistoryKeyPayCart, UserID: account.UserID, Comment: "Успешная оплата корзины", RawDetails: tariffs, }); err != nil { return errors.HTTP(ctx, err) } // TODO: обработать ошибки при отправке сообщений sendErrors := make([]errors.Error, 0) waitGroup := sync.WaitGroup{} mutex := sync.Mutex{} for _, tariff := range tariffs { waitGroup.Add(1) go func(currentTariff models.Tariff) { defer waitGroup.Done() if err := api.producer.Send(ctx.Request().Context(), userID, ¤tTariff); err != nil { api.logger.Error("failed to send tariff on of ", zap.Error(err)) mutex.Lock() defer mutex.Unlock() sendErrors = append(sendErrors, err) } }(tariff) } waitGroup.Wait() if len(sendErrors) > 0 { for _, err := range sendErrors { api.logger.Error("failed to send tariffs to broker on of ", zap.Error(err)) } return errors.HTTP(ctx, errors.NewWithMessage("failed to send tariffs to broker", errors.ErrInternalError)) } if _, err := api.account.ClearCart(ctx.Request().Context(), account.UserID); err != nil { api.logger.Error("failed to clear cart on of ", zap.Error(err)) return errors.HTTP(ctx, err) } updatedAccount.Cart = []string{} return ctx.JSON(http.StatusOK, updatedAccount) } // Currency func (api *API2) GetCurrencies(ctx echo.Context) error { currencyList, err := api.currency.FindCurrenciesList(ctx.Request().Context(), models.DefaultCurrencyListName) if err != nil && err.Type() != errors.ErrNotFound { api.logger.Error( "failed to get currencies on of ", zap.Error(err), ) return errors.HTTP(ctx, err) } if err != nil && err.Type() == errors.ErrNotFound { return ctx.JSON(http.StatusOK, []string{}) } return ctx.JSON(http.StatusOK, currencyList) } func (api *API2) UpdateCurrencies(ctx echo.Context) error { currenciesPtr, bindErr := echotools.Bind[[]string](ctx) if bindErr != nil { api.logger.Error( "failed to parse body on of ", zap.Error(bindErr), ) return errors.HTTP(ctx, errors.New( fmt.Errorf("failed to parse body: %w", bindErr), errors.ErrInvalidArgs, )) } currencies := *currenciesPtr if len(currencies) < 1 { currencies = make([]string, 0) // TODO WHY? } currencyList, err := api.currency.ReplaceCurrencies(ctx.Request().Context(), &models.CurrencyList{ Name: models.DefaultCurrencyListName, Currencies: currencies, }) if err != nil && err.Type() != errors.ErrNotFound { api.logger.Error( "failed to put currencies on of ", zap.Error(err), ) return errors.HTTP(ctx, err) } if err != nil && err.Type() == errors.ErrNotFound { newCurrencyList, err := api.currency.Insert(ctx.Request().Context(), &models.CurrencyList{ Name: models.DefaultCurrencyListName, Currencies: currencies, }) if err != nil && err.Type() != errors.ErrNotFound { api.logger.Error( "failed to insert new currency list on of ", zap.Error(err), ) return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, newCurrencyList.Currencies) } return ctx.JSON(http.StatusOK, currencyList.Currencies) } // History func (api *API2) GetHistory(ctx echo.Context, params GetHistoryParams) error { dto := &history.GetHistories{ Type: params.Type, Pagination: &models.Pagination{ Page: int64(*params.Page), Limit: int64(*params.Limit), }, } count, err := api.history.CountAll(ctx.Request().Context(), dto) if err != nil { api.logger.Error("failed to count histories on of ", zap.Error(err), ) return errors.HTTP(ctx, err) } var returnHistories models.PaginationResponse[models.History] if count == 0 { returnHistories = models.PaginationResponse[models.History]{TotalPages: 0, Records: []models.History{}} } else { totalPages := int64(math.Ceil(float64(count) / float64(dto.Pagination.Limit))) histories, err := api.history.FindMany(ctx.Request().Context(), dto) if err != nil { api.logger.Error("failed to get historiy list on of ", zap.Error(err), ) return errors.HTTP(ctx, err) } returnHistories = models.PaginationResponse[models.History]{ TotalPages: totalPages, Records: histories, } } return ctx.JSON(http.StatusOK, returnHistories) } // Wallet func (api *API2) RequestMoney(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { api.logger.Error("failed to convert jwt payload to string on of ") return errors.HTTP(ctx, errors.New( fmt.Errorf("failed to convert jwt payload to string: %s", userID), errors.ErrInvalidArgs, )) } request, bindErr := echotools.Bind[models.GetPaymentLinkBody](ctx) if bindErr != nil { api.logger.Error("failed to bind body on of ", zap.Error(bindErr)) return errors.HTTP(ctx, errors.New( fmt.Errorf("failed to parse body on of : %w", bindErr), errors.ErrInvalidArgs, )) } if validateErr := utils.ValidateGetPaymentLinkBody(request); validateErr != nil { api.logger.Error("failed to validate body on of ", zap.Error(validateErr)) return errors.HTTP(ctx, validateErr) } link, err := api.GetPaymentLink(ctx.Request().Context(), &models.GetPaymentLinkRequest{ Body: request, UserID: userID, ClientIP: ctx.RealIP(), }) if err != nil { api.logger.Error("failed to get payment link on of ", zap.Error(err)) return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, &models.GetPaymentLinkResponse{Link: link}) } func (api *API2) ChangeCurrency(ctx echo.Context) error { userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string) if !ok { api.logger.Error("failed to convert jwt payload to string on of ") return errors.HTTP(ctx, errors.New( fmt.Errorf("failed to convert jwt payload to string: %s", userID), errors.ErrInvalidArgs, )) } request, bindErr := echotools.Bind[models.ChangeCurrency](ctx) if bindErr != nil { api.logger.Error("failed to bind body on of ", zap.Error(bindErr)) return errors.HTTP(ctx, errors.New( fmt.Errorf("failed to parse body on of : %w", bindErr), errors.ErrInvalidArgs, )) } if validate.IsStringEmpty(request.Currency) { return errors.HTTP(ctx, errors.New( fmt.Errorf("empty currency key on of : %w", errors.ErrInvalidArgs), errors.ErrInvalidArgs, )) } currency := request.Currency account, err := api.account.FindByUserID(ctx.Request().Context(), userID) if err != nil { api.logger.Error("failed to find account on of ", zap.Error(err), zap.String("userID", userID), zap.Any("currency", currency), ) return errors.HTTP(ctx, err) } cash, err := api.clients.currency.Translate(ctx.Request().Context(), &models.TranslateCurrency{ Money: account.Wallet.Cash, From: account.Wallet.Currency, To: currency, }) if err != nil { api.logger.Error("failed to translate currency on of ", zap.Error(err)) return errors.HTTP(ctx, err) } updatedAccount, err := api.account.ChangeWallet(ctx.Request().Context(), account.UserID, &models.Wallet{ Cash: cash, Currency: currency, Money: account.Wallet.Money, }) if err != nil { api.logger.Error("failed to update wallet on of ", zap.Error(err)) return errors.HTTP(ctx, err) } return ctx.JSON(http.StatusOK, updatedAccount) }