Self Order

This commit is contained in:
ryan 2026-05-04 16:23:29 +07:00
parent 2c76962959
commit b993da898f
16 changed files with 443 additions and 3 deletions

View File

@ -42,7 +42,7 @@ func (a *App) Initialize(cfg *config.Config) error {
processors := a.initProcessors(cfg, repos)
services := a.initServices(processors, repos, cfg)
validators := a.initValidators()
middleware := a.initMiddleware(services, cfg)
middleware := a.initMiddleware(services, cfg, repos)
healthHandler := handler.NewHealthHandler()
a.router = router.NewRouter(
@ -105,6 +105,14 @@ func (a *App) Initialize(cfg *config.Config) error {
services.customerPointsService,
services.spinGameService,
middleware.customerAuthMiddleware,
handler.NewSelfOrderHandler(
services.orderService,
services.productService,
repos.customerRepo,
repos.userRepo,
repos.outletRepo,
),
middleware.selfOrderMiddleware,
)
return nil
@ -441,12 +449,14 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
type middlewares struct {
authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware
selfOrderMiddleware *middleware.SelfOrderMiddleware
}
func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares {
func (a *App) initMiddleware(services *services, cfg *config.Config, repos *repositories) *middlewares {
return &middlewares{
authMiddleware: middleware.NewAuthMiddleware(services.authService),
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
selfOrderMiddleware: middleware.NewSelfOrderMiddleware(repos.tableRepo),
}
}

View File

@ -56,6 +56,7 @@ const (
CampaignRuleEntity = "campaign_rule"
CustomerEntity = "customer"
SpinGameHandlerEntity = "spin_game_handler"
SelfOrderEntity = "self_order"
)
var HttpErrorMap = map[string]int{

View File

@ -60,6 +60,13 @@ func GetAllOrderItemStatuses() []OrderItemStatus {
}
}
type OrderSource string
const (
OrderSourceStaff OrderSource = "staff"
OrderSourceSelfOrder OrderSource = "self_order"
)
func (o OrderType) IsValidOrderType() bool {
for _, validType := range GetAllOrderTypes() {
if o == validType {

View File

@ -0,0 +1,51 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateSelfOrderRequest struct {
CustomerName string `json:"customer_name" validate:"required,min=1,max=255"`
PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty"`
OrderItems []SelfOrderItemRequest `json:"order_items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"`
}
type SelfOrderItemRequest struct {
ProductID uuid.UUID `json:"product_id" validate:"required"`
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
Quantity int `json:"quantity" validate:"required,min=1"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type SelfOrderResponse struct {
OrderID uuid.UUID `json:"order_id"`
OrderNumber string `json:"order_number"`
TableID uuid.UUID `json:"table_id"`
TableName string `json:"table_name"`
OutletID uuid.UUID `json:"outlet_id"`
OutletName string `json:"outlet_name"`
CustomerName string `json:"customer_name"`
OrderItems []OrderItemResponse `json:"order_items"`
Subtotal float64 `json:"subtotal"`
TaxAmount float64 `json:"tax_amount"`
TotalAmount float64 `json:"total_amount"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
type SelfOrderMenuResponse struct {
OutletID uuid.UUID `json:"outlet_id"`
OutletName string `json:"outlet_name"`
TableID uuid.UUID `json:"table_id"`
TableName string `json:"table_name"`
Organization OrganizationMenuInfo `json:"organization"`
Products ListProductsResponse `json:"products"`
}
type OrganizationMenuInfo struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
}

View File

@ -53,6 +53,7 @@ type Order struct {
RemainingAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"remaining_amount"`
PaymentStatus PaymentStatus `gorm:"default:'pending';size:50" json:"payment_status"`
RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"`
Source string `gorm:"default:'staff';size:50" json:"source"`
IsVoid bool `gorm:"default:false" json:"is_void"`
IsRefund bool `gorm:"default:false" json:"is_refund"`
VoidReason *string `gorm:"size:255" json:"void_reason,omitempty"`

View File

@ -19,6 +19,7 @@ type Table struct {
PositionX float64 `gorm:"type:decimal(10,2);default:0.00" json:"position_x"`
PositionY float64 `gorm:"type:decimal(10,2);default:0.00" json:"position_y"`
Capacity int `gorm:"default:4" json:"capacity"`
Token string `gorm:"uniqueIndex;size:100" json:"token"`
IsActive bool `gorm:"default:true" json:"is_active"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`

View File

@ -0,0 +1,239 @@
package handler
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"apskel-pos-be/internal/service"
"apskel-pos-be/internal/util"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type SelfOrderHandler struct {
orderService service.OrderService
productService service.ProductService
customerRepo *repository.CustomerRepository
userRepo *repository.UserRepositoryImpl
outletRepo *repository.OutletRepositoryImpl
}
func NewSelfOrderHandler(
orderService service.OrderService,
productService service.ProductService,
customerRepo *repository.CustomerRepository,
userRepo *repository.UserRepositoryImpl,
outletRepo *repository.OutletRepositoryImpl,
) *SelfOrderHandler {
return &SelfOrderHandler{
orderService: orderService,
productService: productService,
customerRepo: customerRepo,
userRepo: userRepo,
outletRepo: outletRepo,
}
}
func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
ctx := c.Request.Context()
organizationIDStr, _ := c.Get("self_order_organization_id")
outletIDStr, _ := c.Get("self_order_outlet_id")
tableIDStr, _ := c.Get("self_order_table_id")
tableName, _ := c.Get("self_order_table_name")
organizationID, err := uuid.Parse(organizationIDStr.(string))
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.SelfOrderEntity, "invalid organization ID"),
}), "SelfOrderHandler::GetMenu")
return
}
outletID, err := uuid.Parse(outletIDStr.(string))
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.SelfOrderEntity, "invalid outlet ID"),
}), "SelfOrderHandler::GetMenu")
return
}
tableID, _ := uuid.Parse(tableIDStr.(string))
isActive := true
req := &contract.ListProductsRequest{
OrganizationID: &organizationID,
IsActive: &isActive,
Page: 1,
Limit: 100,
}
productsResponse := h.productService.ListProducts(ctx, req)
outlet, outletErr := h.outletRepo.GetByID(ctx, outletID)
outletName := ""
if outletErr == nil && outlet != nil {
outletName = outlet.Name
}
menuResponse := &contract.SelfOrderMenuResponse{
OutletID: outletID,
OutletName: outletName,
TableID: tableID,
TableName: tableName.(string),
Organization: contract.OrganizationMenuInfo{
ID: organizationID,
},
}
if productsResponse != nil {
if data, ok := productsResponse.Data.(*contract.ListProductsResponse); ok {
menuResponse.Products = *data
}
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menuResponse), "SelfOrderHandler::GetMenu")
}
func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
ctx := c.Request.Context()
organizationIDStr, _ := c.Get("self_order_organization_id")
outletIDStr, _ := c.Get("self_order_outlet_id")
tableIDStr, _ := c.Get("self_order_table_id")
tableName, _ := c.Get("self_order_table_name")
organizationID, _ := uuid.Parse(organizationIDStr.(string))
outletID, _ := uuid.Parse(outletIDStr.(string))
tableID, _ := uuid.Parse(tableIDStr.(string))
var req contract.CreateSelfOrderRequest
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 req.CustomerName == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.SelfOrderEntity, "customer_name is required"),
}), "SelfOrderHandler::CreateOrder")
return
}
if len(req.OrderItems) == 0 {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.SelfOrderEntity, "at least one order item is required"),
}), "SelfOrderHandler::CreateOrder")
return
}
adminUser, err := h.userRepo.GetAdminByOrganizationID(ctx, organizationID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to get admin user")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.InternalServerErrorCode, constants.SelfOrderEntity, "failed to resolve system user"),
}), "SelfOrderHandler::CreateOrder")
return
}
var customerID *uuid.UUID
if req.PhoneNumber != nil && *req.PhoneNumber != "" {
customer, err := h.customerRepo.GetByPhoneNumber(ctx, *req.PhoneNumber)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to lookup customer by phone")
}
if customer != nil {
customerID = &customer.ID
}
}
metadata := map[string]interface{}{
"source": string(constants.OrderSourceSelfOrder),
"customer_phone": "",
}
if req.PhoneNumber != nil {
metadata["customer_phone"] = *req.PhoneNumber
}
orderItems := make([]models.CreateOrderItemRequest, len(req.OrderItems))
for i, item := range req.OrderItems {
orderItems[i] = models.CreateOrderItemRequest{
ProductID: item.ProductID,
ProductVariantID: item.ProductVariantID,
Quantity: item.Quantity,
Notes: item.Notes,
}
}
tableNameStr := tableName.(string)
modelReq := &models.CreateOrderRequest{
OutletID: outletID,
UserID: adminUser.ID,
CustomerID: customerID,
TableID: &tableID,
TableNumber: &tableNameStr,
OrderType: constants.OrderTypeDineIn,
OrderItems: orderItems,
Notes: req.Notes,
CustomerName: &req.CustomerName,
Metadata: metadata,
}
response, err := h.orderService.CreateOrder(ctx, modelReq, 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.SelfOrderEntity, err.Error()),
}), "SelfOrderHandler::CreateOrder")
return
}
outlet, _ := h.outletRepo.GetByID(ctx, outletID)
outletName := ""
if outlet != nil {
outletName = outlet.Name
}
orderItemsResp := make([]contract.OrderItemResponse, len(response.OrderItems))
for i, item := range response.OrderItems {
orderItemsResp[i] = contract.OrderItemResponse{
ID: item.ID,
OrderID: item.OrderID,
ProductID: item.ProductID,
ProductName: item.ProductName,
ProductVariantID: item.ProductVariantID,
ProductVariantName: item.ProductVariantName,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
TotalPrice: item.TotalPrice,
Notes: item.Notes,
Status: string(item.Status),
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}
selfOrderResp := &contract.SelfOrderResponse{
OrderID: response.ID,
OrderNumber: response.OrderNumber,
TableID: tableID,
TableName: tableNameStr,
OutletID: outletID,
OutletName: outletName,
CustomerName: req.CustomerName,
OrderItems: orderItemsResp,
Subtotal: response.Subtotal,
TaxAmount: response.TaxAmount,
TotalAmount: response.TotalAmount,
Status: string(response.Status),
CreatedAt: response.CreatedAt,
}
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(selfOrderResp), "SelfOrderHandler::CreateOrder")
}

