package kanban import ( "context" "fmt" "sort" "tm/pkg/logger" ) // KanbanService interface defines kanban business logic operations type Service interface { // Board operations CreateBoard(ctx context.Context, req CreateBoardRequest) (*BoardResponse, error) GetBoardByID(ctx context.Context, id string) (*BoardDetailResponse, error) GetBoardsByUserID(ctx context.Context, userID string) (*BoardsListResponse, error) GetDefaultBoard(ctx context.Context, userID string) (*BoardDetailResponse, error) UpdateBoard(ctx context.Context, req UpdateBoardRequest) (*BoardResponse, error) DeleteBoard(ctx context.Context, id string) error // Column operations CreateColumn(ctx context.Context, req CreateColumnRequest) (*ColumnResponse, error) UpdateColumn(ctx context.Context, req UpdateColumnRequest) (*ColumnResponse, error) DeleteColumn(ctx context.Context, id string) error ReorderColumns(ctx context.Context, req ReorderColumnsRequest) error // Card operations CreateCard(ctx context.Context, req CreateCardRequest) (*CardResponse, error) GetCardByID(ctx context.Context, id string) (*CardResponse, error) UpdateCard(ctx context.Context, req UpdateCardRequest) (*CardResponse, error) DeleteCard(ctx context.Context, id string) error MoveCard(ctx context.Context, req MoveCardRequest) error ReorderCards(ctx context.Context, req ReorderCardsRequest) error } // kanbanService implements KanbanService interface type kanbanService struct { boardRepository BoardRepository columnRepository ColumnRepository cardRepository CardRepository logger logger.Logger } // NewService creates a new kanban service func NewService(boardRepo BoardRepository, columnRepo ColumnRepository, cardRepo CardRepository, logger logger.Logger) Service { return &kanbanService{ boardRepository: boardRepo, columnRepository: columnRepo, cardRepository: cardRepo, logger: logger, } } // CreateBoard creates a new kanban board func (s *kanbanService) CreateBoard(ctx context.Context, req CreateBoardRequest) (*BoardResponse, error) { s.logger.Info("Creating new board", map[string]interface{}{ "name": req.Name, "user_id": req.UserID, "is_default": req.IsDefault, }) // If this is set as default, unset any existing default board for this user if req.IsDefault { if err := s.unsetDefaultBoard(ctx, req.UserID); err != nil { s.logger.Warn("Failed to unset existing default board", map[string]interface{}{ "user_id": req.UserID, "error": err.Error(), }) } } board := &Board{ Name: req.Name, Description: req.Description, IsDefault: req.IsDefault, UserID: req.UserID, Columns: []Column{}, // Start with empty columns } if err := s.boardRepository.CreateBoard(ctx, board); err != nil { return nil, err } return board.ToResponse(), nil } // GetBoardByID retrieves a board with full column and card details func (s *kanbanService) GetBoardByID(ctx context.Context, id string) (*BoardDetailResponse, error) { s.logger.Info("Retrieving board by ID", map[string]interface{}{ "board_id": id, }) board, err := s.boardRepository.GetBoardByID(ctx, id) if err != nil { return nil, err } boardResponse := board.ToResponse() // Load columns with cards columns, err := s.columnRepository.GetColumnsByBoardID(ctx, id) if err != nil { return nil, err } for _, column := range columns { columnResponse := column.ToResponse() // Load cards for this column cards, err := s.cardRepository.GetCardsByColumnID(ctx, column.ID.Hex()) if err != nil { s.logger.Warn("Failed to load cards for column", map[string]interface{}{ "column_id": column.ID.Hex(), "error": err.Error(), }) continue } columnResponse.CardCount = len(cards) if column.Limit != nil && len(cards) > *column.Limit { columnResponse.IsOverLimit = true } for _, card := range cards { columnResponse.Cards = append(columnResponse.Cards, *card.ToResponse()) } boardResponse.Columns = append(boardResponse.Columns, *columnResponse) } return &BoardDetailResponse{ Board: *boardResponse, }, nil } // GetBoardsByUserID retrieves all boards for a user func (s *kanbanService) GetBoardsByUserID(ctx context.Context, userID string) (*BoardsListResponse, error) { s.logger.Info("Retrieving boards by user ID", map[string]interface{}{ "user_id": userID, }) boards, err := s.boardRepository.GetBoardsByUserID(ctx, userID) if err != nil { return nil, err } var boardResponses []BoardResponse for _, board := range boards { boardResponses = append(boardResponses, *board.ToResponse()) } return &BoardsListResponse{ Boards: boardResponses, }, nil } // GetDefaultBoard retrieves the default board for a user func (s *kanbanService) GetDefaultBoard(ctx context.Context, userID string) (*BoardDetailResponse, error) { s.logger.Info("Retrieving default board", map[string]interface{}{ "user_id": userID, }) board, err := s.boardRepository.GetDefaultBoard(ctx, userID) if err != nil { return nil, err } return s.GetBoardByID(ctx, board.ID.Hex()) } // UpdateBoard updates an existing board func (s *kanbanService) UpdateBoard(ctx context.Context, req UpdateBoardRequest) (*BoardResponse, error) { s.logger.Info("Updating board", map[string]interface{}{ "board_id": req.ID, }) board, err := s.boardRepository.GetBoardByID(ctx, req.ID) if err != nil { return nil, err } // Update fields if req.Name != nil { board.Name = *req.Name } if req.Description != nil { board.Description = *req.Description } if req.IsDefault != nil { // If setting as default, unset other default boards if *req.IsDefault && !board.IsDefault { if err := s.unsetDefaultBoard(ctx, board.UserID); err != nil { s.logger.Warn("Failed to unset existing default board", map[string]interface{}{ "user_id": board.UserID, "error": err.Error(), }) } } board.IsDefault = *req.IsDefault } if err := s.boardRepository.UpdateBoard(ctx, board); err != nil { return nil, err } return board.ToResponse(), nil } // DeleteBoard deletes a board and all its columns and cards func (s *kanbanService) DeleteBoard(ctx context.Context, id string) error { s.logger.Info("Deleting board", map[string]interface{}{ "board_id": id, }) // Get all columns for this board columns, err := s.columnRepository.GetColumnsByBoardID(ctx, id) if err != nil { return err } // Delete all cards in each column for _, column := range columns { cards, err := s.cardRepository.GetCardsByColumnID(ctx, column.ID.Hex()) if err != nil { s.logger.Warn("Failed to get cards for column during board deletion", map[string]interface{}{ "column_id": column.ID.Hex(), "error": err.Error(), }) continue } for _, card := range cards { if err := s.cardRepository.DeleteCard(ctx, card.ID.Hex()); err != nil { s.logger.Warn("Failed to delete card during board deletion", map[string]interface{}{ "card_id": card.ID.Hex(), "error": err.Error(), }) } } // Delete the column if err := s.columnRepository.DeleteColumn(ctx, column.ID.Hex()); err != nil { s.logger.Warn("Failed to delete column during board deletion", map[string]interface{}{ "column_id": column.ID.Hex(), "error": err.Error(), }) } } // Finally delete the board return s.boardRepository.DeleteBoard(ctx, id) } // CreateColumn creates a new column in a board func (s *kanbanService) CreateColumn(ctx context.Context, req CreateColumnRequest) (*ColumnResponse, error) { s.logger.Info("Creating new column", map[string]interface{}{ "name": req.Name, "board_id": req.BoardID, "order": req.Order, }) column := &Column{ BoardID: req.BoardID, Name: req.Name, Color: req.Color, Order: req.Order, Limit: req.Limit, } if err := s.columnRepository.CreateColumn(ctx, column); err != nil { return nil, err } return column.ToResponse(), nil } // UpdateColumn updates an existing column func (s *kanbanService) UpdateColumn(ctx context.Context, req UpdateColumnRequest) (*ColumnResponse, error) { s.logger.Info("Updating column", map[string]interface{}{ "column_id": req.ID, }) column, err := s.columnRepository.GetColumnByID(ctx, req.ID) if err != nil { return nil, err } // Update fields if req.Name != nil { column.Name = *req.Name } if req.Color != nil { column.Color = *req.Color } if req.Order != nil { column.Order = *req.Order } if req.Limit != nil { column.Limit = req.Limit } if err := s.columnRepository.UpdateColumn(ctx, column); err != nil { return nil, err } return column.ToResponse(), nil } // DeleteColumn deletes a column and moves all its cards to the next column func (s *kanbanService) DeleteColumn(ctx context.Context, id string) error { s.logger.Info("Deleting column", map[string]interface{}{ "column_id": id, }) column, err := s.columnRepository.GetColumnByID(ctx, id) if err != nil { return err } // Get all columns for this board to find the next one columns, err := s.columnRepository.GetColumnsByBoardID(ctx, column.BoardID) if err != nil { return err } // Find the next column (one with higher order) var nextColumn *Column sort.Slice(columns, func(i, j int) bool { return columns[i].Order < columns[j].Order }) for _, col := range columns { if col.Order > column.Order { nextColumn = &col break } } // Move cards to next column or delete them if no next column exists cards, err := s.cardRepository.GetCardsByColumnID(ctx, id) if err != nil { return err } if nextColumn != nil { // Move cards to next column for _, card := range cards { if err := s.cardRepository.MoveCard(ctx, card.ID.Hex(), nextColumn.ID.Hex(), card.Order); err != nil { s.logger.Warn("Failed to move card during column deletion", map[string]interface{}{ "card_id": card.ID.Hex(), "target_column_id": nextColumn.ID.Hex(), "error": err.Error(), }) } } } else { // Delete cards if no next column for _, card := range cards { if err := s.cardRepository.DeleteCard(ctx, card.ID.Hex()); err != nil { s.logger.Warn("Failed to delete card during column deletion", map[string]interface{}{ "card_id": card.ID.Hex(), "error": err.Error(), }) } } } // Delete the column return s.columnRepository.DeleteColumn(ctx, id) } // ReorderColumns reorders columns within a board func (s *kanbanService) ReorderColumns(ctx context.Context, req ReorderColumnsRequest) error { s.logger.Info("Reordering columns", map[string]interface{}{ "board_id": req.BoardID, "count": len(req.ColumnOrders), }) return s.columnRepository.UpdateColumnOrder(ctx, req.BoardID, req.ColumnOrders) } // CreateCard creates a new card in a column func (s *kanbanService) CreateCard(ctx context.Context, req CreateCardRequest) (*CardResponse, error) { s.logger.Info("Creating new card", map[string]interface{}{ "title": req.Title, "tender_id": req.TenderID, "column_id": req.ColumnID, "order": req.Order, }) // Validate that the column exists _, err := s.columnRepository.GetColumnByID(ctx, req.ColumnID) if err != nil { return nil, fmt.Errorf("column not found: %w", err) } // Check for duplicate card (same tender in same column) existingCards, err := s.cardRepository.GetCardsByTenderID(ctx, req.TenderID) if err != nil { return nil, err } for _, card := range existingCards { if card.ColumnID == req.ColumnID { return nil, fmt.Errorf("card for this tender already exists in the column") } } card := &Card{ ColumnID: req.ColumnID, TenderID: req.TenderID, Title: req.Title, Order: req.Order, Priority: req.Priority, Labels: req.Labels, DueDate: req.DueDate, } if err := s.cardRepository.CreateCard(ctx, card); err != nil { return nil, err } return card.ToResponse(), nil } // GetCardByID retrieves a card by ID func (s *kanbanService) GetCardByID(ctx context.Context, id string) (*CardResponse, error) { s.logger.Info("Retrieving card by ID", map[string]interface{}{ "card_id": id, }) card, err := s.cardRepository.GetCardByID(ctx, id) if err != nil { return nil, err } return card.ToResponse(), nil } // UpdateCard updates an existing card func (s *kanbanService) UpdateCard(ctx context.Context, req UpdateCardRequest) (*CardResponse, error) { s.logger.Info("Updating card", map[string]interface{}{ "card_id": req.ID, }) card, err := s.cardRepository.GetCardByID(ctx, req.ID) if err != nil { return nil, err } // Update fields if req.Title != nil { card.Title = *req.Title } if req.Priority != nil { card.Priority = *req.Priority } if req.Labels != nil { card.Labels = req.Labels } if req.DueDate != nil { card.DueDate = req.DueDate } if err := s.cardRepository.UpdateCard(ctx, card); err != nil { return nil, err } return card.ToResponse(), nil } // DeleteCard deletes a card func (s *kanbanService) DeleteCard(ctx context.Context, id string) error { s.logger.Info("Deleting card", map[string]interface{}{ "card_id": id, }) return s.cardRepository.DeleteCard(ctx, id) } // MoveCard moves a card to a different column func (s *kanbanService) MoveCard(ctx context.Context, req MoveCardRequest) error { s.logger.Info("Moving card", map[string]interface{}{ "card_id": req.CardID, "column_id": req.ColumnID, "new_order": req.NewOrder, }) // Validate that the target column exists _, err := s.columnRepository.GetColumnByID(ctx, req.ColumnID) if err != nil { return fmt.Errorf("target column not found: %w", err) } return s.cardRepository.MoveCard(ctx, req.CardID, req.ColumnID, req.NewOrder) } // ReorderCards reorders cards within a column func (s *kanbanService) ReorderCards(ctx context.Context, req ReorderCardsRequest) error { s.logger.Info("Reordering cards", map[string]interface{}{ "column_id": req.ColumnID, "count": len(req.CardOrders), }) return s.cardRepository.UpdateCardOrder(ctx, req.ColumnID, req.CardOrders) } // unsetDefaultBoard removes the default flag from all boards for a user func (s *kanbanService) unsetDefaultBoard(ctx context.Context, userID string) error { boards, err := s.boardRepository.GetBoardsByUserID(ctx, userID) if err != nil { return err } for _, board := range boards { if board.IsDefault { board.IsDefault = false if err := s.boardRepository.UpdateBoard(ctx, &board); err != nil { return err } } } return nil }