Self Order
This commit is contained in:
parent
2c76962959
commit
b993da898f
@ -42,7 +42,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
processors := a.initProcessors(cfg, repos)
|
processors := a.initProcessors(cfg, repos)
|
||||||
services := a.initServices(processors, repos, cfg)
|
services := a.initServices(processors, repos, cfg)
|
||||||
validators := a.initValidators()
|
validators := a.initValidators()
|
||||||
middleware := a.initMiddleware(services, cfg)
|
middleware := a.initMiddleware(services, cfg, repos)
|
||||||
healthHandler := handler.NewHealthHandler()
|
healthHandler := handler.NewHealthHandler()
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
@ -105,6 +105,14 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
services.customerPointsService,
|
services.customerPointsService,
|
||||||
services.spinGameService,
|
services.spinGameService,
|
||||||
middleware.customerAuthMiddleware,
|
middleware.customerAuthMiddleware,
|
||||||
|
handler.NewSelfOrderHandler(
|
||||||
|
services.orderService,
|
||||||
|
services.productService,
|
||||||
|
repos.customerRepo,
|
||||||
|
repos.userRepo,
|
||||||
|
repos.outletRepo,
|
||||||
|
),
|
||||||
|
middleware.selfOrderMiddleware,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -441,12 +449,14 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
type middlewares struct {
|
type middlewares struct {
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
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{
|
return &middlewares{
|
||||||
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
||||||
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
|
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
|
||||||
|
selfOrderMiddleware: middleware.NewSelfOrderMiddleware(repos.tableRepo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@ const (
|
|||||||
CampaignRuleEntity = "campaign_rule"
|
CampaignRuleEntity = "campaign_rule"
|
||||||
CustomerEntity = "customer"
|
CustomerEntity = "customer"
|
||||||
SpinGameHandlerEntity = "spin_game_handler"
|
SpinGameHandlerEntity = "spin_game_handler"
|
||||||
|
SelfOrderEntity = "self_order"
|
||||||
)
|
)
|
||||||
|
|
||||||
var HttpErrorMap = map[string]int{
|
var HttpErrorMap = map[string]int{
|
||||||
|
|||||||
@ -60,6 +60,13 @@ func GetAllOrderItemStatuses() []OrderItemStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OrderSource string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderSourceStaff OrderSource = "staff"
|
||||||
|
OrderSourceSelfOrder OrderSource = "self_order"
|
||||||
|
)
|
||||||
|
|
||||||
func (o OrderType) IsValidOrderType() bool {
|
func (o OrderType) IsValidOrderType() bool {
|
||||||
for _, validType := range GetAllOrderTypes() {
|
for _, validType := range GetAllOrderTypes() {
|
||||||
if o == validType {
|
if o == validType {
|
||||||
|
|||||||
51
internal/contract/self_order_contract.go
Normal file
51
internal/contract/self_order_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
@ -53,6 +53,7 @@ type Order struct {
|
|||||||
RemainingAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"remaining_amount"`
|
RemainingAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"remaining_amount"`
|
||||||
PaymentStatus PaymentStatus `gorm:"default:'pending';size:50" json:"payment_status"`
|
PaymentStatus PaymentStatus `gorm:"default:'pending';size:50" json:"payment_status"`
|
||||||
RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"`
|
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"`
|
IsVoid bool `gorm:"default:false" json:"is_void"`
|
||||||
IsRefund bool `gorm:"default:false" json:"is_refund"`
|
IsRefund bool `gorm:"default:false" json:"is_refund"`
|
||||||
VoidReason *string `gorm:"size:255" json:"void_reason,omitempty"`
|
VoidReason *string `gorm:"size:255" json:"void_reason,omitempty"`
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type Table struct {
|
|||||||
PositionX float64 `gorm:"type:decimal(10,2);default:0.00" json:"position_x"`
|
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"`
|
PositionY float64 `gorm:"type:decimal(10,2);default:0.00" json:"position_y"`
|
||||||
Capacity int `gorm:"default:4" json:"capacity"`
|
Capacity int `gorm:"default:4" json:"capacity"`
|
||||||
|
Token string `gorm:"uniqueIndex;size:100" json:"token"`
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|||||||
239
internal/handler/self_order_handler.go
Normal file
239
internal/handler/self_order_handler.go
Normal 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")
|
||||||
|
}
|
||||||
53
internal/middleware/self_order_middleware.go
Normal file
53
internal/middleware/self_order_middleware.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -138,3 +138,15 @@ func (r *CustomerRepository) GetByEmail(ctx context.Context, email string, organ
|
|||||||
}
|
}
|
||||||
return &customer, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -170,3 +170,16 @@ func (r *TableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUID) (
|
|||||||
}
|
}
|
||||||
return &table, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -23,4 +23,5 @@ type TableRepositoryInterface interface {
|
|||||||
OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error
|
OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error
|
||||||
ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error
|
ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error
|
||||||
GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error)
|
GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error)
|
||||||
|
GetByToken(ctx context.Context, token string) (*entities.Table, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,17 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
|
|||||||
return users, total, err
|
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) {
|
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
query := r.db.WithContext(ctx).Model(&entities.User{})
|
query := r.db.WithContext(ctx).Model(&entities.User{})
|
||||||
|
|||||||
@ -46,11 +46,13 @@ type Router struct {
|
|||||||
customerAuthHandler *handler.CustomerAuthHandler
|
customerAuthHandler *handler.CustomerAuthHandler
|
||||||
customerPointsHandler *handler.CustomerPointsHandler
|
customerPointsHandler *handler.CustomerPointsHandler
|
||||||
spinGameHandler *handler.SpinGameHandler
|
spinGameHandler *handler.SpinGameHandler
|
||||||
|
selfOrderHandler *handler.SelfOrderHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
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{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -88,6 +90,8 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
|
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
customerAuthMiddleware: customerAuthMiddleware,
|
customerAuthMiddleware: customerAuthMiddleware,
|
||||||
|
selfOrderHandler: selfOrderHandler,
|
||||||
|
selfOrderMiddleware: selfOrderMiddleware,
|
||||||
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,6 +149,14 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
customer.POST("/spin", r.spinGameHandler.PlaySpinGame)
|
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 := v1.Group("/organizations")
|
||||||
{
|
{
|
||||||
organizations.POST("", r.organizationHandler.CreateOrganization)
|
organizations.POST("", r.organizationHandler.CreateOrganization)
|
||||||
|
|||||||
@ -182,6 +182,14 @@ func (m *MockTableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUI
|
|||||||
return args.Get(0).(*entities.Table), args.Error(1)
|
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) {
|
func TestCreateOrderWithTableOccupation(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
7
migrations/000063_add_self_order_support.down.sql
Normal file
7
migrations/000063_add_self_order_support.down.sql
Normal 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;
|
||||||
13
migrations/000063_add_self_order_support.up.sql
Normal file
13
migrations/000063_add_self_order_support.up.sql
Normal 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';
|
||||||
Loading…
x
Reference in New Issue
Block a user