475 lines
17 KiB
Go
475 lines
17 KiB
Go
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
|
|
}
|
|
|
|
func NewSelfOrderHandler(
|
|
orderService service.OrderService,
|
|
categoryService service.CategoryService,
|
|
productService service.ProductService,
|
|
tableRepo repository.TableRepositoryInterface,
|
|
outletRepo processor.OutletRepository,
|
|
userRepo processor.UserRepository,
|
|
sessionRepo repository.SessionRepository,
|
|
) *SelfOrderHandler {
|
|
return &SelfOrderHandler{
|
|
orderService: orderService,
|
|
categoryService: categoryService,
|
|
productService: productService,
|
|
tableRepo: tableRepo,
|
|
outletRepo: outletRepo,
|
|
userRepo: userRepo,
|
|
sessionRepo: sessionRepo,
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
tableID := table.ID
|
|
modelReq := &models.CreateOrderRequest{
|
|
OutletID: table.OutletID,
|
|
UserID: userID,
|
|
TableID: &tableID,
|
|
TableNumber: &table.TableName,
|
|
OrderType: constants.OrderTypeDineIn,
|
|
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) 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.SessionID == "" {
|
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "session_id is required"),
|
|
}), "SelfOrderHandler::ListCategories")
|
|
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::ListCategories")
|
|
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::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) 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
|
|
}
|