customer/internal/interface/swagger/api.2.go
Maxim Dolgushin 13c955d65f safe append
2023-11-08 13:08:27 +07:00

653 lines
20 KiB
Go

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 <SetVerificationStatus> of <AccountService>", 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, &currentTariff); err != nil {
api.logger.Error("failed to send tariff on <Send> of <TariffBrokerService>", 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 <Pay> of <CartService>", 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 <Pay> of <CartService>", 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 <GetCurrencies> of <CurrencyService>",
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 <PutCurrencies> of <CurrencyController>",
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 <PutCurrencies> of <CurrencyService>",
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 <PutCurrencies> of <CurrencyService>",
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 <GetHistoryList> of <HistoryService>",
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 <GetHistoryList> of <HistoryService>",
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 <GetPaymentLink> of <WallerController>")
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 <GetPaymentLink> of <WalletController>", zap.Error(bindErr))
return errors.HTTP(ctx, errors.New(
fmt.Errorf("failed to parse body on <GetPaymentLink> of <WalletController>: %w", bindErr),
errors.ErrInvalidArgs,
))
}
if validateErr := utils.ValidateGetPaymentLinkBody(request); validateErr != nil {
api.logger.Error("failed to validate body on <GetPaymentLink> of <WalletController>", 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 <GetPaymentLink> of <WalletController>", 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 <ChangeCurrency> of <WallerController>")
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 <ChangeCurrency> of <WalletController>", zap.Error(bindErr))
return errors.HTTP(ctx, errors.New(
fmt.Errorf("failed to parse body on <ChangeCurrency> of <WalletController>: %w", bindErr),
errors.ErrInvalidArgs,
))
}
if validate.IsStringEmpty(request.Currency) {
return errors.HTTP(ctx, errors.New(
fmt.Errorf("empty currency key on <ChangeCurrency> of <WalletController>: %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 <ChangeCurrency> of <WalletService>",
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 <ChangeCurrency> of <WalletService>", 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 <ChangeCurrency> of <WalletService>", zap.Error(err))
return errors.HTTP(ctx, err)
}
return ctx.JSON(http.StatusOK, updatedAccount)
}