[hw-6] add notifier service, kafka

This commit is contained in:
Никита Шубин
2025-07-17 19:20:27 +00:00
parent 424d6905da
commit 6e1ad86128
33 changed files with 1412 additions and 92 deletions

View File

@@ -35,17 +35,23 @@ type txManager interface {
ReadWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error)
}
type LomsService struct {
orders OrderRepository
stocks StockRepository
txManager txManager
type StatusProducer interface {
Send(ctx context.Context, id entity.ID, status string) error
}
func NewLomsService(orderRepo OrderRepository, stockRepo StockRepository, txManager txManager) *LomsService {
type LomsService struct {
orders OrderRepository
stocks StockRepository
txManager txManager
statusProducer StatusProducer
}
func NewLomsService(orderRepo OrderRepository, stockRepo StockRepository, txManager txManager, statusProducer StatusProducer) *LomsService {
return &LomsService{
orders: orderRepo,
stocks: stockRepo,
txManager: txManager,
orders: orderRepo,
stocks: stockRepo,
txManager: txManager,
statusProducer: statusProducer,
}
}
@@ -57,17 +63,24 @@ func (s *LomsService) rollbackStocks(ctx context.Context, stocks []*entity.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
// Wraps writing status to DB and status topic.
// Should use this function for status updates.
// Guarantees that status upd event will be sent only if DB write is successful.
func (s *LomsService) setStatus(ctx context.Context, id entity.ID, status string) error {
log.Trace().Msgf("running status update for %d with status %s", id, status)
if err := s.orders.OrderSetStatus(ctx, id, status); err != nil {
return fmt.Errorf("orders.OrderSetStatus: %w", err)
}
for _, item := range orderReq.Items {
if item.Sku <= 0 || item.Count == 0 {
return 0, model.ErrInvalidInput
}
if err := s.statusProducer.Send(ctx, id, status); err != nil {
log.Error().Err(err).Msg("statusProducer.Send")
}
return nil
}
func (s *LomsService) createInitial(ctx context.Context, orderReq *pb.OrderCreateRequest) (*entity.Order, error) {
order := &entity.Order{
OrderID: 0,
Status: pb.OrderStatus_ORDER_STATUS_NEW.String(),
@@ -86,44 +99,82 @@ func (s *LomsService) OrderCreate(ctx context.Context, orderReq *pb.OrderCreateR
return int(a.ID - b.ID)
})
var (
orderID entity.ID
resErr error
)
err := s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
id, err := s.orders.OrderCreate(txCtx, order)
if err != nil {
return err
}
order.OrderID = id
orderID = id
order.OrderID = id
return nil
})
if err != nil {
return nil, err
}
return order, nil
}
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, err := s.createInitial(ctx, orderReq)
if err != nil {
return 0, err
}
if statErr := s.setStatus(ctx, order.OrderID, order.Status); statErr != nil {
return 0, statErr
}
var resErr error
err = s.txManager.WriteWithTransaction(ctx, func(txCtx context.Context) error {
committed := make([]*entity.Stock, 0, len(order.Items))
for _, it := range order.Items {
st := &entity.Stock{Item: it, Reserved: it.Count}
if err := s.stocks.StockReserve(txCtx, st); err != nil {
if resErr = s.stocks.StockReserve(txCtx, st); resErr != nil {
s.rollbackStocks(txCtx, committed)
_ = s.orders.OrderSetStatus(txCtx, id,
pb.OrderStatus_ORDER_STATUS_FAILED.String())
resErr = fmt.Errorf("stocks.StockReserve: %w", resErr)
resErr = fmt.Errorf("stocks.StockReserve: %w", err)
return nil
}
committed = append(committed, st)
}
return s.orders.OrderSetStatus(txCtx, id,
pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String())
return nil
})
finalStatus := pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String()
defer func() {
if statErr := s.setStatus(ctx, order.OrderID, finalStatus); statErr != nil {
log.Error().Err(statErr).Msgf("failed to setStatus to %s", finalStatus)
}
}()
if err != nil {
finalStatus = pb.OrderStatus_ORDER_STATUS_FAILED.String()
return 0, err
}
if resErr != nil {
finalStatus = pb.OrderStatus_ORDER_STATUS_FAILED.String()
return 0, resErr
}
return orderID, nil
return order.OrderID, nil
}
func (s *LomsService) OrderInfo(ctx context.Context, orderID entity.ID) (*entity.Order, error) {
@@ -157,7 +208,7 @@ func (s *LomsService) OrderPay(ctx context.Context, orderID entity.ID) error {
log.Error().Err(err).Msg("failed to free stock reservation")
}
}
return s.orders.OrderSetStatus(txCtx, orderID,
return s.setStatus(txCtx, orderID,
pb.OrderStatus_ORDER_STATUS_PAYED.String())
default:
return model.ErrOrderInvalidStatus
@@ -192,7 +243,7 @@ func (s *LomsService) OrderCancel(ctx context.Context, orderID entity.ID) error
return err
}
}
return s.orders.OrderSetStatus(txCtx, orderID,
return s.setStatus(txCtx, orderID,
pb.OrderStatus_ORDER_STATUS_CANCELLED.String())
})
}

