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 selfOrderJWTSecret string selfOrderJWTTTL int } func NewSelfOrderHandler( orderService service.OrderService, categoryService service.CategoryService, productService service.ProductService, tableRepo repository.TableRepositoryInterface, outletRepo processor.OutletRepository, userRepo processor.UserRepository, selfOrderJWTSecret string, selfOrderJWTTTL int, ) *SelfOrderHandler { return &SelfOrderHandler{ orderService: orderService, categoryService: categoryService, productService: productService, tableRepo: tableRepo, outletRepo: outletRepo, userRepo: userRepo, selfOrderJWTSecret: selfOrderJWTSecret, selfOrderJWTTTL: selfOrderJWTTTL, } } func (h *SelfOrderHandler) getSelfOrderContext(c *gin.Context) (uuid.UUID, string, string, error) { tableIDStr, _ := c.Get("self_order_table_id") customerName, _ := c.Get("self_order_customer_name") phoneStr, _ := c.Get("self_order_phone") tableIDStrTyped, ok := tableIDStr.(string) if !ok || tableIDStrTyped == "" { return uuid.Nil, "", "", fmt.Errorf("table_id not found in context") } tableID, err := uuid.Parse(tableIDStrTyped) if err != nil { return uuid.Nil, "", "", fmt.Errorf("invalid table_id in token") } nameTyped, ok := customerName.(string) if !ok || nameTyped == "" { return uuid.Nil, "", "", fmt.Errorf("customer_name not found in context") } phone, _ := phoneStr.(string) return tableID, nameTyped, phone, nil } func (h *SelfOrderHandler) CreateSession(c *gin.Context) { ctx := c.Request.Context() var req contract.SelfOrderSessionRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> request binding failed") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::CreateSession") 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::CreateSession") 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::CreateSession") return } table, err := h.tableRepo.GetByID(ctx, req.TableID) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> table not found") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"), }), "SelfOrderHandler::CreateSession") 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::CreateSession") return } phone := "" if req.Phone != nil { phone = *req.Phone } token, expiresAt, err := util.GenerateSelfOrderSessionToken(req.TableID, req.CustomerName, phone, h.selfOrderJWTSecret, h.selfOrderJWTTTL) if err != nil { logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> failed to generate token") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create session"), }), "SelfOrderHandler::CreateSession") return } util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(&contract.SelfOrderSessionResponse{ Token: token, ExpiresAt: expiresAt, TableID: req.TableID, }), "SelfOrderHandler::CreateSession") } func (h *SelfOrderHandler) GetMenu(c *gin.Context) { ctx := c.Request.Context() tableID, customerName, _, err := h.getSelfOrderContext(c) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::GetMenu") return } table, err := h.tableRepo.GetByID(ctx, 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 } _ = customerName 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() tableID, customerName, phone, err := h.getSelfOrderContext(c) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::CreateOrder") return } 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, 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, }) } customerPhone := phone if req.Phone != nil { customerPhone = *req.Phone } metadata := make(map[string]interface{}) metadata["self_order"] = true metadata["customer_name"] = customerName if customerPhone != "" { metadata["customer_phone"] = customerPhone } tableIDPtr := tableID modelReq := &models.CreateOrderRequest{ OutletID: table.OutletID, UserID: userID, TableID: &tableIDPtr, TableNumber: &table.TableName, OrderType: constants.OrderTypeDineIn, OrderItems: orderItems, CustomerName: &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 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() tableID, _, _, err := h.getSelfOrderContext(c) if err != nil { util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{ contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()), }), "SelfOrderHandler::ListCategories") return } table, err := h.tableRepo.GetByID(ctx, tableID) 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 }