[hw-1] implement cart service

This commit is contained in:
Никита Шубин
2025-05-25 15:49:17 +00:00
parent 3d3f10647b
commit 5077f04b0c
28 changed files with 1151 additions and 2 deletions

View File

@@ -0,0 +1,118 @@
package repository
import (
"context"
"sync"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
)
type storage = map[entity.UID]*entity.Cart
type InMemoryRepository struct {
storage storage
mx sync.RWMutex
}
func NewInMemoryRepository(cap int) *InMemoryRepository {
return &InMemoryRepository{
storage: make(storage, cap),
}
}
func (r *InMemoryRepository) AddItem(_ context.Context, userID entity.UID, item *model.Item) error {
r.mx.Lock()
defer r.mx.Unlock()
cart, ok := r.storage[userID]
if !ok {
cart = &entity.Cart{
UserID: userID,
Items: []entity.Sku{},
ItemCount: map[entity.Sku]uint32{},
}
r.storage[userID] = cart
}
if _, ok := cart.ItemCount[item.Product.Sku]; !ok {
cart.Items = append(cart.Items, item.Product.Sku)
}
cart.ItemCount[item.Product.Sku] += item.Count
return nil
}
func (r *InMemoryRepository) GetItemsByUserID(_ context.Context, userID entity.UID) (entity.Cart, error) {
r.mx.Lock()
defer r.mx.Unlock()
cart, ok := r.storage[userID]
if !ok {
return entity.Cart{}, nil
}
resultCart := entity.Cart{
UserID: userID,
Items: make([]entity.Sku, len(cart.Items)),
ItemCount: make(map[entity.Sku]uint32, len(cart.ItemCount)),
}
for i, sku := range cart.Items {
resultCart.Items[i] = cart.Items[i]
resultCart.ItemCount[sku] = cart.ItemCount[sku]
}
return resultCart, nil
}
func (r *InMemoryRepository) DeleteItem(_ context.Context, userID entity.UID, sku entity.Sku) error {
r.mx.Lock()
defer r.mx.Unlock()
cart, ok := r.storage[userID]
if !ok {
return model.ErrCartNotFound
}
if len(cart.Items) == 0 {
return nil
}
delete(cart.ItemCount, sku)
i := 0
found := false
for _, v := range cart.Items {
if v == sku {
found = true
break
}
i++
}
if !found {
return model.ErrItemNotFoundInCart
}
cart.Items[i] = cart.Items[len(cart.Items)-1]
cart.Items = cart.Items[:len(cart.Items)-1]
return nil
}
func (r *InMemoryRepository) DeleteItemsByUserID(_ context.Context, userID entity.UID) error {
r.mx.Lock()
defer r.mx.Unlock()
_, ok := r.storage[userID]
if ok {
delete(r.storage, userID)
}
return nil
}

View File

@@ -0,0 +1,165 @@
package service
import (
"context"
"fmt"
"slices"
"sync"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"github.com/rs/zerolog/log"
)
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 CartService struct {
repository Repository
productService ProductService
}
func NewCartService(repository Repository, productService ProductService) *CartService {
return &CartService{
repository: repository,
productService: productService,
}
}
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)
}
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
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,
}
resultCart.TotalPrice += uint32(product.Price) * count
}(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
}

View File

@@ -0,0 +1,12 @@
package entity
type (
UID uint64
Sku int64
)
type Cart struct {
UserID UID
Items []Sku
ItemCount map[Sku]uint32
}

View File

@@ -0,0 +1,9 @@
package model
import "route256/cart/internal/domain/entity"
type Cart struct {
UserID entity.UID
Items []*Item
TotalPrice uint32
}

View File

@@ -0,0 +1,10 @@
package model
import "errors"
var (
ErrProductNotFound = errors.New("invalid sku")
ErrCartNotFound = errors.New("cart not found")
ErrItemNotFoundInCart = errors.New("item not found in cart")
)

View File

@@ -0,0 +1,16 @@
package model
import "fmt"
type Item struct {
Product *Product
Count uint32 `validate:"gt=0"`
}
func (i *Item) Validate() error {
if err := validate.Struct(i); err != nil {
return fmt.Errorf("invalid requested values: %w", err)
}
return nil
}

View File

@@ -0,0 +1,21 @@
package model
import (
"fmt"
"route256/cart/internal/domain/entity"
)
type Product struct {
Name string
Price int32
Sku entity.Sku `validate:"gt=0"`
}
func (p *Product) Validate() error {
if err := validate.Struct(p); err != nil {
return fmt.Errorf("invalid requested values: %w", err)
}
return nil
}

View File

@@ -0,0 +1,9 @@
package model
import "github.com/go-playground/validator/v10"
var validate *validator.Validate
func init() {
validate = validator.New()
}

View File

@@ -0,0 +1,75 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
)
type ProductService struct {
httpClient http.Client
token string
address string
}
func NewProductService(httpClient http.Client, token string, address string) *ProductService {
return &ProductService{
httpClient: httpClient,
token: token,
address: address,
}
}
func (s *ProductService) GetProductBySku(ctx context.Context, sku entity.Sku) (*model.Product, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("http://%s/product/%d", s.address, sku),
http.NoBody,
)
if err != nil {
return nil, fmt.Errorf("http.NewRequestWithContext: %w", err)
}
req.Header.Add("X-API-KEY", s.token)
response, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("httpClient.Do: %w", err)
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotFound {
return nil, model.ErrProductNotFound
}
if response.StatusCode != http.StatusOK {
return nil, errors.New("status not ok")
}
resp := &GetProductResponse{}
if err := json.NewDecoder(response.Body).Decode(resp); err != nil {
return nil, fmt.Errorf("json.NewDecoder: %w", err)
}
return &model.Product{
Name: resp.Name,
Price: resp.Price,
Sku: entity.Sku(resp.Sku),
}, nil
}
type GetProductResponse struct {
Name string `json:"name"`
Price int32 `json:"price"`
Sku int64 `json:"sku"`
}