[hw-3] loms service

This commit is contained in:
Никита Шубин
2025-06-20 10:11:59 +00:00
parent c8e056bc99
commit b88dfe6db5
73 changed files with 8837 additions and 52 deletions

View File

@@ -1,7 +1,7 @@
package entity
type (
UID uint64
UID int64
Sku int64
)

View File

@@ -4,6 +4,7 @@ import "errors"
var (
ErrProductNotFound = errors.New("invalid sku")
ErrNotEnoughStocks = errors.New("not enough stocks")
ErrCartNotFound = errors.New("cart not found")
ErrItemNotFoundInCart = errors.New("item not found in cart")

View File

@@ -1,75 +0,0 @@
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"`
}

View File

@@ -6,10 +6,10 @@ import (
"slices"
"sync"
"github.com/rs/zerolog/log"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"github.com/rs/zerolog/log"
)
//go:generate minimock -i Repository -o ./mock -s _mock.go
@@ -24,15 +24,23 @@ type ProductService interface {
GetProductBySku(ctx context.Context, sku entity.Sku) (*model.Product, error)
}
type CartService struct {
repository Repository
productService ProductService
type LomsService interface {
OrderCreate(ctx context.Context, cart *model.Cart) (int64, error)
StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error)
}
func NewCartService(repository Repository, productService ProductService) *CartService {
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,
}
}
@@ -50,6 +58,15 @@ func (s *CartService) AddItem(ctx context.Context, userID entity.UID, item *mode
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)
}
@@ -170,3 +187,25 @@ func (s *CartService) DeleteItemsByUserID(ctx context.Context, userID entity.UID
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
}

View File

