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/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 } func NewSelfOrderHandler( orderService service.OrderService, categoryService service.CategoryService, productService service.ProductService, tableRepo repository.TableRepositoryInterface, outletRepo processor.OutletRepository, userRepo processor.UserRepository, ) *SelfOrderHandler { return &SelfOrderHandler{ orderService: orderService, categoryService: categoryService, productService: productService, tableRepo: tableRepo, outletRepo: outletRepo, userRepo: userRepo, } } func (h *SelfOrderHandler) GetMenu(c *gin.Context) { ctx := c.Request.Context() var req contract.SelfOrderMenuRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> request binding failed") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::GetMenu") return } if req.TableID == uuid.Nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"), }), "SelfOrderHandler::GetMenu") return } if req.CustomerName == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "customer_name is required"), }), "SelfOrderHandler::GetMenu") return } table, err := h.tableRepo.GetByID(ctx, req.TableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"), }), "SelfOrderHandler::GetMenu") 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::GetMenu") return } outlet, err := h.outletRepo.GetByID(ctx, table.OutletID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> outlet not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.OrderServiceEntity, "outlet not found"), }), "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 } table, err := h.tableRepo.GetByID(ctx, req.TableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"), }), "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["customer_name"] = req.CustomerName if req.Phone != nil { metadata["customer_phone"] = *req.Phone } tableID := req.TableID modelReq := &models.CreateOrderRequest{ OutletID: table.OutletID, UserID: userID, TableID: &tableID, TableNumber: &table.TableName, OrderType: constants.OrderTypeDineIn, OrderItems: orderItems, CustomerName: &req.CustomerName, 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) validateCreateOrderRequest(req *contract.SelfOrderCreateOrderRequest) error { if req.TableID == uuid.Nil { return fmt.Errorf("table_id is required") } if req.CustomerName == "" { return fmt.Errorf("customer_name 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.TableID == "" { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"), }), "SelfOrderHandler::ListCategories") return } parsedTableID, err := uuid.Parse(req.TableID) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "table_id must be a valid UUID"), }), "SelfOrderHandler::ListCategories") return } table, err := h.tableRepo.GetByID(ctx, parsedTableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"), }), "SelfOrderHandler::ListCategories") return } 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::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) 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 }