mirror of
https://github.com/3ybactuk/marketplace-go-service-project.git
synced 2025-10-30 14:03:45 +03:00
[hw-3] loms service
This commit is contained in:
1160
loms/internal/domain/service/mock/order_repository_mock.go
Normal file
1160
loms/internal/domain/service/mock/order_repository_mock.go
Normal file
File diff suppressed because it is too large
Load Diff
1483
loms/internal/domain/service/mock/stock_repository_mock.go
Normal file
1483
loms/internal/domain/service/mock/stock_repository_mock.go
Normal file
File diff suppressed because it is too large
Load Diff
186
loms/internal/domain/service/service.go
Normal file
186
loms/internal/domain/service/service.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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
|
||||
}
|
||||
448
loms/internal/domain/service/service_test.go
Normal file
448
loms/internal/domain/service/service_test.go
Normal file
@@ -0,0 +1,448 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/gojuno/minimock/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"route256/loms/internal/domain/entity"
|
||||
"route256/loms/internal/domain/model"
|
||||
mock "route256/loms/internal/domain/service/mock"
|
||||
|
||||
pb "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
testUser = entity.ID(1337)
|
||||
testSku = entity.Sku(199)
|
||||
)
|
||||
|
||||
func TestLomsService_OrderCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
goodReq := &pb.OrderCreateRequest{
|
||||
UserId: int64(testUser),
|
||||
Items: []*pb.OrderItem{
|
||||
{
|
||||
Sku: int64(testSku),
|
||||
Count: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
badItemReq := &pb.OrderCreateRequest{
|
||||
UserId: int64(testUser),
|
||||
Items: []*pb.OrderItem{
|
||||
{
|
||||
Sku: 0,
|
||||
Count: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
type args struct {
|
||||
req *pb.OrderCreateRequest
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(1, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
req: &pb.OrderCreateRequest{
|
||||
UserId: 0,
|
||||
},
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid input with bad items",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
req: badItemReq,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "order create error",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(0, errors.New("order create error")),
|
||||
stocks: nil,
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "stock reserve error",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(1, nil).
|
||||
OrderSetStatusMock.Return(errors.New("status update error")),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveMock.Return(errors.New("reservation error")),
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "final status update error",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderCreateMock.Return(1, nil).
|
||||
OrderSetStatusMock.Return(errors.New("unexpected error")),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveMock.Return(nil).
|
||||
StockCancelMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
req: goodReq,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(tt.fields.orders, tt.fields.stocks)
|
||||
_, err := svc.OrderCreate(ctx, tt.args.req)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLomsService_OrderPay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
awaitingOrder := &entity.Order{
|
||||
OrderID: 1,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(),
|
||||
Items: []entity.OrderItem{{
|
||||
ID: testSku,
|
||||
Count: 2,
|
||||
}},
|
||||
}
|
||||
|
||||
payedOrder := &entity.Order{OrderID: 2, Status: pb.OrderStatus_ORDER_STATUS_PAYED.String()}
|
||||
badStatusOrder := &entity.Order{OrderID: 3, Status: pb.OrderStatus_ORDER_STATUS_FAILED.String()}
|
||||
|
||||
type fields struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
type args struct {
|
||||
id entity.ID
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(awaitingOrder, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveRemoveMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
id: awaitingOrder.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc),
|
||||
stocks: mock.NewStockRepositoryMock(mc),
|
||||
},
|
||||
args: args{
|
||||
id: 0,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already payed",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(payedOrder, nil),
|
||||
},
|
||||
args: args{
|
||||
id: payedOrder.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid status",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(badStatusOrder, nil),
|
||||
},
|
||||
args: args{
|
||||
id: badStatusOrder.OrderID,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrOrderInvalidStatus)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unexpected logged error on updating stocks reserves",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(awaitingOrder, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockReserveRemoveMock.Return(errors.New("unexpected error")),
|
||||
},
|
||||
args: args{
|
||||
id: badStatusOrder.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(tt.fields.orders, tt.fields.stocks)
|
||||
err := svc.OrderPay(ctx, tt.args.id)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLomsService_OrderInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mc := minimock.NewController(t)
|
||||
svc := NewLomsService(
|
||||
mock.NewOrderRepositoryMock(mc),
|
||||
mock.NewStockRepositoryMock(mc),
|
||||
)
|
||||
|
||||
err := svc.OrderPay(context.Background(), 0)
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
}
|
||||
|
||||
func TestLomsService_OrderCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
awaiting := &entity.Order{
|
||||
OrderID: 1,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(),
|
||||
Items: []entity.OrderItem{{
|
||||
ID: testSku, Count: 1,
|
||||
}},
|
||||
}
|
||||
cancelled := &entity.Order{
|
||||
OrderID: 2,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_CANCELLED.String(),
|
||||
}
|
||||
payed := &entity.Order{
|
||||
OrderID: 3,
|
||||
Status: pb.OrderStatus_ORDER_STATUS_PAYED.String(),
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
orders OrderRepository
|
||||
stocks StockRepository
|
||||
}
|
||||
type args struct {
|
||||
id entity.ID
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(awaiting, nil).
|
||||
OrderSetStatusMock.Return(nil),
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockCancelMock.Return(nil),
|
||||
},
|
||||
args: args{
|
||||
id: awaiting.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc),
|
||||
stocks: mock.NewStockRepositoryMock(mc),
|
||||
},
|
||||
args: args{
|
||||
id: 0,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already cancelled",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(cancelled, nil),
|
||||
},
|
||||
args: args{
|
||||
id: cancelled.OrderID,
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid status",
|
||||
fields: fields{
|
||||
orders: mock.NewOrderRepositoryMock(mc).
|
||||
OrderGetByIDMock.Return(payed, nil),
|
||||
},
|
||||
args: args{
|
||||
id: payed.OrderID,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrOrderInvalidStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(tt.fields.orders, tt.fields.stocks)
|
||||
err := svc.OrderCancel(ctx, tt.args.id)
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLomsService_StocksInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
type fields struct{ stocks StockRepository }
|
||||
type args struct{ sku entity.Sku }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want uint32
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockGetByIDMock.Return(&entity.Stock{
|
||||
Item: entity.OrderItem{
|
||||
Count: 7,
|
||||
},
|
||||
}, nil),
|
||||
},
|
||||
args: args{
|
||||
sku: testSku,
|
||||
},
|
||||
want: 7,
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid sku",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
sku: 0,
|
||||
},
|
||||
wantErr: func(t require.TestingT, err error, _ ...interface{}) {
|
||||
require.ErrorIs(t, err, model.ErrInvalidInput)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get by id error",
|
||||
fields: fields{
|
||||
stocks: mock.NewStockRepositoryMock(mc).
|
||||
StockGetByIDMock.Return(nil, errors.New("unexpected error")),
|
||||
},
|
||||
args: args{
|
||||
sku: testSku,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := NewLomsService(nil, tt.fields.stocks)
|
||||
got, err := svc.StocksInfo(ctx, tt.args.sku)
|
||||
tt.wantErr(t, err)
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user