mirror of
https://github.com/3ybactuk/marketplace-go-service-project.git
synced 2025-10-30 14:03:45 +03:00
[hw-3] loms service
This commit is contained in:
119
loms/internal/app/app.go
Normal file
119
loms/internal/app/app.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
"route256/loms/internal/app/server"
|
||||
ordersRepository "route256/loms/internal/domain/repository/orders"
|
||||
stocksRepository "route256/loms/internal/domain/repository/stocks"
|
||||
"route256/loms/internal/domain/service"
|
||||
"route256/loms/internal/infra/config"
|
||||
mw "route256/loms/internal/infra/grpc/middleware"
|
||||
|
||||
pb "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
config *config.Config
|
||||
controller *server.Server
|
||||
}
|
||||
|
||||
func NewApp(configPath string) (*App, error) {
|
||||
c, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load config: %w", err)
|
||||
}
|
||||
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
if c.Service.LogLevel != "" {
|
||||
level, logErr := zerolog.ParseLevel(c.Service.LogLevel)
|
||||
if logErr != nil {
|
||||
return nil, fmt.Errorf("unknown log level `%s` provided: %w", c.Service.LogLevel, logErr)
|
||||
}
|
||||
|
||||
zerolog.SetGlobalLevel(level)
|
||||
}
|
||||
|
||||
log.WithLevel(zerolog.GlobalLevel()).Msgf("using logging level=`%s`", zerolog.GlobalLevel().String())
|
||||
|
||||
stockRepo, err := stocksRepository.NewInMemoryRepository(100)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stocksRepository.NewInMemoryRepository: %w", err)
|
||||
}
|
||||
|
||||
orderRepo := ordersRepository.NewInMemoryRepository(100)
|
||||
service := service.NewLomsService(orderRepo, stockRepo)
|
||||
controller := server.NewServer(service)
|
||||
|
||||
app := &App{
|
||||
config: c,
|
||||
controller: controller,
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *App) ListenAndServe() error {
|
||||
grpcAddr := fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.GRPCPort)
|
||||
l, err := net.Listen("tcp", grpcAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(
|
||||
mw.Logging,
|
||||
mw.Validate,
|
||||
),
|
||||
)
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
pb.RegisterLOMSServer(grpcServer, app.controller)
|
||||
|
||||
go func() {
|
||||
if err = grpcServer.Serve(l); err != nil {
|
||||
log.Fatal().Msgf("failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info().Msgf("Serving grpc loms at grpc://%s", l.Addr())
|
||||
|
||||
// Setup HTTP gateway
|
||||
|
||||
conn, err := grpc.NewClient(
|
||||
grpcAddr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("grpc.NewClient: %w", err)
|
||||
}
|
||||
|
||||
gwmux := runtime.NewServeMux()
|
||||
if err = pb.RegisterLOMSHandler(context.Background(), gwmux, conn); err != nil {
|
||||
return fmt.Errorf("pb.RegisterLOMSHandler: %w", err)
|
||||
}
|
||||
|
||||
gwServer := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.HTTPPort),
|
||||
Handler: gwmux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
log.Info().Msgf("Serving http loms at http://%s", gwServer.Addr)
|
||||
|
||||
return gwServer.ListenAndServe()
|
||||
}
|
||||
148
loms/internal/app/server/server.go
Normal file
148
loms/internal/app/server/server.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"route256/loms/internal/domain/entity"
|
||||
"route256/loms/internal/domain/model"
|
||||
|
||||
pb "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
var _ pb.LOMSServer = (*Server)(nil)
|
||||
|
||||
type LomsService interface {
|
||||
OrderCreate(ctx context.Context, orderReq *pb.OrderCreateRequest) (entity.ID, error)
|
||||
OrderInfo(ctx context.Context, orderID entity.ID) (*entity.Order, error)
|
||||
OrderPay(ctx context.Context, orderID entity.ID) error
|
||||
OrderCancel(ctx context.Context, orderID entity.ID) error
|
||||
StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
pb.UnimplementedLOMSServer
|
||||
|
||||
service LomsService
|
||||
}
|
||||
|
||||
func NewServer(lomsService LomsService) *Server {
|
||||
return &Server{
|
||||
service: lomsService,
|
||||
}
|
||||
}
|
||||
|
||||
func mapOrderStatus(pbStatus pb.OrderStatus) (string, error) {
|
||||
switch pbStatus {
|
||||
case pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT:
|
||||
return "awaiting payment", nil
|
||||
case pb.OrderStatus_ORDER_STATUS_CANCELLED:
|
||||
return "cancelled", nil
|
||||
case pb.OrderStatus_ORDER_STATUS_FAILED:
|
||||
return "failed", nil
|
||||
case pb.OrderStatus_ORDER_STATUS_NEW:
|
||||
return "new", nil
|
||||
case pb.OrderStatus_ORDER_STATUS_PAYED:
|
||||
return "payed", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unexpected OrderStatus: %v", pbStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) OrderCreate(ctx context.Context, req *pb.OrderCreateRequest) (*pb.OrderCreateResponse, error) {
|
||||
id, err := s.service.OrderCreate(ctx, req)
|
||||
switch {
|
||||
case errors.Is(err, model.ErrInvalidInput):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, model.ErrNotEnoughStocks):
|
||||
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
||||
case err != nil:
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &pb.OrderCreateResponse{OrderId: int64(id)}, nil
|
||||
}
|
||||
|
||||
func (s *Server) OrderInfo(ctx context.Context, req *pb.OrderInfoRequest) (*pb.OrderInfoResponse, error) {
|
||||
ord, err := s.service.OrderInfo(ctx, entity.ID(req.OrderId))
|
||||
switch {
|
||||
case errors.Is(err, model.ErrInvalidInput):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, model.ErrOrderNotFound):
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
case err != nil:
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
items := make([]*pb.OrderItem, len(ord.Items))
|
||||
for i, item := range ord.Items {
|
||||
items[i] = &pb.OrderItem{
|
||||
Sku: int64(item.ID),
|
||||
Count: item.Count,
|
||||
}
|
||||
}
|
||||
|
||||
orderStatus, err := mapOrderStatus(pb.OrderStatus(pb.OrderStatus_value[ord.Status]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &pb.OrderInfoResponse{
|
||||
Status: orderStatus,
|
||||
UserId: int64(ord.UserID),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) OrderPay(ctx context.Context, req *pb.OrderPayRequest) (*emptypb.Empty, error) {
|
||||
err := s.service.OrderPay(ctx, entity.ID(req.OrderId))
|
||||
switch {
|
||||
case errors.Is(err, model.ErrInvalidInput):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, model.ErrOrderNotFound):
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, model.ErrOrderInvalidStatus):
|
||||
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
||||
case err != nil:
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) OrderCancel(ctx context.Context, req *pb.OrderCancelRequest) (*emptypb.Empty, error) {
|
||||
err := s.service.OrderCancel(ctx, entity.ID(req.OrderId))
|
||||
switch {
|
||||
case errors.Is(err, model.ErrInvalidInput):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, model.ErrOrderNotFound):
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, model.ErrOrderInvalidStatus):
|
||||
return nil, status.Error(codes.FailedPrecondition, err.Error())
|
||||
case err != nil:
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) StocksInfo(ctx context.Context, req *pb.StocksInfoRequest) (*pb.StocksInfoResponse, error) {
|
||||
count, err := s.service.StocksInfo(ctx, entity.Sku(req.Sku))
|
||||
switch {
|
||||
case errors.Is(err, model.ErrInvalidInput):
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, model.ErrOrderNotFound), errors.Is(err, model.ErrUnknownStock):
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
case err != nil:
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &pb.StocksInfoResponse{Count: count}, nil
|
||||
}
|
||||
18
loms/internal/domain/entity/order.go
Normal file
18
loms/internal/domain/entity/order.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package entity
|
||||
|
||||
type (
|
||||
ID int64
|
||||
Sku int64
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
OrderID ID
|
||||
Status string
|
||||
UserID ID
|
||||
Items []OrderItem
|
||||
}
|
||||
|
||||
type OrderItem struct {
|
||||
ID Sku
|
||||
Count uint32
|
||||
}
|
||||
6
loms/internal/domain/entity/stock.go
Normal file
6
loms/internal/domain/entity/stock.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package entity
|
||||
|
||||
type Stock struct {
|
||||
Item OrderItem
|
||||
Reserved uint32
|
||||
}
|
||||
13
loms/internal/domain/model/errors.go
Normal file
13
loms/internal/domain/model/errors.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
|
||||
ErrNotEnoughStocks = errors.New("not enough stocks")
|
||||
ErrUnknownStock = errors.New("unknown stock provided")
|
||||
|
||||
ErrOrderNotFound = errors.New("order not found")
|
||||
ErrOrderInvalidStatus = errors.New("invalid order status")
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"route256/loms/internal/domain/entity"
|
||||
"route256/loms/internal/domain/model"
|
||||
)
|
||||
|
||||
type storage = map[entity.ID]*entity.Order
|
||||
|
||||
type InMemoryRepository struct {
|
||||
storage storage
|
||||
mx sync.RWMutex
|
||||
idCounter entity.ID
|
||||
}
|
||||
|
||||
func NewInMemoryRepository(cap int) *InMemoryRepository {
|
||||
return &InMemoryRepository{
|
||||
storage: make(storage, cap),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) OrderCreate(_ context.Context, order *entity.Order) (entity.ID, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
r.idCounter++
|
||||
|
||||
orderCopy := &entity.Order{
|
||||
OrderID: r.idCounter,
|
||||
Status: order.Status,
|
||||
UserID: order.UserID,
|
||||
Items: make([]entity.OrderItem, len(order.Items)),
|
||||
}
|
||||
|
||||
copy(orderCopy.Items, order.Items)
|
||||
|
||||
r.storage[orderCopy.OrderID] = orderCopy
|
||||
|
||||
return r.idCounter, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) OrderSetStatus(_ context.Context, orderID entity.ID, newStatus string) error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
if _, ok := r.storage[orderID]; !ok {
|
||||
return model.ErrOrderNotFound
|
||||
}
|
||||
|
||||
r.storage[orderID].Status = newStatus
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) OrderGetByID(_ context.Context, orderID entity.ID) (*entity.Order, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
order, ok := r.storage[orderID]
|
||||
if !ok {
|
||||
return nil, model.ErrOrderNotFound
|
||||
}
|
||||
|
||||
orderCopy := &entity.Order{
|
||||
OrderID: order.OrderID,
|
||||
Status: order.Status,
|
||||
UserID: order.UserID,
|
||||
Items: make([]entity.OrderItem, len(order.Items)),
|
||||
}
|
||||
|
||||
copy(orderCopy.Items, order.Items)
|
||||
|
||||
return orderCopy, nil
|
||||
}
|
||||
114
loms/internal/domain/repository/stocks/in_memory_repository.go
Normal file
114
loms/internal/domain/repository/stocks/in_memory_repository.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"route256/loms/internal/domain/entity"
|
||||
"route256/loms/internal/domain/model"
|
||||
)
|
||||
|
||||
//go:embed stock-data.json
|
||||
var stockData []byte
|
||||
|
||||
type storage = map[entity.Sku]*entity.Stock
|
||||
|
||||
type InMemoryRepository struct {
|
||||
storage storage
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
func NewInMemoryRepository(cap int) (*InMemoryRepository, error) {
|
||||
var rows []struct {
|
||||
Sku entity.Sku `json:"sku"`
|
||||
TotalCount uint32 `json:"total_count"`
|
||||
Reserved uint32 `json:"reserved"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(stockData, &rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repo := &InMemoryRepository{
|
||||
storage: make(storage, cap),
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
repo.storage[r.Sku] = &entity.Stock{
|
||||
Item: entity.OrderItem{
|
||||
ID: r.Sku,
|
||||
Count: r.TotalCount,
|
||||
},
|
||||
Reserved: r.Reserved,
|
||||
}
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) StockReserve(_ context.Context, stock *entity.Stock) error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
if _, ok := r.storage[stock.Item.ID]; !ok {
|
||||
return model.ErrNotEnoughStocks
|
||||
}
|
||||
|
||||
if r.storage[stock.Item.ID].Item.Count < stock.Reserved {
|
||||
return model.ErrNotEnoughStocks
|
||||
}
|
||||
|
||||
r.storage[stock.Item.ID].Item.Count -= stock.Reserved
|
||||
r.storage[stock.Item.ID].Reserved += stock.Reserved
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) StockReserveRemove(_ context.Context, stock *entity.Stock) error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
if _, ok := r.storage[stock.Item.ID]; !ok {
|
||||
return model.ErrUnknownStock
|
||||
}
|
||||
|
||||
if r.storage[stock.Item.ID].Reserved < stock.Reserved {
|
||||
return model.ErrNotEnoughStocks
|
||||
}
|
||||
|
||||
r.storage[stock.Item.ID].Reserved -= stock.Reserved
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) StockCancel(_ context.Context, stock *entity.Stock) error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
if _, ok := r.storage[stock.Item.ID]; !ok {
|
||||
return model.ErrUnknownStock
|
||||
}
|
||||
|
||||
if r.storage[stock.Item.ID].Reserved < stock.Reserved {
|
||||
return model.ErrNotEnoughStocks
|
||||
}
|
||||
|
||||
r.storage[stock.Item.ID].Reserved -= stock.Reserved
|
||||
r.storage[stock.Item.ID].Item.Count += stock.Reserved
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InMemoryRepository) StockGetByID(_ context.Context, sku entity.Sku) (*entity.Stock, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
stock, ok := r.storage[sku]
|
||||
if !ok {
|
||||
return nil, model.ErrUnknownStock
|
||||
}
|
||||
|
||||
return stock, nil
|
||||
}
|
||||
37
loms/internal/domain/repository/stocks/stock-data.json
Normal file
37
loms/internal/domain/repository/stocks/stock-data.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"sku": 139275865,
|
||||
"total_count": 65534,
|
||||
"reserved": 0
|
||||
},
|
||||
{
|
||||
"sku": 2956315,
|
||||
"total_count": 100,
|
||||
"reserved": 30
|
||||
},
|
||||
{
|
||||
"sku": 1076963,
|
||||
"total_count": 100,
|
||||
"reserved": 35
|
||||
},
|
||||
{
|
||||
"sku": 135717466,
|
||||
"total_count": 100,
|
||||
"reserved": 20
|
||||
},
|
||||
{
|
||||
"sku": 135937324,
|
||||
"total_count": 100,
|
||||
"reserved": 30
|
||||
},
|
||||
{
|
||||
"sku": 1625903,
|
||||
"total_count": 10000,
|
||||
"reserved": 0
|
||||
},
|
||||
{
|
||||
"sku": 1148162,
|
||||
"total_count": 100,
|
||||
"reserved": 0
|
||||
}
|
||||
]
|
||||
1160
loms/internal/domain/service/mock/order_repository_mock.go
Normal file
1160
loms/internal/domain/service/mock/order_repository_mock.go
Normal file
File diff suppressed because it is too large
Load Diff
1483
loms/internal/domain/service/mock/stock_repository_mock.go
Normal file
1483
loms/internal/domain/service/mock/stock_repository_mock.go
Normal file
File diff suppressed because it is too large
Load Diff
186
loms/internal/domain/service/service.go
Normal file
186
loms/internal/domain/service/service.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"route256/loms/internal/domain/entity"
|
||||
"route256/loms/internal/domain/model"
|
||||
|
||||
pb "route256/pkg/api/loms/v1"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:generate minimock -i OrderRepository -o ./mock -s _mock.go
|
||||
type OrderRepository interface {
|
||||
OrderCreate(ctx context.Context, order *entity.Order) (entity.ID, error)
|
||||
OrderSetStatus(ctx context.Context, orderID entity.ID, newStatus string) error
|
||||
OrderGetByID(ctx context.Context, orderID entity.ID) (*entity.Order, error)
|
||||
}
|
||||
|
||||
//go:generate minimock -i StockRepository -o ./mock -s _mock.go
|
||||
type StockRepository interface {
|
||||
StockReserve(ctx context.Context, stock *entity.Stock) error
|
||||
StockReserveRemove(ctx context.Context, stock *entity.Stock) error
|
||||
StockCancel(ctx context.Context, stock *entity.Stock) error
|
||||
StockGetByID(ctx context.Context, sku entity.Sku) (*entity.Stock, error)
|
||||
}
|
||||
|
||||
type LomsService struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
|
||||
func NewLomsService(orderRepo OrderRepository, stockRepo StockRepository) *LomsService {
|
||||
return &LomsService{
|
||||
orders: orderRepo,
|
||||
stocks: stockRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LomsService) rollbackStocks(ctx context.Context, stocks []*entity.Stock) {
|
||||
for _, stock := range stocks {
|
||||
if err := s.stocks.StockCancel(ctx, stock); err != nil {
|
||||
log.Error().Err(err).Msg("failed to rollback stock")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LomsService) OrderCreate(ctx context.Context, orderReq *pb.OrderCreateRequest) (entity.ID, error) {
|
||||
if orderReq == nil || orderReq.UserId <= 0 || len(orderReq.Items) == 0 {
|
||||
return 0, model.ErrInvalidInput
|
||||
}
|
||||
|
||||
for _, item := range orderReq.Items {
|
||||
if item.Sku <= 0 || item.Count == 0 {
|
||||
return 0, model.ErrInvalidInput
|
||||
}
|
||||
}
|
||||
|
||||
order := &entity.Order{
|
||||
OrderID: 0,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_NEW.String(),
|
||||
UserID: entity.ID(orderReq.UserId),
|
||||
Items: make([]entity.OrderItem, len(orderReq.Items)),
|
||||
}
|
||||
|
||||
for i, item := range orderReq.Items {
|
||||
order.Items[i] = entity.OrderItem{
|
||||
ID: entity.Sku(item.Sku),
|
||||
Count: item.Count,
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortStableFunc(order.Items, func(a, b entity.OrderItem) int {
|
||||
return int(a.ID - b.ID)
|
||||
})
|
||||
|
||||
id, err := s.orders.OrderCreate(ctx, order)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("orders.OrderCreate: %w", err)
|
||||
}
|
||||
|
||||
order.OrderID = id
|
||||
|
||||
commitedStocks := make([]*entity.Stock, 0, len(order.Items))
|
||||
for _, item := range order.Items {
|
||||
stock := &entity.Stock{
|
||||
Item: item,
|
||||
Reserved: item.Count,
|
||||
}
|
||||
|
||||
if err := s.stocks.StockReserve(ctx, stock); err != nil {
|
||||
s.rollbackStocks(ctx, commitedStocks)
|
||||
|
||||
if statusErr := s.orders.OrderSetStatus(ctx, order.OrderID, pb.OrderStatus_ORDER_STATUS_FAILED.String()); statusErr != nil {
|
||||
log.Error().Err(statusErr).Msg("failed to update status on stock reserve fail")
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("stocks.StockReserve: %w", err)
|
||||
}
|
||||
|
||||
commitedStocks = append(commitedStocks, stock)
|
||||
}
|
||||
|
||||
if err := s.orders.OrderSetStatus(ctx, order.OrderID, pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String()); err != nil {
|
||||
s.rollbackStocks(ctx, commitedStocks)
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return order.OrderID, nil
|
||||
}
|
||||
|
||||
func (s *LomsService) OrderInfo(ctx context.Context, orderID entity.ID) (*entity.Order, error) {
|
||||
if orderID <= 0 {
|
||||
return nil, model.ErrInvalidInput
|
||||
}
|
||||
|
||||
return s.orders.OrderGetByID(ctx, orderID)
|
||||
}
|
||||
|
||||
func (s *LomsService) OrderPay(ctx context.Context, orderID entity.ID) error {
|
||||
order, err := s.OrderInfo(ctx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch order.Status {
|
||||
case pb.OrderStatus_ORDER_STATUS_PAYED.String():
|
||||
return nil
|
||||
case pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String():
|
||||
for _, item := range order.Items {
|
||||
if err := s.stocks.StockReserveRemove(ctx, &entity.Stock{
|
||||
Item: item,
|
||||
Reserved: item.Count,
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("failed to free stock reservation")
|
||||
}
|
||||
}
|
||||
|
||||
return s.orders.OrderSetStatus(ctx, orderID, pb.OrderStatus_ORDER_STATUS_PAYED.String())
|
||||
default:
|
||||
return model.ErrOrderInvalidStatus
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LomsService) OrderCancel(ctx context.Context, orderID entity.ID) error {
|
||||
order, err := s.OrderInfo(ctx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch order.Status {
|
||||
case pb.OrderStatus_ORDER_STATUS_CANCELLED.String():
|
||||
return nil
|
||||
case pb.OrderStatus_ORDER_STATUS_FAILED.String(), pb.OrderStatus_ORDER_STATUS_PAYED.String():
|
||||
return model.ErrOrderInvalidStatus
|
||||
}
|
||||
|
||||
stocks := make([]*entity.Stock, len(order.Items))
|
||||
for i, item := range order.Items {
|
||||
stocks[i] = &entity.Stock{
|
||||
Item: item,
|
||||
Reserved: item.Count,
|
||||
}
|
||||
}
|
||||
|
||||
s.rollbackStocks(ctx, stocks)
|
||||
|
||||
return s.orders.OrderSetStatus(ctx, orderID, pb.OrderStatus_ORDER_STATUS_CANCELLED.String())
|
||||
}
|
||||
|
||||
func (s *LomsService) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) {
|
||||
if sku <= 0 {
|
||||
return 0, model.ErrInvalidInput
|
||||
}
|
||||
|
||||
stock, err := s.stocks.StockGetByID(ctx, sku)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return stock.Item.Count, nil
|
||||
}
|
||||
448
loms/internal/domain/service/service_test.go
Normal file
448
loms/internal/domain/service/service_test.go
Normal file
@@ -0,0 +1,448 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/gojuno/minimock/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"route256/loms/internal/domain/entity"
|
||||
"route256/loms/internal/domain/model"
|
||||
mock "route256/loms/internal/domain/service/mock"
|
||||
|
||||
pb "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
testUser = entity.ID(1337)
|
||||
testSku = entity.Sku(199)
|
||||
)
|
||||
|
||||
func TestLomsService_OrderCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
goodReq := &pb.OrderCreateRequest{
|
||||
UserId: int64(testUser),
|
||||
Items: []*pb.OrderItem{
|
||||
{
|
||||
Sku: int64(testSku),
|
||||
Count: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
badItemReq := &pb.OrderCreateRequest{
|
||||
UserId: int64(testUser),
|
||||
Items: []*pb.OrderItem{
|
||||
{
|
||||
Sku: 0,
|
||||
Count: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
type args struct {
|
||||
req *pb.OrderCreateRequest
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(1, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
req: &pb.OrderCreateRequest{
|
||||
UserId: 0,
|
||||
},
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid input with bad items",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
req: badItemReq,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "order create error",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(0, errors.New("order create error")),
|
||||
stocks: nil,
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "stock reserve error",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(1, nil).
|
||||
OrderSetStatusMock.Return(errors.New("status update error")),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveMock.Return(errors.New("reservation error")),
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "final status update error",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(1, nil).
|
||||
OrderSetStatusMock.Return(errors.New("unexpected error")),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveMock.Return(nil).
|
||||
StockCancelMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(tt.fields.orders, tt.fields.stocks)
|
||||
_, err := svc.OrderCreate(ctx, tt.args.req)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLomsService_OrderPay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
awaitingOrder := &entity.Order{
|
||||
OrderID: 1,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(),
|
||||
Items: []entity.OrderItem{{
|
||||
ID: testSku,
|
||||
Count: 2,
|
||||
}},
|
||||
}
|
||||
|
||||
payedOrder := &entity.Order{OrderID: 2, Status: pb.OrderStatus_ORDER_STATUS_PAYED.String()}
|
||||
badStatusOrder := &entity.Order{OrderID: 3, Status: pb.OrderStatus_ORDER_STATUS_FAILED.String()}
|
||||
|
||||
type fields struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
type args struct {
|
||||
id entity.ID
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(awaitingOrder, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveRemoveMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
id: awaitingOrder.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc),
|
||||
stocks: mock.NewStockRepositoryMock(mc),
|
||||
},
|
||||
args: args{
|
||||
id: 0,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already payed",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(payedOrder, nil),
|
||||
},
|
||||
args: args{
|
||||
id: payedOrder.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid status",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(badStatusOrder, nil),
|
||||
},
|
||||
args: args{
|
||||
id: badStatusOrder.OrderID,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrOrderInvalidStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unexpected logged error on updating stocks reserves",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(awaitingOrder, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveRemoveMock.Return(errors.New("unexpected error")),
|
||||
},
|
||||
args: args{
|
||||
id: badStatusOrder.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(tt.fields.orders, tt.fields.stocks)
|
||||
err := svc.OrderPay(ctx, tt.args.id)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLomsService_OrderInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mc := minimock.NewController(t)
|
||||
svc := NewLomsService(
|
||||
mock.NewOrderRepositoryMock(mc),
|
||||
mock.NewStockRepositoryMock(mc),
|
||||
)
|
||||
|
||||
err := svc.OrderPay(context.Background(), 0)
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
}
|
||||
|
||||
func TestLomsService_OrderCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
awaiting := &entity.Order{
|
||||
OrderID: 1,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(),
|
||||
Items: []entity.OrderItem{{
|
||||
ID: testSku, Count: 1,
|
||||
}},
|
||||
}
|
||||
cancelled := &entity.Order{
|
||||
OrderID: 2,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_CANCELLED.String(),
|
||||
}
|
||||
payed := &entity.Order{
|
||||
OrderID: 3,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_PAYED.String(),
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
type args struct {
|
||||
id entity.ID
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(awaiting, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockCancelMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
id: awaiting.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc),
|
||||
stocks: mock.NewStockRepositoryMock(mc),
|
||||
},
|
||||
args: args{
|
||||
id: 0,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already cancelled",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(cancelled, nil),
|
||||
},
|
||||
args: args{
|
||||
id: cancelled.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid status",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(payed, nil),
|
||||
},
|
||||
args: args{
|
||||
id: payed.OrderID,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrOrderInvalidStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(tt.fields.orders, tt.fields.stocks)
|
||||
err := svc.OrderCancel(ctx, tt.args.id)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLomsService_StocksInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
type fields struct{ stocks StockRepository }
|
||||
type args struct{ sku entity.Sku }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want uint32
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockGetByIDMock.Return(&entity.Stock{
|
||||
Item: entity.OrderItem{
|
||||
Count: 7,
|
||||
},
|
||||
}, nil),
|
||||
},
|
||||
args: args{
|
||||
sku: testSku,
|
||||
},
|
||||
want: 7,
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid sku",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
sku: 0,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get by id error",
|
||||
fields: fields{
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockGetByIDMock.Return(nil, errors.New("unexpected error")),
|
||||
},
|
||||
args: args{
|
||||
sku: testSku,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(nil, tt.fields.stocks)
|
||||
got, err := svc.StocksInfo(ctx, tt.args.sku)
|
||||
tt.wantErr(t, err)
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
74
loms/internal/infra/config/config.go
Normal file
74
loms/internal/infra/config/config.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Service struct {
|
||||
Host string `yaml:"host"`
|
||||
GRPCPort string `yaml:"grpc_port"`
|
||||
HTTPPort string `yaml:"http_port"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
} `yaml:"service"`
|
||||
|
||||
Jaeger struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
} `yaml:"jaeger"`
|
||||
|
||||
DatabaseMaster struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
DBName string `yaml:"db_name"`
|
||||
} `yaml:"db_master"`
|
||||
|
||||
DatabaseReplica struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
DBName string `yaml:"db_name"`
|
||||
} `yaml:"db_replica"`
|
||||
|
||||
Kafka struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
OrderTopic string `yaml:"order_topic"`
|
||||
Brokers string `yaml:"brokers"`
|
||||
} `yaml:"kafka"`
|
||||
}
|
||||
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfgRoot := filepath.Join(workDir, "configs")
|
||||
absCfgRoot, _ := filepath.Abs(cfgRoot)
|
||||
|
||||
filePath := filepath.Join(absCfgRoot, filepath.Clean(filename))
|
||||
if !strings.HasPrefix(filePath, absCfgRoot) {
|
||||
return nil, fmt.Errorf("invalid path")
|
||||
}
|
||||
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
config := &Config{}
|
||||
if err := yaml.NewDecoder(f).Decode(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
24
loms/internal/infra/grpc/middleware/logging.go
Normal file
24
loms/internal/infra/grpc/middleware/logging.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package mw
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func Logging(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
||||
raw, _ := protojson.Marshal((req).(proto.Message))
|
||||
log.Debug().Msgf("request: method: %v, req: %s", info.FullMethod, string(raw))
|
||||
|
||||
if resp, err = handler(ctx, req); err != nil {
|
||||
log.Debug().Msgf("response: method: %v, err: %s", info.FullMethod, err.Error())
|
||||
return
|
||||
}
|
||||
rawResp, _ := protojson.Marshal((resp).(proto.Message))
|
||||
log.Debug().Msgf("response: method: %v, resp: %s", info.FullMethod, string(rawResp))
|
||||
|
||||
return
|
||||
}
|
||||
18
loms/internal/infra/grpc/middleware/validate.go
Normal file
18
loms/internal/infra/grpc/middleware/validate.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package mw
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func Validate(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
if v, ok := req.(interface{ Validate() error }); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
Reference in New Issue
Block a user