Products like fish are sold per weighing (4.2 ons, 5.6 ons), which the order line could not represent: quantity is INTEGER and prices are always computed as quantity * unit_price. Model one weighing as one order line. quantity stays INTEGER and keeps meaning "how many items"; the measured amount goes into a new nullable order_items.weight, and the line is priced weight * unit_price. Two weighings of the same product are two lines, never merged into one. Keeping quantity integral avoids float comparisons in void, refund and split bill, where accumulated rounding error would silently misbehave — "1.4 + 1.4 + 1.4" is not 4.2 in float64, which would leave a fully paid split-bill item marked unpaid. BillableQuantity() is now the single place that decides between weight and count; every price and cost calculation goes through it. Missing one would bill a 4.2 ons fish as a single ons — wrong money, no error. Two database constraints back the design: a weighed line always carries a positive weight, and its quantity is pinned to 1. The latter also makes void all-or-nothing for weighed lines, so the row-splitting branch can never produce a zero-weight remainder row. Also wires product.unit_id through the API, which was previously not settable at all, and corrects the misleading comment on the request's unit_price field — that value has never been used; price always comes from the database. Design notes and the audit of every price multiplication site are in docs/rfc-weight-based-products.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
573 lines
22 KiB
Go
573 lines
22 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/mappers"
|
|
"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
|
|
productOutletPriceService service.ProductOutletPriceService
|
|
}
|
|
|
|
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,
|
|
productOutletPriceService service.ProductOutletPriceService,
|
|
) *SelfOrderHandler {
|
|
return &SelfOrderHandler{
|
|
orderService: orderService,
|
|
categoryService: categoryService,
|
|
productService: productService,
|
|
tableRepo: tableRepo,
|
|
outletRepo: outletRepo,
|
|
userRepo: userRepo,
|
|
sessionRepo: sessionRepo,
|
|
orderRepo: orderRepo,
|
|
productOutletPriceService: productOutletPriceService,
|
|
}
|
|
}
|
|
|
|
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(ctx, outlet, table, catList.Categories, prodList.Products)
|
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu")
|
|
}
|
|
|
|
func (h *SelfOrderHandler) buildMenuResponse(
|
|
ctx context.Context,
|
|
outlet *entities.Outlet,
|
|
table *entities.Table,
|
|
categories []contract.CategoryResponse,
|
|
products []contract.ProductResponse,
|
|
) *contract.SelfOrderMenuResponse {
|
|
outletPriceMap := make(map[uuid.UUID]float64)
|
|
if h.productOutletPriceService != nil {
|
|
priceResp := h.productOutletPriceService.GetByOutlet(ctx, outlet.ID)
|
|
if priceResp != nil && !priceResp.HasErrors() {
|
|
if priceList, ok := priceResp.Data.(*contract.ListProductOutletPricesResponse); ok {
|
|
for _, p := range priceList.Prices {
|
|
outletPriceMap[p.ProductID] = p.Price
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
price := p.Price
|
|
if outletPrice, exists := outletPriceMap[p.ID]; exists {
|
|
price = outletPrice
|
|
}
|
|
item := contract.SelfOrderMenuItem{
|
|
ID: p.ID,
|
|
Name: p.Name,
|
|
Description: p.Description,
|
|
Price: 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,
|
|
Weight: item.Weight,
|
|
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
|
|
}
|
|
|
|
modelOrders := mappers.OrderEntitiesToResponses(orders)
|
|
contractOrders := make([]contract.OrderResponse, len(modelOrders))
|
|
for i := range modelOrders {
|
|
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
|
|
}
|
|
|
|
resp := &contract.SelfOrderListOrdersResponse{
|
|
Orders: contractOrders,
|
|
}
|
|
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
|
|
}
|