View File

@ -0,0 +1,53 @@
package middleware
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/repository"
"apskel-pos-be/internal/util"
"github.com/gin-gonic/gin"
)
type SelfOrderMiddleware struct {
tableRepo repository.TableRepositoryInterface
}
func NewSelfOrderMiddleware(tableRepo repository.TableRepositoryInterface) *SelfOrderMiddleware {
return &SelfOrderMiddleware{
tableRepo: tableRepo,
}
}
func (m *SelfOrderMiddleware) ResolveToken() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Query("token")
if token == "" {
token = c.GetHeader("X-Table-Token")
}
if token == "" {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.ValidationErrorCode, constants.SelfOrderEntity, "token is required"),
}), "SelfOrderMiddleware::ResolveToken")
c.Abort()
return
}
table, err := m.tableRepo.GetByToken(c.Request.Context(), token)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
contract.NewResponseError(constants.NotFoundErrorCode, constants.SelfOrderEntity, "invalid or expired table token"),
}), "SelfOrderMiddleware::ResolveToken")
c.Abort()
return
}
c.Set("self_order_table_id", table.ID.String())
c.Set("self_order_table_name", table.TableName)
c.Set("self_order_outlet_id", table.OutletID.String())
c.Set("self_order_organization_id", table.OrganizationID.String())
c.Next()
}
}

