diff --git a/internal/order/order_service.go b/internal/order/order_service.go deleted file mode 100644 index 52a57da..0000000 --- a/internal/order/order_service.go +++ /dev/null @@ -1,98 +0,0 @@ -package order - -import ( - "context" - "exchange/internal/db" - "sync" - - "github.com/google/uuid" -) - -type OrderService struct { - orderbook OrderBookInterface - db *db.Queries - mu sync.RWMutex -} - -// OrderBookInterface defines the interface for orderbook operations -type OrderBookInterface interface { - Submit(*Order) bool - Withdraw(*Order) bool -} - -func NewOrderService(orderbook OrderBookInterface, db *db.Queries) *OrderService { - return &OrderService{ - orderbook: orderbook, - db: db, - } -} - -// SubmitOrder handles the submission of a new order, ensuring consistency between memory and database -func (s *OrderService) SubmitOrder(ctx context.Context, o *Order) error { - s.mu.Lock() - defer s.mu.Unlock() - - // First, try to submit to the orderbook - success := s.orderbook.Submit(o) - if !success { - return nil // Order was rejected by the orderbook - } - - // If successful in memory, persist to database - _, err := s.db.CreateOrder(ctx, db.CreateOrderParams{ - Price: o.Price, - Amount: o.Amount, - Side: o.Side, - OrderType: o.Type, - Asset: o.Ticker, - CreatedBy: o.CreatedBy, - }) - - if err != nil { - // If database operation fails, we need to rollback the memory operation - s.orderbook.Withdraw(o) - return err - } - - return nil -} - -// UpdateOrderStatus updates both memory and database order status -func (s *OrderService) UpdateOrderStatus(ctx context.Context, orderID uuid.UUID, status db.OrderStatusType) error { - s.mu.Lock() - defer s.mu.Unlock() - - // Update in database first - _, err := s.db.UpdateOrderStatus(ctx, db.UpdateOrderStatusParams{ - OrderStatus: status, - ID: orderID, - }) - if err != nil { - return err - } - - // If database update successful, update in memory - // Note: This is a simplified version. In a real implementation, you'd need to - // find the order in the appropriate heap and update its status - return nil -} - -// GetOrder retrieves an order from the database -func (s *OrderService) GetOrder(ctx context.Context, orderID uuid.UUID) (*Order, error) { - dbOrder, err := s.db.GetOrderById(ctx, orderID) - if err != nil { - return nil, err - } - - return &Order{ - ID: dbOrder.ID, - Price: dbOrder.Price, - Amount: dbOrder.Amount, - Side: dbOrder.Side, - Time: dbOrder.CreatedAt, - Type: dbOrder.OrderType, - Status: dbOrder.OrderStatus, - Ticker: dbOrder.Asset, - CreatedBy: dbOrder.CreatedBy, - }, nil -} diff --git a/internal/orderbook/orderbook.go b/internal/orderbook/orderbook.go index f14a045..056d824 100644 --- a/internal/orderbook/orderbook.go +++ b/internal/orderbook/orderbook.go @@ -7,8 +7,16 @@ import ( "exchange/internal/order" "fmt" "time" + + "github.com/google/uuid" ) +// OrderCreator defines the interface required for creating an order persistence mechanism. +// This allows mocking the database interaction during tests. +type OrderCreator interface { + CreateOrder(ctx context.Context, arg db.CreateOrderParams) (uuid.UUID, error) +} + type OrderBook struct { Bids *OrderHeap // max-heap (isMax = true) Asks *OrderHeap // min-heap (isMax = false) @@ -24,7 +32,7 @@ func NewOrderBook(ticker string) *OrderBook { } // Submit implements the OrderBookInterface -func (ob *OrderBook) Submit(o *order.Order, queries *db.Queries) bool { +func (ob *OrderBook) Submit(o *order.Order, creator OrderCreator) bool { if !o.IsValid() { fmt.Println("Order not valid. WRONG_ORDER") return false @@ -47,7 +55,8 @@ func (ob *OrderBook) Submit(o *order.Order, queries *db.Queries) bool { return false } - o_id, _ := queries.CreateOrder(context.Background(), db.CreateOrderParams{ + // Use the creator interface to persist the order + o_id, err := creator.CreateOrder(context.Background(), db.CreateOrderParams{ Price: o.Price, Amount: o.Amount, Side: o.Side, @@ -56,6 +65,14 @@ func (ob *OrderBook) Submit(o *order.Order, queries *db.Queries) bool { CreatedBy: o.CreatedBy, }) + // Handle potential error from CreateOrder + if err != nil { + fmt.Printf("Failed to create order in DB: %v\n", err) + // Decide how to handle DB error - perhaps mark order as failed? + // For now, we'll return false as the submission wasn't fully successful. + return false + } + o.ID = o_id fmt.Printf("%s %s %s Order submitted %d @ %.2f ID: %s\n", diff --git a/internal/orderbook/orderbook_test.go b/internal/orderbook/orderbook_test.go index 594e747..e05978d 100644 --- a/internal/orderbook/orderbook_test.go +++ b/internal/orderbook/orderbook_test.go @@ -1,6 +1,7 @@ package orderbook import ( + "context" "exchange/internal/db" "exchange/internal/order" "testing" @@ -8,6 +9,21 @@ import ( "github.com/google/uuid" ) +// mockDBQueries is a mock implementation for db.Queries, focusing on CreateOrder. +// It implements the orderbook.OrderCreator interface. +type mockDBQueries struct { + // Store the last CreateOrderParams received for potential assertions + lastCreateParams *db.CreateOrderParams +} + +// Override CreateOrder to mock database interaction. +func (m *mockDBQueries) CreateOrder(ctx context.Context, arg db.CreateOrderParams) (uuid.UUID, error) { + // Store args for assertion + m.lastCreateParams = &arg + // Return a new UUID as if the DB generated it, and no error. + return uuid.New(), nil +} + func TestNewOrderBook(t *testing.T) { ob := NewOrderBook("LINK") @@ -26,22 +42,23 @@ func TestNewOrderBook(t *testing.T) { } } -// func TestSubmitWrongOrder(t *testing.T) { -// ob := NewOrderBook("LINK") - -// new_bad_order := order.NewOrder(-10, 100, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) -// ob.Submit(new_bad_order, nil) +func TestSubmitWrongOrder(t *testing.T) { + ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} + new_bad_order := order.NewOrder(-10, 100, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) + ob.Submit(new_bad_order, mockQueries) -// if ob.Asks.Len() > 0 { -// t.Errorf("Expected order not to be submitted to the orderbook.") -// } -// } + if ob.Asks.Len() > 0 { + t.Errorf("Expected order not to be submitted to the orderbook.") + } +} func TestSubmitLimitSellOrder(t *testing.T) { ob := NewOrderBook("LINK") - test_order := order.NewOrder(60, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) + mockQueries := &mockDBQueries{} + test_order := order.NewOrder(60, 100, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) - ob.Submit(test_order, nil) + ob.Submit(test_order, mockQueries) if ob.Bids.Len() > 0 { t.Error("SELL order was transmitted as a BUY order.") } @@ -53,9 +70,10 @@ func TestSubmitLimitSellOrder(t *testing.T) { func TestSubmitLimitBuyOrder(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} test_order := order.NewOrder(60, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) - ob.Submit(test_order, nil) + ob.Submit(test_order, mockQueries) if ob.Asks.Len() > 0 { t.Error("BUY order was transmitted as a SELL order.") } @@ -67,11 +85,12 @@ func TestSubmitLimitBuyOrder(t *testing.T) { func TestSubmitMarketBuyOrder(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} test_order := order.NewOrder(10, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) test_order_sell := order.NewOrder(10, 100, db.OrderSideTypeSELL, db.OrderTypeMARKET, "LINK", uuid.New()) - ob.Submit(test_order, nil) - ob.Submit(test_order_sell, nil) + ob.Submit(test_order, mockQueries) + ob.Submit(test_order_sell, mockQueries) if ob.Asks.Len() > 0 { t.Error("BUY order was transmitted as a SELL order.") @@ -88,9 +107,10 @@ func TestSubmitMarketBuyOrder(t *testing.T) { func TestSubmitMarketSellOrder(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} test_order_sell := order.NewOrder(10, 100, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) - ob.Submit(test_order_sell, nil) + ob.Submit(test_order_sell, mockQueries) if ob.Bids.Len() > 0 { t.Error("BUY order was transmitted as a SELL order.") @@ -103,9 +123,10 @@ func TestSubmitMarketSellOrder(t *testing.T) { func TestMarketOrderNoLiquidity(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} test_order := order.NewOrder(10, 100, db.OrderSideTypeBUY, db.OrderTypeMARKET, "LINK", uuid.New()) - result := ob.Submit(test_order, nil) + result := ob.Submit(test_order, mockQueries) if result == true { t.Error("Market order was processed despite insufficient liquidity.") } @@ -113,13 +134,14 @@ func TestMarketOrderNoLiquidity(t *testing.T) { func TestWithdrawOrder(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} test_order := order.NewOrder(12, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) buy_order_to_withdraw := order.NewOrder(60, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) sell_order_to_withdraw := order.NewOrder(120, 100, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) - ob.Submit(test_order, nil) - ob.Submit(buy_order_to_withdraw, nil) - ob.Submit(sell_order_to_withdraw, nil) + ob.Submit(test_order, mockQueries) + ob.Submit(buy_order_to_withdraw, mockQueries) + ob.Submit(sell_order_to_withdraw, mockQueries) if ob.Bids.Len()+ob.Asks.Len() != 3 { t.Error("Not all orders were submitted.") @@ -163,13 +185,14 @@ func TestWithdrawBadOrder(t *testing.T) { func TestOrdersMatched(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} // Case 1: similar orders order_1 := order.NewOrder(60, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) order_2 := order.NewOrder(60, 100, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) - ob.Submit(order_1, nil) - ob.Submit(order_2, nil) + ob.Submit(order_1, mockQueries) + ob.Submit(order_2, mockQueries) if ob.Asks.Len()-ob.Bids.Len() != 0 { t.Errorf("Expected an empty order book.") @@ -179,26 +202,27 @@ func TestOrdersMatched(t *testing.T) { func TestOrdersPartiallyMatched(t *testing.T) { ob := NewOrderBook("LINK") + mockQueries := &mockDBQueries{} order_1 := order.NewOrder(60, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) order_2 := order.NewOrder(60, 50, db.OrderSideTypeSELL, db.OrderTypeLIMIT, "LINK", uuid.New()) order_3 := order.NewOrder(60, 165, db.OrderSideTypeSELL, db.OrderTypeMARKET, "LINK", uuid.New()) order_4 := order.NewOrder(62, 150, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) - ob.Submit(order_1, nil) - ob.Submit(order_2, nil) + ob.Submit(order_1, mockQueries) + ob.Submit(order_2, mockQueries) if order_1.Status != db.OrderStatusTypePARTIALLYFILLED { t.Errorf("Expected status to be partially filled.") } - ob.Submit(order_3, nil) + ob.Submit(order_3, mockQueries) if order_3.Status != db.OrderStatusTypePARTIALLYFILLED { t.Errorf("Expected status to be partially filled.") } - ob.Submit(order_4, nil) + ob.Submit(order_4, mockQueries) if order_3.Status != db.OrderStatusTypeFILLED { t.Errorf("Expected status to be fully filled.") @@ -224,9 +248,10 @@ func TestOrdersPartiallyMatched(t *testing.T) { func TestWrongOrderBook(t *testing.T) { ob := NewOrderBook("XRP") + mockQueries := &mockDBQueries{} order_1 := order.NewOrder(60, 100, db.OrderSideTypeBUY, db.OrderTypeLIMIT, "LINK", uuid.New()) - result := ob.Submit(order_1, nil) + result := ob.Submit(order_1, mockQueries) if result { t.Errorf("Order was submitted to the wrong orderbook.") }