View File

@@ -40,6 +40,12 @@ func (t *mockTxManager) ReadWithRepeatableRead(ctx context.Context, fn func(ctx
return fn(ctx)
}
type mockKafkaProducer struct{}
func (kp mockKafkaProducer) Send(_ context.Context, _ entity.ID, _ string) error {
return nil
}
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
@@ -138,8 +144,7 @@ func TestLomsService_OrderCreate(t *testing.T) {
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")),
stocks: mock.NewStockRepositoryMock(mc),
},
args: args{
req: goodReq,
@@ -152,8 +157,7 @@ func TestLomsService_OrderCreate(t *testing.T) {
orders: mock.NewOrderRepositoryMock(mc).
OrderCreateMock.Return(1, nil).
OrderSetStatusMock.Return(errors.New("unexpected error")),
stocks: mock.NewStockRepositoryMock(mc).
StockReserveMock.Return(nil),
stocks: mock.NewStockRepositoryMock(mc),
},
args: args{
req: goodReq,
@@ -167,7 +171,7 @@ func TestLomsService_OrderCreate(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{})
svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
_, err := svc.OrderCreate(ctx, tt.args.req)
tt.wantErr(t, err)
})
@@ -278,7 +282,7 @@ func TestLomsService_OrderPay(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{})
svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
err := svc.OrderPay(ctx, tt.args.id)
tt.wantErr(t, err)
})
@@ -292,6 +296,7 @@ func TestLomsService_OrderInfoBadInput(t *testing.T) {
nil,
nil,
&mockTxManager{},
&mockKafkaProducer{},
)
_, err := svc.OrderInfo(context.Background(), 0)
@@ -313,6 +318,7 @@ func TestLomsService_OrderInfoSuccess(t *testing.T) {
mock.NewOrderRepositoryMock(mc).OrderGetByIDMock.Return(order, nil),
nil,
&mockTxManager{},
&mockKafkaProducer{},
)
gotOrder, err := svc.OrderInfo(context.Background(), 123)
@@ -414,7 +420,7 @@ func TestLomsService_OrderCancel(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{})
svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
err := svc.OrderCancel(ctx, tt.args.id)
tt.wantErr(t, err)
})
@@ -481,7 +487,7 @@ func TestLomsService_StocksInfo(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
svc := NewLomsService(nil, tt.fields.stocks, &mockTxManager{})
svc := NewLomsService(nil, tt.fields.stocks, &mockTxManager{}, &mockKafkaProducer{})
got, err := svc.StocksInfo(ctx, tt.args.sku)
tt.wantErr(t, err)
if err == nil {