View File

@ -138,3 +138,15 @@ func (r *CustomerRepository) GetByEmail(ctx context.Context, email string, organ
}
return &customer, nil
}
func (r *CustomerRepository) GetByPhoneNumber(ctx context.Context, phoneNumber string) (*entities.Customer, error) {
var customer entities.Customer
err := r.db.WithContext(ctx).Where("phone_number = ?", phoneNumber).First(&customer).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &customer, nil
}

View File

@ -170,3 +170,16 @@ func (r *TableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUID) (
}
return &table, nil
}
func (r *TableRepository) GetByToken(ctx context.Context, token string) (*entities.Table, error) {
var table entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Where("token = ? AND is_active = ?", token, true).
First(&table).Error
if err != nil {
return nil, err
}
return &table, nil
}

View File

@ -23,4 +23,5 @@ type TableRepositoryInterface interface {
OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error
ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error
GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error)
GetByToken(ctx context.Context, token string) (*entities.Table, error)
}

View File

@ -99,6 +99,17 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
return users, total, err
}
func (r *UserRepositoryImpl) GetAdminByOrganizationID(ctx context.Context, organizationID uuid.UUID) (*entities.User, error) {
var user entities.User
err := r.db.WithContext(ctx).
Where("organization_id = ? AND role = ? AND is_active = ?", organizationID, entities.RoleAdmin, true).
First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.User{})

