package service import ( "context" "fmt" "time" "github.com/genex/trading-service/internal/domain/entity" "github.com/genex/trading-service/internal/domain/event" "github.com/genex/trading-service/internal/domain/repository" "github.com/genex/trading-service/internal/domain/vo" ) // TradeService is the application-level use-case service for trading operations. // It coordinates between the matching engine, repositories, and event publishing. type TradeService struct { orderRepo repository.OrderRepository tradeRepo repository.TradeRepository matchingService *MatchingService publisher event.EventPublisher } // NewTradeService creates a new TradeService with all dependencies injected. func NewTradeService( orderRepo repository.OrderRepository, tradeRepo repository.TradeRepository, matchingService *MatchingService, publisher event.EventPublisher, ) *TradeService { if publisher == nil { publisher = &event.NoopEventPublisher{} } return &TradeService{ orderRepo: orderRepo, tradeRepo: tradeRepo, matchingService: matchingService, publisher: publisher, } } // PlaceOrderInput carries the validated input for placing an order. type PlaceOrderInput struct { UserID string CouponID string Side vo.OrderSide Type vo.OrderType Price vo.Price Quantity vo.Quantity } // PlaceOrderOutput is the result of a place order use case. type PlaceOrderOutput struct { Order *entity.Order Trades []*entity.Trade } // PlaceOrder handles the full workflow of placing an order: // 1. Create the domain entity // 2. Persist the order // 3. Submit to matching engine // 4. Persist resulting trades // 5. Update order status func (s *TradeService) PlaceOrder(ctx context.Context, input PlaceOrderInput) (*PlaceOrderOutput, error) { // Generate order ID orderID := fmt.Sprintf("ord-%d", time.Now().UnixNano()) // Create domain entity with validation order, err := entity.NewOrder( orderID, input.UserID, input.CouponID, input.Side, input.Type, input.Price, input.Quantity, ) if err != nil { return nil, fmt.Errorf("invalid order: %w", err) } // Persist the new order if err := s.orderRepo.Save(ctx, order); err != nil { return nil, fmt.Errorf("failed to save order: %w", err) } // Submit to matching engine matchResult := s.matchingService.PlaceOrder(order) // Persist resulting trades for _, trade := range matchResult.Trades { if err := s.tradeRepo.Save(ctx, trade); err != nil { return nil, fmt.Errorf("failed to save trade %s: %w", trade.ID, err) } } // Update order status after matching if err := s.orderRepo.UpdateStatus(ctx, order); err != nil { return nil, fmt.Errorf("failed to update order status: %w", err) } return &PlaceOrderOutput{ Order: matchResult.UpdatedOrder, Trades: matchResult.Trades, }, nil } // CancelOrder cancels an active order by removing it from the order book. func (s *TradeService) CancelOrder(ctx context.Context, couponID, orderID string, side vo.OrderSide) error { success := s.matchingService.CancelOrder(couponID, orderID, side) if !success { return fmt.Errorf("order not found in order book: %s", orderID) } // Update order status in repository order, err := s.orderRepo.FindByID(ctx, orderID) if err == nil && order != nil { order.Cancel() _ = s.orderRepo.UpdateStatus(ctx, order) } return nil } // GetOrdersByUser retrieves all orders for a given user. func (s *TradeService) GetOrdersByUser(ctx context.Context, userID string) ([]*entity.Order, error) { return s.orderRepo.FindByUserID(ctx, userID) } // GetOrder retrieves a single order by ID. func (s *TradeService) GetOrder(ctx context.Context, orderID string) (*entity.Order, error) { return s.orderRepo.FindByID(ctx, orderID) } // GetTradesByOrder retrieves all trades associated with an order. func (s *TradeService) GetTradesByOrder(ctx context.Context, orderID string) ([]*entity.Trade, error) { return s.tradeRepo.FindByOrderID(ctx, orderID) } // GetRecentTrades retrieves the most recent trades. func (s *TradeService) GetRecentTrades(ctx context.Context, limit int) ([]*entity.Trade, error) { return s.tradeRepo.FindRecent(ctx, limit) } // GetOrderBookSnapshot delegates to the matching service for order book data. func (s *TradeService) GetOrderBookSnapshot(couponID string, depth int) (bids []entity.PriceLevel, asks []entity.PriceLevel) { return s.matchingService.GetOrderBookSnapshot(couponID, depth) } // GetOrdersByUserPaginated retrieves paginated orders for a user with optional status filter. func (s *TradeService) GetOrdersByUserPaginated(ctx context.Context, userID string, status string, page, limit int) ([]*entity.Order, int, error) { offset := (page - 1) * limit return s.orderRepo.FindByUserIDPaginated(ctx, userID, status, offset, limit) } // TransferCoupon validates ownership and transfers a coupon to a new owner by updating the order record. func (s *TradeService) TransferCoupon(ctx context.Context, couponID, ownerUserID, recipientID string) error { // Validate that the user actually owns this coupon by checking filled buy orders orders, err := s.orderRepo.FindByCouponID(ctx, couponID) if err != nil { return fmt.Errorf("failed to look up coupon orders: %w", err) } // Verify the user has a filled buy order for this coupon (ownership proof) ownsIt := false for _, o := range orders { if o.UserID == ownerUserID && o.Status == entity.OrderFilled && o.Side == vo.Buy { ownsIt = true break } } if !ownsIt { return fmt.Errorf("user does not own coupon %s", couponID) } // Create a transfer record as a special filled order pair transferID := fmt.Sprintf("xfr-%d", time.Now().UnixNano()) transferOrder, err := entity.NewOrder( transferID, recipientID, couponID, vo.Buy, vo.Market, vo.ZeroPrice(), vo.MustNewQuantity(1), ) if err != nil { return fmt.Errorf("failed to create transfer order: %w", err) } transferOrder.Status = entity.OrderFilled transferOrder.FilledQty = vo.MustNewQuantity(1) transferOrder.RemainingQty = vo.ZeroQuantity() return s.orderRepo.Save(ctx, transferOrder) } // GetAllOrderBooks returns all active order books (admin use). func (s *TradeService) GetAllOrderBooks() map[string]*entity.OrderBook { return s.matchingService.GetAllOrderBooks() } // GetTradeCount returns the total trade count. func (s *TradeService) GetTradeCount() int64 { return s.matchingService.GetTradeCount() }