package service import ( "context" "fmt" "slices" "sync" "github.com/rs/zerolog/log" "route256/cart/internal/domain/entity" "route256/cart/internal/domain/model" ) //go:generate minimock -i Repository -o ./mock -s _mock.go type Repository interface { AddItem(ctx context.Context, userID entity.UID, item *model.Item) error GetItemsByUserID(ctx context.Context, userID entity.UID) (entity.Cart, error) DeleteItem(ctx context.Context, userID entity.UID, sku entity.Sku) error DeleteItemsByUserID(ctx context.Context, userID entity.UID) error } type ProductService interface { GetProductBySku(ctx context.Context, sku entity.Sku) (*model.Product, error) } type LomsService interface { OrderCreate(ctx context.Context, cart *model.Cart) (int64, error) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) } type CartService struct { repository Repository productService ProductService lomsService LomsService } func NewCartService(repository Repository, productService ProductService, lomsService LomsService) *CartService { return &CartService{ repository: repository, productService: productService, lomsService: lomsService, } } func (s *CartService) AddItem(ctx context.Context, userID entity.UID, item *model.Item) error { if err := item.Validate(); err != nil { return fmt.Errorf("invalid requested values: %w", err) } if userID <= 0 { return fmt.Errorf("invalid userID") } _, err := s.productService.GetProductBySku(ctx, item.Product.Sku) if err != nil { return fmt.Errorf("productService.GetProductBySku: %w", err) } count, err := s.lomsService.StocksInfo(ctx, item.Product.Sku) if err != nil { return fmt.Errorf("lomsService.StocksInfo: %w", err) } if count < item.Count { return model.ErrNotEnoughStocks } if err := s.repository.AddItem(ctx, userID, item); err != nil { return fmt.Errorf("repository.AddItemToCart: %w", err) } return nil } // GetUserCart gets all user cart's item ids, gets the item description from the product-service // and return a list of the collected items. // In case of failed request to product-service, return nothing and error. // // TODO: add worker group, BUT it's OK for now, // assuming user does not have hundreds of different items in his cart. func (s *CartService) GetItemsByUserID(ctx context.Context, userID entity.UID) (*model.Cart, error) { if userID <= 0 { return nil, fmt.Errorf("userID invalid") } cart, err := s.repository.GetItemsByUserID(ctx, userID) if err != nil { return nil, fmt.Errorf("repository.AddItemToCart: %w", err) } if len(cart.Items) == 0 { return nil, model.ErrCartNotFound } ctx, cancel := context.WithCancel(ctx) defer cancel() resultCart := &model.Cart{ UserID: userID, Items: make([]*model.Item, len(cart.Items)), TotalPrice: 0, } errCh := make(chan error, 1) var ( wg sync.WaitGroup sumMutex sync.Mutex ) for idx, sku := range cart.Items { wg.Add(1) go func(sku entity.Sku, count uint32, idx int) { defer wg.Done() product, err := s.productService.GetProductBySku(ctx, sku) if err != nil { select { case errCh <- fmt.Errorf("productService.GetProductBySku: %w", err): case <-ctx.Done(): } log.Error().Err(err).Msg("productService.GetProductBySku") return } resultCart.Items[idx] = &model.Item{ Product: product, Count: count, } sumMutex.Lock() resultCart.TotalPrice += uint32(product.Price) * count sumMutex.Unlock() }(sku, cart.ItemCount[sku], idx) } doneCh := make(chan struct{}) go func() { wg.Wait() close(doneCh) }() select { case err := <-errCh: cancel() return nil, err case <-doneCh: slices.SortStableFunc(resultCart.Items, func(a, b *model.Item) int { return int(a.Product.Sku - b.Product.Sku) }) return resultCart, nil } } func (s *CartService) DeleteItem(ctx context.Context, userID entity.UID, sku entity.Sku) error { if userID <= 0 { return fmt.Errorf("userID invalid") } if sku <= 0 { return fmt.Errorf("sku invalid") } if err := s.repository.DeleteItem(ctx, userID, sku); err != nil { return fmt.Errorf("repository.DeleteItemFromUserCart: %w", err) } return nil } func (s *CartService) DeleteItemsByUserID(ctx context.Context, userID entity.UID) error { if userID <= 0 { return fmt.Errorf("userID invalid") } if err := s.repository.DeleteItemsByUserID(ctx, userID); err != nil { return fmt.Errorf("repository.DeleteUserCart: %w", err) } return nil } func (s *CartService) CheckoutUserCart(ctx context.Context, userID entity.UID) (int64, error) { if userID <= 0 { return 0, fmt.Errorf("userID invalid") } cart, err := s.GetItemsByUserID(ctx, entity.UID(userID)) if err != nil { return 0, err } orderID, err := s.lomsService.OrderCreate(ctx, cart) if err != nil { return 0, fmt.Errorf("lomsService.OrderCreate: %w", err) } if err := s.DeleteItemsByUserID(ctx, userID); err != nil { return 0, err } return orderID, nil }