View File

@ -46,11 +46,13 @@ type Router struct {
customerAuthHandler *handler.CustomerAuthHandler
customerPointsHandler *handler.CustomerPointsHandler
spinGameHandler *handler.SpinGameHandler
selfOrderHandler *handler.SelfOrderHandler
authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware
selfOrderMiddleware *middleware.SelfOrderMiddleware
}
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router {
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, selfOrderHandler *handler.SelfOrderHandler, selfOrderMiddleware *middleware.SelfOrderMiddleware) *Router {
return &Router{
config: cfg,
@ -88,6 +90,8 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
authMiddleware: authMiddleware,
customerAuthMiddleware: customerAuthMiddleware,
selfOrderHandler: selfOrderHandler,
selfOrderMiddleware: selfOrderMiddleware,
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
}
}
@ -145,6 +149,14 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
customer.POST("/spin", r.spinGameHandler.PlaySpinGame)
}
// Self-order routes (public, token-based table identification)
selfOrder := v1.Group("/self-order")
selfOrder.Use(r.selfOrderMiddleware.ResolveToken())
{
selfOrder.GET("/menu", r.selfOrderHandler.GetMenu)
selfOrder.POST("/order", r.selfOrderHandler.CreateOrder)
}
organizations := v1.Group("/organizations")
{
organizations.POST("", r.organizationHandler.CreateOrganization)

View File

@ -182,6 +182,14 @@ func (m *MockTableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUI
return args.Get(0).(*entities.Table), args.Error(1)
}
func (m *MockTableRepository) GetByToken(ctx context.Context, token string) (*entities.Table, error) {
args := m.Called(ctx, token)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entities.Table), args.Error(1)
}
func TestCreateOrderWithTableOccupation(t *testing.T) {
// Setup
ctx := context.Background()

View File

@ -0,0 +1,7 @@
-- Remove source column from orders
ALTER TABLE orders DROP COLUMN IF EXISTS source;
-- Remove token column from tables
DROP INDEX IF EXISTS idx_tables_token_active;
DROP INDEX IF EXISTS idx_tables_token;
ALTER TABLE tables DROP COLUMN IF EXISTS token;

View File

@ -0,0 +1,13 @@
-- Add token column to tables for self-order QR code identification
ALTER TABLE tables ADD COLUMN IF NOT EXISTS token VARCHAR(100);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tables_token ON tables(token);
CREATE INDEX IF NOT EXISTS idx_tables_token_active ON tables(token) WHERE is_active = true;
-- Backfill existing tables with unique tokens
-- Uses gen_random_uuid() to generate unique tokens for each existing table
UPDATE tables SET token = gen_random_uuid()::text WHERE token IS NULL;
-- Make token NOT NULL after backfill (optional, keep nullable for flexibility)
-- Add source column to orders for tracking order origin
ALTER TABLE orders ADD COLUMN IF NOT EXISTS source VARCHAR(50) DEFAULT 'staff';