package handler import ( "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/logger" "apskel-pos-be/internal/models" "apskel-pos-be/internal/pkg/tabletoken" "apskel-pos-be/internal/processor" "apskel-pos-be/internal/repository" "apskel-pos-be/internal/service" "apskel-pos-be/internal/transformer" "apskel-pos-be/internal/util" "context" "fmt" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type SelfOrderHandler struct { orderService service.OrderService categoryService service.CategoryService productService service.ProductService tableRepo repository.TableRepositoryInterface outletRepo processor.OutletRepository userRepo processor.UserRepository sessionRepo repository.SessionRepository orderRepo repository.OrderRepository } func NewSelfOrderHandler( orderService service.OrderService, categoryService service.CategoryService, productService service.ProductService, tableRepo repository.TableRepositoryInterface, outletRepo processor.OutletRepository, userRepo processor.UserRepository, sessionRepo repository.SessionRepository, orderRepo repository.OrderRepository, ) *SelfOrderHandler { return &SelfOrderHandler{ orderService: orderService, categoryService: categoryService, productService: productService, tableRepo: tableRepo, outletRepo: outletRepo, userRepo: userRepo, sessionRepo: sessionRepo, orderRepo: orderRepo, } } func (h *SelfOrderHandler) ValidateToken(c *gin.Context) { ctx := c.Request.Context() token := c.Param("token") if token == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "token is required"), }), "SelfOrderHandler::ValidateToken") return } tableID, orgID, outletID, err := tabletoken.Decode(token) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> invalid token") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid table token"), }), "SelfOrderHandler::ValidateToken") return } table, err := h.tableRepo.GetByID(ctx, tableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"), }), "SelfOrderHandler::ValidateToken") return } if !table.IsActive { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not active"), }), "SelfOrderHandler::ValidateToken") return } if table.OrganizationID != orgID || table.OutletID != outletID { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "token does not match table"), }), "SelfOrderHandler::ValidateToken") return } outlet, err := h.outletRepo.GetByID(ctx, table.OutletID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> outlet not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.OrderServiceEntity, "outlet not found"), }), "SelfOrderHandler::ValidateToken") return } existingSession, err := h.sessionRepo.GetActiveByTableID(ctx, table.ID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> failed to check session") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to check session"), }), "SelfOrderHandler::ValidateToken") return } var sessionStatus string var sessionID string if existingSession != nil { sessionStatus = "joined_session" sessionID = existingSession.ID } else { session := &models.SelfOrderSession{ TableID: table.ID, OrganizationID: table.OrganizationID, OutletID: table.OutletID, } if err := h.sessionRepo.Create(ctx, session); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ValidateToken -> failed to create session") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create session"), }), "SelfOrderHandler::ValidateToken") return } sessionStatus = "new_session" sessionID = session.ID } resp := &contract.SelfOrderTableTokenResponse{ SessionID: sessionID, TableID: table.ID.String(), OrganizationID: table.OrganizationID.String(), OutletID: table.OutletID.String(), TableName: table.TableName, OutletName: outlet.Name, Status: sessionStatus, } util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::ValidateToken") } func (h *SelfOrderHandler) GetMenu(c *gin.Context) { ctx := c.Request.Context() var req contract.SelfOrderMenuRequest if err := c.ShouldBindQuery(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> query binding failed") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::GetMenu") return } session, table, outlet, err := h.resolveSession(ctx, req.SessionID) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::GetMenu") return } if session == nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found or expired"), }), "SelfOrderHandler::GetMenu") return } isActive := true catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{ OrganizationID: &table.OrganizationID, Page: 1, Limit: 100, }) if catResp.HasErrors() { logger.FromContext(ctx).WithError(catResp.GetErrors()[0]).Error("SelfOrderHandler::GetMenu -> failed to list categories") util.HandleResponse(c.Writer, c.Request, catResp, "SelfOrderHandler::GetMenu") return } prodResp := h.productService.ListProducts(ctx, &contract.ListProductsRequest{ OrganizationID: &table.OrganizationID, IsActive: &isActive, Page: 1, Limit: 1000, }) if prodResp.HasErrors() { logger.FromContext(ctx).WithError(prodResp.GetErrors()[0]).Error("SelfOrderHandler::GetMenu -> failed to list products") util.HandleResponse(c.Writer, c.Request, prodResp, "SelfOrderHandler::GetMenu") return } catList, ok := catResp.Data.(*contract.ListCategoriesResponse) if !ok { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, "unexpected categories response type"), }), "SelfOrderHandler::GetMenu") return } prodList, ok := prodResp.Data.(*contract.ListProductsResponse) if !ok { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, "unexpected products response type"), }), "SelfOrderHandler::GetMenu") return } menu := h.buildMenuResponse(outlet, table, catList.Categories, prodList.Products) util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu") } func (h *SelfOrderHandler) buildMenuResponse( outlet *entities.Outlet, table *entities.Table, categories []contract.CategoryResponse, products []contract.ProductResponse, ) *contract.SelfOrderMenuResponse { productMap := make(map[uuid.UUID][]contract.ProductResponse) for _, p := range products { productMap[p.CategoryID] = append(productMap[p.CategoryID], p) } menuCategories := make([]contract.SelfOrderMenuCategory, 0, len(categories)) for _, cat := range categories { menuItems := make([]contract.SelfOrderMenuItem, 0) if prods, ok := productMap[cat.ID]; ok { for _, p := range prods { item := contract.SelfOrderMenuItem{ ID: p.ID, Name: p.Name, Description: p.Description, Price: p.Price, ImageURL: p.ImageURL, } for _, v := range p.Variants { item.Variants = append(item.Variants, contract.SelfOrderMenuVariant{ ID: v.ID, Name: v.Name, PriceModifier: v.PriceModifier, }) } menuItems = append(menuItems, item) } } menuCategories = append(menuCategories, contract.SelfOrderMenuCategory{ ID: cat.ID, Name: cat.Name, Description: cat.Description, Order: cat.Order, Products: menuItems, }) } return &contract.SelfOrderMenuResponse{ OutletName: outlet.Name, TableName: table.TableName, Categories: menuCategories, } } func (h *SelfOrderHandler) CreateOrder(c *gin.Context) { ctx := c.Request.Context() var req contract.SelfOrderCreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> request binding failed") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::CreateOrder") return } if err := h.validateCreateOrderRequest(&req); err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::CreateOrder") return } session, table, _, err := h.resolveSession(ctx, req.SessionID) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::CreateOrder") return } if session == nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found or expired"), }), "SelfOrderHandler::CreateOrder") return } if !table.IsActive || !table.IsAvailable() { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not available for ordering"), }), "SelfOrderHandler::CreateOrder") return } userID, err := h.resolveOrgUser(ctx, table.OrganizationID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to resolve org user") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create self-order"), }), "SelfOrderHandler::CreateOrder") return } orderItems := make([]models.CreateOrderItemRequest, 0, len(req.OrderItems)) for _, item := range req.OrderItems { orderItems = append(orderItems, models.CreateOrderItemRequest{ ProductID: item.ProductID, ProductVariantID: item.ProductVariantID, Quantity: item.Quantity, Notes: item.Notes, }) } metadata := make(map[string]interface{}) metadata["self_order"] = true metadata["session_id"] = session.ID metadata["customer_name"] = req.CustomerName tableID := table.ID modelReq := &models.CreateOrderRequest{ OutletID: table.OutletID, UserID: userID, TableID: &tableID, TableNumber: &table.TableName, OrderType: constants.OrderType(req.OrderType), OrderItems: orderItems, Metadata: metadata, } response, err := h.orderService.CreateOrder(ctx, modelReq, table.OrganizationID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to create order") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, err.Error()), }), "SelfOrderHandler::CreateOrder") return } contractResp := transformer.OrderModelToContract(response) util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder") } func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) { ctx := c.Request.Context() sessionID := c.Param("session_id") if sessionID == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "session_id is required"), }), "SelfOrderHandler::GetOrdersBySession") return } session, err := h.sessionRepo.GetByID(ctx, sessionID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetOrdersBySession -> failed to get session") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found"), }), "SelfOrderHandler::GetOrdersBySession") return } if session == nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "session not found"), }), "SelfOrderHandler::GetOrdersBySession") return } orders, err := h.orderRepo.ListBySessionID(ctx, sessionID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetOrdersBySession -> failed to list orders") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to list orders"), }), "SelfOrderHandler::GetOrdersBySession") return } resp := &contract.SelfOrderListOrdersResponse{ Orders: make([]contract.SelfOrderOrderItem, 0, len(orders)), } for _, o := range orders { item := contract.SelfOrderOrderItem{ ID: o.ID, OrderNumber: o.OrderNumber, TableNumber: o.TableNumber, OrderType: string(o.OrderType), Status: string(o.Status), Subtotal: o.Subtotal, TaxAmount: o.TaxAmount, DiscountAmount: o.DiscountAmount, TotalAmount: o.TotalAmount, RemainingAmount: o.RemainingAmount, PaymentStatus: string(o.PaymentStatus), IsVoid: o.IsVoid, Metadata: o.Metadata, IsRefund: o.IsRefund, CreatedAt: o.CreatedAt, } for _, oi := range o.OrderItems { lineItem := contract.SelfOrderOrderLineItem{ ProductID: oi.ProductID, Quantity: oi.Quantity, UnitPrice: oi.UnitPrice, TotalPrice: oi.TotalPrice, Notes: oi.Notes, Status: string(oi.Status), ProductVariantID: oi.ProductVariantID, } if oi.Product.ID != uuid.Nil { lineItem.ProductName = oi.Product.Name } if oi.ProductVariant != nil { lineItem.ProductVariantNam = &oi.ProductVariant.Name } item.Items = append(item.Items, lineItem) } resp.Orders = append(resp.Orders, item) } util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession") } func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCreateOrderRequest) error { if req.SessionID == "" { return fmt.Errorf("session_id is required") } if len(req.OrderItems) == 0 { return fmt.Errorf("at least one order item is required") } for i, item := range req.OrderItems { if item.ProductID == uuid.Nil { return fmt.Errorf("product_id is required for item %d", i+1) } if item.Quantity <= 0 { return fmt.Errorf("quantity must be greater than zero for item %d", i+1) } } return nil } func (h *SelfOrderHandler) ListCategories(c *gin.Context) { ctx := c.Request.Context() var req contract.SelfOrderListCategoriesRequest if err := c.ShouldBindQuery(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> query binding failed") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::ListCategories") return } if req.OrganizationID == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organization_id is required"), }), "SelfOrderHandler::ListCategories") return } if req.OutletID == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "outlet_id is required"), }), "SelfOrderHandler::ListCategories") return } orgID, err := uuid.Parse(req.OrganizationID) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organization_id format"), }), "SelfOrderHandler::ListCategories") return } outletID, err := uuid.Parse(req.OutletID) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid outlet_id format"), }), "SelfOrderHandler::ListCategories") return } outlet, err := h.outletRepo.GetByID(ctx, outletID) if err != nil || outlet == nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "outlet not found"), }), "SelfOrderHandler::ListCategories") return } if outlet.OrganizationID != orgID { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "outlet does not belong to the specified organization"), }), "SelfOrderHandler::ListCategories") return } catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{ OrganizationID: &orgID, Page: 1, Limit: 100, }) if catResp.HasErrors() { logger.FromContext(ctx).WithError(catResp.GetErrors()[0]).Error("SelfOrderHandler::ListCategories -> failed to list categories") util.HandleResponse(c.Writer, c.Request, catResp, "SelfOrderHandler::ListCategories") return } catList, ok := catResp.Data.(*contract.ListCategoriesResponse) if !ok { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, "unexpected categories response type"), }), "SelfOrderHandler::ListCategories") return } items := make([]contract.SelfOrderCategoryItem, 0, len(catList.Categories)) for _, cat := range catList.Categories { items = append(items, contract.SelfOrderCategoryItem{ ID: cat.ID, Name: cat.Name, Description: cat.Description, Order: cat.Order, }) } util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(&contract.SelfOrderListCategoriesResponse{ Categories: items, }), "SelfOrderHandler::ListCategories") } func (h *SelfOrderHandler) resolveSession(ctx context.Context, sessionID string) (*models.SelfOrderSession, *entities.Table, *entities.Outlet, error) { session, err := h.sessionRepo.GetByID(ctx, sessionID) if err != nil { return nil, nil, nil, fmt.Errorf("failed to get session: %w", err) } if session == nil { return nil, nil, nil, nil } if session.Status != "active" { return nil, nil, nil, fmt.Errorf("session is no longer active") } table, err := h.tableRepo.GetByID(ctx, session.TableID) if err != nil { return nil, nil, nil, fmt.Errorf("table not found for session") } outlet, err := h.outletRepo.GetByID(ctx, table.OutletID) if err != nil { return nil, nil, nil, fmt.Errorf("outlet not found for session") } return session, table, outlet, nil } func (h *SelfOrderHandler) resolveOrgUser(ctx context.Context, organizationID uuid.UUID) (uuid.UUID, error) { users, err := h.userRepo.GetByOrganizationID(ctx, organizationID) if err != nil { return uuid.Nil, fmt.Errorf("failed to get users for organization: %w", err) } if len(users) == 0 { return uuid.Nil, fmt.Errorf("no users found for organization") } return users[0].ID, nil }