mirror of
				https://github.com/3ybactuk/marketplace-go-service-project.git
				synced 2025-10-31 06:23:44 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			282 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			282 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package service
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"slices"
 | |
| 
 | |
| 	"route256/loms/internal/domain/entity"
 | |
| 	"route256/loms/internal/domain/model"
 | |
| 	"route256/loms/internal/infra/tracing"
 | |
| 
 | |
| 	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 txManager interface {
 | |
| 	WriteWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error)
 | |
| 	ReadWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error)
 | |
| 	WriteWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error)
 | |
| 	ReadWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error)
 | |
| }
 | |
| 
 | |
| type StatusProducer interface {
 | |
| 	Send(ctx context.Context, id entity.ID, status string) error
 | |
| }
 | |
| 
 | |
| type LomsService struct {
 | |
| 	orders         OrderRepository
 | |
| 	stocks         StockRepository
 | |
| 	txManager      txManager
 | |
| 	statusProducer StatusProducer
 | |
| }
 | |
| 
 | |
| func NewLomsService(orderRepo OrderRepository, stockRepo StockRepository, txManager txManager, statusProducer StatusProducer) *LomsService {
 | |
| 	return &LomsService{
 | |
| 		orders:         orderRepo,
 | |
| 		stocks:         stockRepo,
 | |
| 		txManager:      txManager,
 | |
| 		statusProducer: statusProducer,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *LomsService) rollbackStocks(ctx context.Context, stocks []*entity.Stock) {
 | |
| 	ctx, span := tracing.Tracer().Start(ctx, "rollbackStocks")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	for _, stock := range stocks {
 | |
| 		if err := s.stocks.StockCancel(ctx, stock); err != nil {
 | |
| 			log.Error().Err(err).Msg("failed to rollback stock")
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Wraps writing status to DB and status topic.
 | |
| // Should use this function for status updates.
 | |
| // Guarantees that status upd event will be sent only if DB write is successful.
 | |
| func (s *LomsService) setStatus(ctx context.Context, id entity.ID, status string) error {
 | |
| 	log.Trace().Msgf("running status update for %d with status %s", id, status)
 | |
| 
 | |
| 	if err := s.orders.OrderSetStatus(ctx, id, status); err != nil {
 | |
| 		return fmt.Errorf("orders.OrderSetStatus: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := s.statusProducer.Send(ctx, id, status); err != nil {
 | |
| 		log.Error().Err(err).Msg("statusProducer.Send")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *LomsService) createInitial(ctx context.Context, orderReq *pb.OrderCreateRequest) (*entity.Order, error) {
 | |
| 	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)
 | |
| 	})
 | |
| 
 | |
| 	err := s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
 | |
| 		id, err := s.orders.OrderCreate(txCtx, order)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		order.OrderID = id
 | |
| 
 | |
| 		return nil
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return order, nil
 | |
| }
 | |
| 
 | |
| func (s *LomsService) OrderCreate(ctx context.Context, orderReq *pb.OrderCreateRequest) (entity.ID, error) {
 | |
| 	ctx, span := tracing.Tracer().Start(ctx, "OrderCreate")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	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, err := s.createInitial(ctx, orderReq)
 | |
| 	if err != nil {
 | |
| 		return 0, err
 | |
| 	}
 | |
| 
 | |
| 	if statErr := s.setStatus(ctx, order.OrderID, order.Status); statErr != nil {
 | |
| 		return 0, statErr
 | |
| 	}
 | |
| 
 | |
| 	var resErr error
 | |
| 
 | |
| 	err = s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
 | |
| 		committed := make([]*entity.Stock, 0, len(order.Items))
 | |
| 		for _, it := range order.Items {
 | |
| 			st := &entity.Stock{Item: it, Reserved: it.Count}
 | |
| 			if resErr = s.stocks.StockReserve(txCtx, st); resErr != nil {
 | |
| 				s.rollbackStocks(txCtx, committed)
 | |
| 
 | |
| 				resErr = fmt.Errorf("stocks.StockReserve: %w", resErr)
 | |
| 
 | |
| 				return nil
 | |
| 			}
 | |
| 			committed = append(committed, st)
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	})
 | |
| 
 | |
| 	finalStatus := pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String()
 | |
| 
 | |
| 	defer func() {
 | |
| 		if statErr := s.setStatus(ctx, order.OrderID, finalStatus); statErr != nil {
 | |
| 			log.Error().Err(statErr).Msgf("failed to setStatus to %s", finalStatus)
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	if err != nil {
 | |
| 		finalStatus = pb.OrderStatus_ORDER_STATUS_FAILED.String()
 | |
| 
 | |
| 		return 0, err
 | |
| 	}
 | |
| 	if resErr != nil {
 | |
| 		finalStatus = pb.OrderStatus_ORDER_STATUS_FAILED.String()
 | |
| 
 | |
| 		return 0, resErr
 | |
| 	}
 | |
| 
 | |
| 	return order.OrderID, nil
 | |
| }
 | |
| 
 | |
| func (s *LomsService) OrderInfo(ctx context.Context, orderID entity.ID) (*entity.Order, error) {
 | |
| 	ctx, span := tracing.Tracer().Start(ctx, "OrderInfo")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	if orderID <= 0 {
 | |
| 		return nil, model.ErrInvalidInput
 | |
| 	}
 | |
| 
 | |
| 	return s.orders.OrderGetByID(ctx, orderID)
 | |
| }
 | |
| 
 | |
| func (s *LomsService) OrderPay(ctx context.Context, orderID entity.ID) error {
 | |
| 	ctx, span := tracing.Tracer().Start(ctx, "OrderPay")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	if orderID <= 0 {
 | |
| 		return model.ErrInvalidInput
 | |
| 	}
 | |
| 
 | |
| 	return s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
 | |
| 		order, err := s.orders.OrderGetByID(txCtx, 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 _, it := range order.Items {
 | |
| 				if err := s.stocks.StockReserveRemove(txCtx, &entity.Stock{
 | |
| 					Item:     it,
 | |
| 					Reserved: it.Count,
 | |
| 				}); err != nil {
 | |
| 					log.Error().Err(err).Msg("failed to free stock reservation")
 | |
| 				}
 | |
| 			}
 | |
| 			return s.setStatus(txCtx, orderID,
 | |
| 				pb.OrderStatus_ORDER_STATUS_PAYED.String())
 | |
| 		default:
 | |
| 			return model.ErrOrderInvalidStatus
 | |
| 		}
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (s *LomsService) OrderCancel(ctx context.Context, orderID entity.ID) error {
 | |
| 	ctx, span := tracing.Tracer().Start(ctx, "OrderCancel")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	if orderID <= 0 {
 | |
| 		return model.ErrInvalidInput
 | |
| 	}
 | |
| 
 | |
| 	return s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
 | |
| 		order, err := s.orders.OrderGetByID(txCtx, 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
 | |
| 		}
 | |
| 
 | |
| 		for _, it := range order.Items {
 | |
| 			if err := s.stocks.StockCancel(txCtx, &entity.Stock{
 | |
| 				Item:     it,
 | |
| 				Reserved: it.Count,
 | |
| 			}); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 		return s.setStatus(txCtx, orderID,
 | |
| 			pb.OrderStatus_ORDER_STATUS_CANCELLED.String())
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (s *LomsService) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) {
 | |
| 	ctx, span := tracing.Tracer().Start(ctx, "StocksInfo")
 | |
| 	defer span.End()
 | |
| 
 | |
| 	if sku <= 0 {
 | |
| 		return 0, model.ErrInvalidInput
 | |
| 	}
 | |
| 
 | |
| 	stock, err := s.stocks.StockGetByID(ctx, sku)
 | |
| 	if err != nil {
 | |
| 		return 0, err
 | |
| 	}
 | |
| 
 | |
| 	return stock.Item.Count - stock.Reserved, nil
 | |
| }
 | 