@@ -9,9 +9,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"route256/cart/internal/domain/cart/service/mock"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"route256/cart/internal/domain/service/mock"
)
const (
@@ -19,9 +19,9 @@ const (
validName = "some product name"
)
type ProductServiceFake struct{}
type productServiceFake struct{}
func (f *ProductServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) {
func (f *productServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) {
if sku%2 == 0 {
return nil, errors.New("empty shelf")
}
@@ -33,6 +33,24 @@ func (f *ProductServiceFake) GetProductBySku(_ context.Context, sku entity.Sku)
}, nil
}
type lomsServiceFake struct{}
func (f *lomsServiceFake) StocksInfo(_ context.Context, sku entity.Sku) (uint32, error) {
if sku == 1111 {
return 0, errors.New("stock error")
}
return 100, nil
}
func (f *lomsServiceFake) OrderCreate(_ context.Context, cart *model.Cart) (int64, error) {
if cart.UserID == 1111 {
return 0, errors.New("order create error")
}
return 1234, nil
}
func TestCartService_AddItem(t *testing.T) {
t.Parallel()
@@ -49,9 +67,20 @@ func TestCartService_AddItem(t *testing.T) {
Count: 1,
}
testSKULomsFailing := entity.Sku(1111)
testItemLomsFailing := model.Item{
Product: &model.Product{
Name: validName,
Price: validPrice,
Sku: testSKULomsFailing,
},
Count: 1,
}
type fields struct {
repository Repository
productService ProductService
lomsService LomsService
}
type args struct {
ctx context.Context
@@ -72,7 +101,8 @@ func TestCartService_AddItem(t *testing.T) {
AddItemMock.
Expect(ctx, 1337, &testItem).
Return(nil),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{
ctx: ctx,
@@ -118,7 +148,8 @@ func TestCartService_AddItem(t *testing.T) {
name: "product service error",
fields: fields{
repository: nil,
productService: &ProductServiceFake{},
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{
ctx: ctx,
@@ -141,7 +172,8 @@ func TestCartService_AddItem(t *testing.T) {
AddItemMock.
Expect(ctx, 1337, &testItem).
Return(assert.AnError),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{
ctx: ctx,
@@ -150,6 +182,41 @@ func TestCartService_AddItem(t *testing.T) {
},
wantErr: require.Error,
},
{
name: "stocks acquiring error",
fields: fields{
repository: nil,
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{
ctx: ctx,
item: &testItemLomsFailing,
userID: 1337,
},
wantErr: require.Error,
},
{
name: "not enough stocks",
fields: fields{
repository: nil,
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{
ctx: ctx,
item: &model.Item{
Product: &model.Product{
Name: validName,
Price: validPrice,
Sku: testSKU,
},
Count: 10000,
},
userID: 1337,
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -158,6 +225,7 @@ func TestCartService_AddItem(t *testing.T) {
s := &CartService{
repository: tt.fields.repository,
productService: tt.fields.productService,
lomsService: tt.fields.lomsService,
}
err := s.AddItem(tt.args.ctx, tt.args.userID, tt.args.item)
@@ -218,7 +286,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(testEntityCart, nil),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
},
args: args{
ctx: ctx,
@@ -238,7 +306,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
Items: []entity.Sku{testSKU},
ItemCount: map[entity.Sku]uint32{testSKU: 2},
}, nil),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
},
args: args{
ctx: ctx,
@@ -271,7 +339,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
Items: []entity.Sku{testSKU, 1},
ItemCount: map[entity.Sku]uint32{testSKU: 1, 1: 1},
}, nil),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
},
args: args{
ctx: ctx,
@@ -357,7 +425,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
Items: []entity.Sku{2},
ItemCount: map[entity.Sku]uint32{2: 1},
}, nil),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
},
args: args{
ctx: ctx,
@@ -377,7 +445,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
Items: []entity.Sku{2},
ItemCount: map[entity.Sku]uint32{2: 1},
}, nil),
productService: &ProductServiceFake{},
productService: &productServiceFake{},
},
args: args{
ctx: ctx,
@@ -573,3 +641,118 @@ func TestCartService_DeleteItemsByUserID(t *testing.T) {
})
}
}
func TestCartService_CheckoutUserCart(t *testing.T) {
t.Parallel()
ctx := context.Background()
mc := minimock.NewController(t)
testSKU := entity.Sku(199)
testCart := entity.Cart{
Items: []entity.Sku{testSKU},
ItemCount: map[entity.Sku]uint32{testSKU: 1},
}
type fields struct {
repository Repository
productService ProductService
lomsService LomsService
}
type args struct {
ctx context.Context
userID entity.UID
}
tests := []struct {
name string
fields fields
args args
wantOrderID int64
wantErr require.ErrorAssertionFunc
}{
{
name: "success",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1337).
Return(testCart, nil).
DeleteItemsByUserIDMock.
Expect(ctx, 1337).
Return(nil),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{ctx: ctx, userID: 1337},
wantOrderID: 1234,
wantErr: require.NoError,
},
{
name: "invalid user id",
fields: fields{},
args: args{ctx: ctx, userID: 0},
wantErr: require.Error,
},
{
name: "get cart error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1337).
Return(entity.Cart{}, assert.AnError),
},
args: args{ctx: ctx, userID: 1337},
wantErr: require.Error,
},
{
name: "order create error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1111).
Return(testCart, nil),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{ctx: ctx, userID: 1111},
wantErr: require.Error,
},
{
name: "delete order error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, 1337).
Return(testCart, nil).
DeleteItemsByUserIDMock.
Expect(ctx, 1337).
Return(assert.AnError),
productService: &productServiceFake{},
lomsService: &lomsServiceFake{},
},
args: args{ctx: ctx, userID: 1337},
wantErr: require.Error,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
svc := &CartService{
repository: tt.fields.repository,
productService: tt.fields.productService,
lomsService: tt.fields.lomsService,
}
orderID, err := svc.CheckoutUserCart(tt.args.ctx, tt.args.userID)
tt.wantErr(t, err)
if err == nil {
require.Equal(t, tt.wantOrderID, orderID)
}
})
}
}