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 }