This commit is contained in:
Aditya Siregar
2025-07-30 23:18:20 +07:00
parent 4a921df55d
commit a759e0f57c
57 changed files with 3633 additions and 190 deletions
BIN
View File
Binary file not shown.
+43 -31
View File
@@ -73,6 +73,7 @@ func (a *App) Initialize(cfg *config.Config) error {
services.paymentMethodService,
validators.paymentMethodValidator,
services.analyticsService,
services.tableService,
)
return nil
@@ -118,40 +119,44 @@ func (a *App) Shutdown() {
}
type repositories struct {
userRepo *repository.UserRepositoryImpl
organizationRepo *repository.OrganizationRepositoryImpl
outletRepo *repository.OutletRepositoryImpl
outletSettingRepo *repository.OutletSettingRepositoryImpl
categoryRepo *repository.CategoryRepositoryImpl
productRepo *repository.ProductRepositoryImpl
productVariantRepo *repository.ProductVariantRepositoryImpl
inventoryRepo *repository.InventoryRepositoryImpl
orderRepo *repository.OrderRepositoryImpl
orderItemRepo *repository.OrderItemRepositoryImpl
paymentRepo *repository.PaymentRepositoryImpl
paymentMethodRepo *repository.PaymentMethodRepositoryImpl
fileRepo *repository.FileRepositoryImpl
customerRepo *repository.CustomerRepository
analyticsRepo *repository.AnalyticsRepositoryImpl
userRepo *repository.UserRepositoryImpl
organizationRepo *repository.OrganizationRepositoryImpl
outletRepo *repository.OutletRepositoryImpl
outletSettingRepo *repository.OutletSettingRepositoryImpl
categoryRepo *repository.CategoryRepositoryImpl
productRepo *repository.ProductRepositoryImpl
productVariantRepo *repository.ProductVariantRepositoryImpl
inventoryRepo *repository.InventoryRepositoryImpl
inventoryMovementRepo *repository.InventoryMovementRepositoryImpl
orderRepo *repository.OrderRepositoryImpl
orderItemRepo *repository.OrderItemRepositoryImpl
paymentRepo *repository.PaymentRepositoryImpl
paymentMethodRepo *repository.PaymentMethodRepositoryImpl
fileRepo *repository.FileRepositoryImpl
customerRepo *repository.CustomerRepository
analyticsRepo *repository.AnalyticsRepositoryImpl
tableRepo *repository.TableRepository
}
func (a *App) initRepositories() *repositories {
return &repositories{
userRepo: repository.NewUserRepository(a.db),
organizationRepo: repository.NewOrganizationRepositoryImpl(a.db),
outletRepo: repository.NewOutletRepositoryImpl(a.db),
outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db),
categoryRepo: repository.NewCategoryRepositoryImpl(a.db),
productRepo: repository.NewProductRepositoryImpl(a.db),
productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db),
inventoryRepo: repository.NewInventoryRepositoryImpl(a.db),
orderRepo: repository.NewOrderRepositoryImpl(a.db),
orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db),
paymentRepo: repository.NewPaymentRepositoryImpl(a.db),
paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db),
fileRepo: repository.NewFileRepositoryImpl(a.db),
customerRepo: repository.NewCustomerRepository(a.db),
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
userRepo: repository.NewUserRepository(a.db),
organizationRepo: repository.NewOrganizationRepositoryImpl(a.db),
outletRepo: repository.NewOutletRepositoryImpl(a.db),
outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db),
categoryRepo: repository.NewCategoryRepositoryImpl(a.db),
productRepo: repository.NewProductRepositoryImpl(a.db),
productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db),
inventoryRepo: repository.NewInventoryRepositoryImpl(a.db),
inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db),
orderRepo: repository.NewOrderRepositoryImpl(a.db),
orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db),
paymentRepo: repository.NewPaymentRepositoryImpl(a.db),
paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db),
fileRepo: repository.NewFileRepositoryImpl(a.db),
customerRepo: repository.NewCustomerRepository(a.db),
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
tableRepo: repository.NewTableRepository(a.db),
}
}
@@ -169,6 +174,7 @@ type processors struct {
fileProcessor processor.FileProcessor
customerProcessor *processor.CustomerProcessor
analyticsProcessor *processor.AnalyticsProcessorImpl
tableProcessor *processor.TableProcessor
}
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
@@ -183,11 +189,12 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo),
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo),
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo),
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
}
}
@@ -206,6 +213,7 @@ type services struct {
fileService service.FileService
customerService service.CustomerService
analyticsService *service.AnalyticsServiceImpl
tableService *service.TableService
}
func (a *App) initServices(processors *processors, cfg *config.Config) *services {
@@ -224,6 +232,7 @@ func (a *App) initServices(processors *processors, cfg *config.Config) *services
fileService := service.NewFileServiceImpl(processors.fileProcessor)
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor)
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
return &services{
userService: service.NewUserService(processors.userProcessor),
@@ -240,6 +249,7 @@ func (a *App) initServices(processors *processors, cfg *config.Config) *services
fileService: fileService,
customerService: customerService,
analyticsService: analyticsService,
tableService: tableService,
}
}
@@ -265,6 +275,7 @@ type validators struct {
paymentMethodValidator validator.PaymentMethodValidator
fileValidator validator.FileValidator
customerValidator validator.CustomerValidator
tableValidator *validator.TableValidator
}
func (a *App) initValidators() *validators {
@@ -280,5 +291,6 @@ func (a *App) initValidators() *validators {
paymentMethodValidator: validator.NewPaymentMethodValidator(),
fileValidator: validator.NewFileValidatorImpl(),
customerValidator: validator.NewCustomerValidator(),
tableValidator: validator.NewTableValidator(),
}
}
+30
View File
@@ -0,0 +1,30 @@
package constants
type TableStatus string
const (
TableStatusAvailable TableStatus = "available"
TableStatusOccupied TableStatus = "occupied"
TableStatusReserved TableStatus = "reserved"
TableStatusCleaning TableStatus = "cleaning"
TableStatusMaintenance TableStatus = "maintenance"
)
func GetAllTableStatuses() []TableStatus {
return []TableStatus{
TableStatusAvailable,
TableStatusOccupied,
TableStatusReserved,
TableStatusCleaning,
TableStatusMaintenance,
}
}
func IsValidTableStatus(status TableStatus) bool {
for _, validStatus := range GetAllTableStatuses() {
if status == validStatus {
return true
}
}
return false
}
+6 -2
View File
@@ -14,7 +14,8 @@ type CreateProductRequest struct {
Price float64 `json:"price" validate:"required,min=0"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
BusinessType *string `json:"business_type,omitempty"`
Image *string `json:"image,omitempty"` // Will be stored in metadata["image"]
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
@@ -31,7 +32,8 @@ type UpdateProductRequest struct {
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
BusinessType *string `json:"business_type,omitempty"`
Image *string `json:"image,omitempty"` // Will be stored in metadata["image"]
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
// Stock management fields
@@ -63,6 +65,8 @@ type ProductResponse struct {
Price float64 `json:"price"`
Cost float64 `json:"cost"`
BusinessType string `json:"business_type"`
ImageURL *string `json:"image_url"`
PrinterType string `json:"printer_type"`
Metadata map[string]interface{} `json:"metadata"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
+82
View File
@@ -0,0 +1,82 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateTableRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
TableName string `json:"table_name" validate:"required,max=100"`
PositionX float64 `json:"position_x" validate:"required"`
PositionY float64 `json:"position_y" validate:"required"`
Capacity int `json:"capacity" validate:"required,min=1,max=20"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateTableRequest struct {
TableName *string `json:"table_name,omitempty" validate:"omitempty,max=100"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=available occupied reserved cleaning maintenance"`
PositionX *float64 `json:"position_x,omitempty"`
PositionY *float64 `json:"position_y,omitempty"`
Capacity *int `json:"capacity,omitempty" validate:"omitempty,min=1,max=20"`
IsActive *bool `json:"is_active,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type OccupyTableRequest struct {
OrderID uuid.UUID `json:"order_id" validate:"required"`
StartTime time.Time `json:"start_time" validate:"required"`
}
type ReleaseTableRequest struct {
PaymentAmount float64 `json:"payment_amount" validate:"required,min=0"`
}
type TableResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID uuid.UUID `json:"outlet_id"`
TableName string `json:"table_name"`
StartTime *time.Time `json:"start_time,omitempty"`
Status string `json:"status"`
OrderID *uuid.UUID `json:"order_id,omitempty"`
PaymentAmount float64 `json:"payment_amount"`
PositionX float64 `json:"position_x"`
PositionY float64 `json:"position_y"`
Capacity int `json:"capacity"`
IsActive bool `json:"is_active"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Order *OrderResponse `json:"order,omitempty"`
}
type ListTablesQuery struct {
OrganizationID string `form:"organization_id"`
OutletID string `form:"outlet_id"`
Status string `form:"status"`
IsActive string `form:"is_active"`
Search string `form:"search"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=10"`
}
type ListTablesRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=available occupied reserved cleaning maintenance"`
IsActive *bool `json:"is_active,omitempty"`
Search string `json:"search,omitempty"`
}
type ListTablesResponse struct {
Tables []TableResponse `json:"tables"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
+4
View File
@@ -30,6 +30,10 @@ type ChangePasswordRequest struct {
NewPassword string `json:"new_password" validate:"required,min=6"`
}
type UpdateUserOutletRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
}
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
+1
View File
@@ -17,6 +17,7 @@ func GetAllEntities() []interface{} {
&PaymentMethod{},
&Payment{},
&Customer{},
&Table{},
// Analytics entities are not database tables, they are query results
}
}
+110
View File
@@ -0,0 +1,110 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type InventoryMovementType string
const (
InventoryMovementTypeSale InventoryMovementType = "sale"
InventoryMovementTypePurchase InventoryMovementType = "purchase"
InventoryMovementTypeAdjustment InventoryMovementType = "adjustment"
InventoryMovementTypeReturn InventoryMovementType = "return"
InventoryMovementTypeRefund InventoryMovementType = "refund"
InventoryMovementTypeVoid InventoryMovementType = "void"
InventoryMovementTypeTransferIn InventoryMovementType = "transfer_in"
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
InventoryMovementTypeDamage InventoryMovementType = "damage"
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
)
type InventoryMovementReferenceType string
const (
InventoryMovementReferenceTypeOrder InventoryMovementReferenceType = "order"
InventoryMovementReferenceTypePayment InventoryMovementReferenceType = "payment"
InventoryMovementReferenceTypeRefund InventoryMovementReferenceType = "refund"
InventoryMovementReferenceTypeVoid InventoryMovementReferenceType = "void"
InventoryMovementReferenceTypeManual InventoryMovementReferenceType = "manual"
InventoryMovementReferenceTypeTransfer InventoryMovementReferenceType = "transfer"
InventoryMovementReferenceTypePurchaseOrder InventoryMovementReferenceType = "purchase_order"
)
type InventoryMovement struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"`
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
Quantity int `gorm:"not null" json:"quantity" validate:"required"`
PreviousQuantity int `gorm:"not null" json:"previous_quantity" validate:"required"`
NewQuantity int `gorm:"not null" json:"new_quantity" validate:"required"`
UnitCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"unit_cost"`
TotalCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"total_cost"`
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
Reason *string `gorm:"size:255" json:"reason"`
Notes *string `gorm:"type:text" json:"notes"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
if im.ID == uuid.Nil {
im.ID = uuid.New()
}
return nil
}
func (InventoryMovement) TableName() string {
return "inventory_movements"
}
func (im *InventoryMovement) IsPositiveMovement() bool {
return im.Quantity > 0
}
func (im *InventoryMovement) IsNegativeMovement() bool {
return im.Quantity < 0
}
func (im *InventoryMovement) GetMovementDescription() string {
switch im.MovementType {
case InventoryMovementTypeSale:
return "Sale"
case InventoryMovementTypePurchase:
return "Purchase"
case InventoryMovementTypeAdjustment:
return "Manual Adjustment"
case InventoryMovementTypeReturn:
return "Return"
case InventoryMovementTypeRefund:
return "Refund"
case InventoryMovementTypeVoid:
return "Void"
case InventoryMovementTypeTransferIn:
return "Transfer In"
case InventoryMovementTypeTransferOut:
return "Transfer Out"
case InventoryMovementTypeDamage:
return "Damage"
case InventoryMovementTypeExpiry:
return "Expiry"
default:
return "Unknown"
}
}
+2
View File
@@ -17,6 +17,8 @@ type Product struct {
Price float64 `gorm:"type:decimal(10,2);not null" json:"price" validate:"required,min=0"`
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost" validate:"min=0"`
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
ImageURL *string `gorm:"size:500" json:"image_url"`
PrinterType string `gorm:"size:50;default:'kitchen'" json:"printer_type"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
+53
View File
@@ -0,0 +1,53 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Table struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
TableName string `gorm:"not null;size:100" json:"table_name" validate:"required"`
StartTime *time.Time `gorm:"" json:"start_time"`
Status string `gorm:"default:'available';size:50" json:"status"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
PaymentAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"payment_amount"`
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"`
IsActive bool `gorm:"default:true" json:"is_active"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
}
func (t *Table) BeforeCreate(tx *gorm.DB) error {
if t.ID == uuid.Nil {
t.ID = uuid.New()
}
return nil
}
func (Table) TableName() string {
return "tables"
}
func (t *Table) IsAvailable() bool {
return t.Status == "available"
}
func (t *Table) IsOccupied() bool {
return t.Status == "occupied"
}
func (t *Table) CanBeOccupied() bool {
return t.Status == "available" || t.Status == "cleaning"
}
+3 -1
View File
@@ -1,6 +1,7 @@
package handler
import (
"apskel-pos-be/internal/util"
"net/http"
"strings"
@@ -51,7 +52,8 @@ func (h *AuthHandler) Login(c *gin.Context) {
}
logger.FromContext(c.Request.Context()).Infof("AuthHandler::Login -> Successfully logged in user = %s", loginResponse.User.Email)
c.JSON(http.StatusOK, loginResponse)
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(loginResponse), "AuthHandler::Login")
}
func (h *AuthHandler) Logout(c *gin.Context) {
+385
View File
@@ -0,0 +1,385 @@
package handler
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/service"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type TableHandler struct {
tableService *service.TableService
}
func NewTableHandler(tableService *service.TableService) *TableHandler {
return &TableHandler{
tableService: tableService,
}
}
// CreateTable godoc
// @Summary Create a new table
// @Description Create a new table for the organization
// @Tags tables
// @Accept json
// @Produce json
// @Param table body contract.CreateTableRequest true "Table data"
// @Success 201 {object} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 401 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables [post]
func (h *TableHandler) Create(c *gin.Context) {
var req contract.CreateTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
organizationID := c.GetString("organization_id")
orgID, err := uuid.Parse(organizationID)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid organization ID",
Message: err.Error(),
})
return
}
response, err := h.tableService.Create(c.Request.Context(), req, orgID)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to create table",
Message: err.Error(),
})
return
}
c.JSON(http.StatusCreated, response)
}
// GetTable godoc
// @Summary Get table by ID
// @Description Get table details by ID
// @Tags tables
// @Produce json
// @Param id path string true "Table ID"
// @Success 200 {object} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 404 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables/{id} [get]
func (h *TableHandler) GetByID(c *gin.Context) {
id := c.Param("id")
tableID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid table ID",
Message: err.Error(),
})
return
}
response, err := h.tableService.GetByID(c.Request.Context(), tableID)
if err != nil {
c.JSON(http.StatusNotFound, contract.ResponseError{
Error: "Table not found",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, response)
}
// UpdateTable godoc
// @Summary Update table
// @Description Update table details
// @Tags tables
// @Accept json
// @Produce json
// @Param id path string true "Table ID"
// @Param table body contract.UpdateTableRequest true "Table update data"
// @Success 200 {object} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 404 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables/{id} [put]
func (h *TableHandler) Update(c *gin.Context) {
id := c.Param("id")
tableID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid table ID",
Message: err.Error(),
})
return
}
var req contract.UpdateTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
response, err := h.tableService.Update(c.Request.Context(), tableID, req)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to update table",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, response)
}
// DeleteTable godoc
// @Summary Delete table
// @Description Delete table by ID
// @Tags tables
// @Produce json
// @Param id path string true "Table ID"
// @Success 204 "No Content"
// @Failure 400 {object} contract.ResponseError
// @Failure 404 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables/{id} [delete]
func (h *TableHandler) Delete(c *gin.Context) {
id := c.Param("id")
tableID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid table ID",
Message: err.Error(),
})
return
}
err = h.tableService.Delete(c.Request.Context(), tableID)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to delete table",
Message: err.Error(),
})
return
}
c.Status(http.StatusNoContent)
}
// ListTables godoc
// @Summary List tables
// @Description Get paginated list of tables
// @Tags tables
// @Produce json
// @Param organization_id query string false "Organization ID"
// @Param outlet_id query string false "Outlet ID"
// @Param status query string false "Table status"
// @Param is_active query string false "Is active"
// @Param search query string false "Search term"
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Page size" default(10)
// @Success 200 {object} contract.ListTablesResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables [get]
func (h *TableHandler) List(c *gin.Context) {
query := contract.ListTablesQuery{
OrganizationID: c.Query("organization_id"),
OutletID: c.Query("outlet_id"),
Status: c.Query("status"),
IsActive: c.Query("is_active"),
Search: c.Query("search"),
Page: 1,
Limit: 10,
}
if pageStr := c.Query("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
query.Page = page
}
}
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 && limit <= 100 {
query.Limit = limit
}
}
response, err := h.tableService.List(c.Request.Context(), query)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to list tables",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, response)
}
// OccupyTable godoc
// @Summary Occupy table
// @Description Occupy a table with an order
// @Tags tables
// @Accept json
// @Produce json
// @Param id path string true "Table ID"
// @Param request body contract.OccupyTableRequest true "Occupy table data"
// @Success 200 {object} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 404 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables/{id}/occupy [post]
func (h *TableHandler) OccupyTable(c *gin.Context) {
id := c.Param("id")
tableID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid table ID",
Message: err.Error(),
})
return
}
var req contract.OccupyTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
response, err := h.tableService.OccupyTable(c.Request.Context(), tableID, req)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to occupy table",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, response)
}
// ReleaseTable godoc
// @Summary Release table
// @Description Release a table and record payment amount
// @Tags tables
// @Accept json
// @Produce json
// @Param id path string true "Table ID"
// @Param request body contract.ReleaseTableRequest true "Release table data"
// @Success 200 {object} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 404 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /tables/{id}/release [post]
func (h *TableHandler) ReleaseTable(c *gin.Context) {
id := c.Param("id")
tableID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid table ID",
Message: err.Error(),
})
return
}
var req contract.ReleaseTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
response, err := h.tableService.ReleaseTable(c.Request.Context(), tableID, req)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to release table",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, response)
}
// GetAvailableTables godoc
// @Summary Get available tables
// @Description Get list of available tables for an outlet
// @Tags tables
// @Produce json
// @Param outlet_id path string true "Outlet ID"
// @Success 200 {array} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /outlets/{outlet_id}/tables/available [get]
func (h *TableHandler) GetAvailableTables(c *gin.Context) {
outletIDStr := c.Param("outlet_id")
outletID, err := uuid.Parse(outletIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid outlet ID",
Message: err.Error(),
})
return
}
tables, err := h.tableService.GetAvailableTables(c.Request.Context(), outletID)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to get available tables",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, tables)
}
// GetOccupiedTables godoc
// @Summary Get occupied tables
// @Description Get list of occupied tables for an outlet
// @Tags tables
// @Produce json
// @Param outlet_id path string true "Outlet ID"
// @Success 200 {array} contract.TableResponse
// @Failure 400 {object} contract.ResponseError
// @Failure 500 {object} contract.ResponseError
// @Router /outlets/{outlet_id}/tables/occupied [get]
func (h *TableHandler) GetOccupiedTables(c *gin.Context) {
outletIDStr := c.Param("outlet_id")
outletID, err := uuid.Parse(outletIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, contract.ResponseError{
Error: "Invalid outlet ID",
Message: err.Error(),
})
return
}
tables, err := h.tableService.GetOccupiedTables(c.Request.Context(), outletID)
if err != nil {
c.JSON(http.StatusInternalServerError, contract.ResponseError{
Error: "Failed to get occupied tables",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, tables)
}
+44 -2
View File
@@ -8,6 +8,7 @@ import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/transformer"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
@@ -288,8 +289,49 @@ func (h *UserHandler) DeactivateUser(c *gin.Context) {
return
}
logger.FromContext(c).Info("UserHandler::DeactivateUser -> Successfully deactivated user")
c.JSON(http.StatusOK, transformer.CreateSuccessResponse("User deactivated successfully", nil))
logger.FromContext(c).Infof("UserHandler::DeactivateUser -> Successfully deactivated user with ID: %s", userID.String())
c.JSON(http.StatusOK, gin.H{"message": "User deactivated successfully"})
}
func (h *UserHandler) UpdateUserOutlet(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := uuid.Parse(userIDStr)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUserOutlet -> Invalid user ID")
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
return
}
validationError, validationErrorCode := h.userValidator.ValidateUserID(userID)
if validationError != nil {
logger.FromContext(c).WithError(validationError).Error("UserHandler::UpdateUserOutlet -> user ID validation failed")
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
return
}
var req contract.UpdateUserOutletRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUserOutlet -> request binding failed")
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
return
}
validationError, validationErrorCode = h.userValidator.ValidateUpdateUserOutletRequest(&req)
if validationError != nil {
logger.FromContext(c).WithError(validationError).Error("UserHandler::UpdateUserOutlet -> request validation failed")
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
return
}
userResponse, err := h.userService.UpdateUserOutlet(c.Request.Context(), userID, &req)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUserOutlet -> Failed to update user outlet from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
logger.FromContext(c).Infof("UserHandler::UpdateUserOutlet -> Successfully updated user outlet = %+v", userResponse)
c.JSON(http.StatusOK, userResponse)
}
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
+1
View File
@@ -16,4 +16,5 @@ type UserService interface {
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
ActivateUser(ctx context.Context, userID uuid.UUID) error
DeactivateUser(ctx context.Context, userID uuid.UUID) error
UpdateUserOutlet(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserOutletRequest) (*contract.UserResponse, error)
}
+2
View File
@@ -2,6 +2,7 @@ package handler
import (
"apskel-pos-be/internal/contract"
"github.com/google/uuid"
)
@@ -11,4 +12,5 @@ type UserValidator interface {
ValidateListUsersRequest(req *contract.ListUsersRequest) (error, string)
ValidateChangePasswordRequest(req *contract.ChangePasswordRequest) (error, string)
ValidateUserID(userID uuid.UUID) (error, string)
ValidateUpdateUserOutletRequest(req *contract.UpdateUserOutletRequest) (error, string)
}
@@ -0,0 +1,135 @@
package mappers
import (
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
)
func InventoryMovementEntityToModel(entity *entities.InventoryMovement) *models.InventoryMovement {
if entity == nil {
return nil
}
return &models.InventoryMovement{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ProductID: entity.ProductID,
MovementType: models.InventoryMovementType(entity.MovementType),
Quantity: entity.Quantity,
PreviousQuantity: entity.PreviousQuantity,
NewQuantity: entity.NewQuantity,
UnitCost: entity.UnitCost,
TotalCost: entity.TotalCost,
ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType),
ReferenceID: entity.ReferenceID,
OrderID: entity.OrderID,
PaymentID: entity.PaymentID,
UserID: entity.UserID,
Reason: entity.Reason,
Notes: entity.Notes,
Metadata: map[string]interface{}(entity.Metadata),
CreatedAt: entity.CreatedAt,
}
}
func InventoryMovementModelToEntity(model *models.InventoryMovement) *entities.InventoryMovement {
if model == nil {
return nil
}
return &entities.InventoryMovement{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
ProductID: model.ProductID,
MovementType: entities.InventoryMovementType(model.MovementType),
Quantity: model.Quantity,
PreviousQuantity: model.PreviousQuantity,
NewQuantity: model.NewQuantity,
UnitCost: model.UnitCost,
TotalCost: model.TotalCost,
ReferenceType: (*entities.InventoryMovementReferenceType)(model.ReferenceType),
ReferenceID: model.ReferenceID,
OrderID: model.OrderID,
PaymentID: model.PaymentID,
UserID: model.UserID,
Reason: model.Reason,
Notes: model.Notes,
Metadata: entities.Metadata(model.Metadata),
CreatedAt: model.CreatedAt,
}
}
func InventoryMovementEntityToResponse(entity *entities.InventoryMovement) *models.InventoryMovementResponse {
if entity == nil {
return nil
}
return &models.InventoryMovementResponse{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
ProductID: entity.ProductID,
MovementType: models.InventoryMovementType(entity.MovementType),
Quantity: entity.Quantity,
PreviousQuantity: entity.PreviousQuantity,
NewQuantity: entity.NewQuantity,
UnitCost: entity.UnitCost,
TotalCost: entity.TotalCost,
ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType),
ReferenceID: entity.ReferenceID,
OrderID: entity.OrderID,
PaymentID: entity.PaymentID,
UserID: entity.UserID,
Reason: entity.Reason,
Notes: entity.Notes,
Metadata: map[string]interface{}(entity.Metadata),
CreatedAt: entity.CreatedAt,
MovementDescription: entity.GetMovementDescription(),
}
}
func InventoryMovementEntitiesToResponses(entities []*entities.InventoryMovement) []models.InventoryMovementResponse {
responses := make([]models.InventoryMovementResponse, len(entities))
for i, entity := range entities {
if response := InventoryMovementEntityToResponse(entity); response != nil {
responses[i] = *response
}
}
return responses
}
func InventoryMovementEntitiesToModels(entities []*entities.InventoryMovement) []*models.InventoryMovement {
models := make([]*models.InventoryMovement, len(entities))
for i, entity := range entities {
models[i] = InventoryMovementEntityToModel(entity)
}
return models
}
func CreateInventoryMovementRequestToEntity(req *models.CreateInventoryMovementRequest, previousQuantity, newQuantity int) *entities.InventoryMovement {
if req == nil {
return nil
}
return &entities.InventoryMovement{
OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
ProductID: req.ProductID,
MovementType: entities.InventoryMovementType(req.MovementType),
Quantity: req.Quantity,
PreviousQuantity: previousQuantity,
NewQuantity: newQuantity,
UnitCost: req.UnitCost,
TotalCost: float64(req.Quantity) * req.UnitCost,
ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType),
ReferenceID: req.ReferenceID,
OrderID: req.OrderID,
PaymentID: req.PaymentID,
UserID: req.UserID,
Reason: req.Reason,
Notes: req.Notes,
Metadata: entities.Metadata(req.Metadata),
}
}
+21
View File
@@ -21,6 +21,8 @@ func ProductEntityToModel(entity *entities.Product) *models.Product {
Price: entity.Price,
Cost: entity.Cost,
BusinessType: constants.BusinessType(entity.BusinessType),
ImageURL: entity.ImageURL,
PrinterType: entity.PrinterType,
Metadata: map[string]interface{}(entity.Metadata),
IsActive: entity.IsActive,
CreatedAt: entity.CreatedAt,
@@ -43,6 +45,8 @@ func ProductModelToEntity(model *models.Product) *entities.Product {
Price: model.Price,
Cost: model.Cost,
BusinessType: string(model.BusinessType),
ImageURL: model.ImageURL,
PrinterType: model.PrinterType,
Metadata: entities.Metadata(model.Metadata),
IsActive: model.IsActive,
CreatedAt: model.CreatedAt,
@@ -65,6 +69,11 @@ func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Pr
businessType = string(req.BusinessType)
}
printerType := "kitchen"
if req.PrinterType != nil && *req.PrinterType != "" {
printerType = *req.PrinterType
}
metadata := entities.Metadata{}
if req.Metadata != nil {
metadata = entities.Metadata(req.Metadata)
@@ -79,6 +88,8 @@ func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Pr
Price: req.Price,
Cost: cost,
BusinessType: businessType,
ImageURL: req.ImageURL,
PrinterType: printerType,
Metadata: metadata,
IsActive: true, // Default to active
}
@@ -117,6 +128,8 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
Price: entity.Price,
Cost: entity.Cost,
BusinessType: constants.BusinessType(entity.BusinessType),
ImageURL: entity.ImageURL,
PrinterType: entity.PrinterType,
Metadata: map[string]interface{}(entity.Metadata),
IsActive: entity.IsActive,
CreatedAt: entity.CreatedAt,
@@ -154,6 +167,14 @@ func UpdateProductEntityFromRequest(entity *entities.Product, req *models.Update
entity.Cost = *req.Cost
}
if req.ImageURL != nil {
entity.ImageURL = req.ImageURL
}
if req.PrinterType != nil {
entity.PrinterType = *req.PrinterType
}
if req.Metadata != nil {
if entity.Metadata == nil {
entity.Metadata = make(entities.Metadata)
+155
View File
@@ -0,0 +1,155 @@
package models
import (
"time"
"github.com/google/uuid"
)
type InventoryMovementType string
const (
InventoryMovementTypeSale InventoryMovementType = "sale"
InventoryMovementTypePurchase InventoryMovementType = "purchase"
InventoryMovementTypeAdjustment InventoryMovementType = "adjustment"
InventoryMovementTypeReturn InventoryMovementType = "return"
InventoryMovementTypeRefund InventoryMovementType = "refund"
InventoryMovementTypeVoid InventoryMovementType = "void"
InventoryMovementTypeTransferIn InventoryMovementType = "transfer_in"
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
InventoryMovementTypeDamage InventoryMovementType = "damage"
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
)
type InventoryMovementReferenceType string
const (
InventoryMovementReferenceTypeOrder InventoryMovementReferenceType = "order"
InventoryMovementReferenceTypePayment InventoryMovementReferenceType = "payment"
InventoryMovementReferenceTypeRefund InventoryMovementReferenceType = "refund"
InventoryMovementReferenceTypeVoid InventoryMovementReferenceType = "void"
InventoryMovementReferenceTypeManual InventoryMovementReferenceType = "manual"
InventoryMovementReferenceTypeTransfer InventoryMovementReferenceType = "transfer"
InventoryMovementReferenceTypePurchaseOrder InventoryMovementReferenceType = "purchase_order"
)
type InventoryMovement struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID uuid.UUID
ProductID uuid.UUID
MovementType InventoryMovementType
Quantity int
PreviousQuantity int
NewQuantity int
UnitCost float64
TotalCost float64
ReferenceType *InventoryMovementReferenceType
ReferenceID *uuid.UUID
OrderID *uuid.UUID
PaymentID *uuid.UUID
UserID uuid.UUID
Reason *string
Notes *string
Metadata map[string]interface{}
CreatedAt time.Time
}
type CreateInventoryMovementRequest struct {
OrganizationID uuid.UUID
OutletID uuid.UUID
ProductID uuid.UUID
MovementType InventoryMovementType
Quantity int
UnitCost float64
ReferenceType *InventoryMovementReferenceType
ReferenceID *uuid.UUID
OrderID *uuid.UUID
PaymentID *uuid.UUID
UserID uuid.UUID
Reason *string
Notes *string
Metadata map[string]interface{}
}
type InventoryMovementResponse struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID uuid.UUID
ProductID uuid.UUID
MovementType InventoryMovementType
Quantity int
PreviousQuantity int
NewQuantity int
UnitCost float64
TotalCost float64
ReferenceType *InventoryMovementReferenceType
ReferenceID *uuid.UUID
OrderID *uuid.UUID
PaymentID *uuid.UUID
UserID uuid.UUID
Reason *string
Notes *string
Metadata map[string]interface{}
CreatedAt time.Time
MovementDescription string
}
type ListInventoryMovementsRequest struct {
OrganizationID *uuid.UUID
OutletID *uuid.UUID
ProductID *uuid.UUID
MovementType *InventoryMovementType
ReferenceType *InventoryMovementReferenceType
ReferenceID *uuid.UUID
OrderID *uuid.UUID
PaymentID *uuid.UUID
UserID *uuid.UUID
DateFrom *time.Time
DateTo *time.Time
Page int
Limit int
}
type ListInventoryMovementsResponse struct {
Movements []InventoryMovementResponse
TotalCount int
Page int
Limit int
TotalPages int
}
func (im *InventoryMovement) IsPositiveMovement() bool {
return im.Quantity > 0
}
func (im *InventoryMovement) IsNegativeMovement() bool {
return im.Quantity < 0
}
func (im *InventoryMovement) GetMovementDescription() string {
switch im.MovementType {
case InventoryMovementTypeSale:
return "Sale"
case InventoryMovementTypePurchase:
return "Purchase"
case InventoryMovementTypeAdjustment:
return "Manual Adjustment"
case InventoryMovementTypeReturn:
return "Return"
case InventoryMovementTypeRefund:
return "Refund"
case InventoryMovementTypeVoid:
return "Void"
case InventoryMovementTypeTransferIn:
return "Transfer In"
case InventoryMovementTypeTransferOut:
return "Transfer Out"
case InventoryMovementTypeDamage:
return "Damage"
case InventoryMovementTypeExpiry:
return "Expiry"
default:
return "Unknown"
}
}
+8
View File
@@ -17,6 +17,8 @@ type Product struct {
Price float64
Cost float64
BusinessType constants.BusinessType
ImageURL *string
PrinterType string
Metadata map[string]interface{}
IsActive bool
CreatedAt time.Time
@@ -43,6 +45,8 @@ type CreateProductRequest struct {
Price float64 `validate:"required,min=0"`
Cost float64 `validate:"min=0"`
BusinessType constants.BusinessType `validate:"required"`
ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"`
Metadata map[string]interface{}
Variants []CreateProductVariantRequest `validate:"omitempty,dive"`
// Stock management fields
@@ -58,6 +62,8 @@ type UpdateProductRequest struct {
Description *string `validate:"omitempty,max=1000"`
Price *float64 `validate:"omitempty,min=0"`
Cost *float64 `validate:"omitempty,min=0"`
ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"`
Metadata map[string]interface{}
IsActive *bool
// Stock management fields
@@ -89,6 +95,8 @@ type ProductResponse struct {
Price float64
Cost float64
BusinessType constants.BusinessType
ImageURL *string
PrinterType string
Metadata map[string]interface{}
IsActive bool
CreatedAt time.Time
+103
View File
@@ -0,0 +1,103 @@
package models
import (
"apskel-pos-be/internal/constants"
"time"
"github.com/google/uuid"
)
type Table struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID uuid.UUID
TableName string
StartTime *time.Time
Status constants.TableStatus
OrderID *uuid.UUID
PaymentAmount float64
PositionX float64
PositionY float64
Capacity int
IsActive bool
Metadata map[string]interface{}
CreatedAt time.Time
UpdatedAt time.Time
}
type CreateTableRequest struct {
OutletID uuid.UUID `validate:"required"`
TableName string `validate:"required,max=100"`
PositionX float64 `validate:"required"`
PositionY float64 `validate:"required"`
Capacity int `validate:"required,min=1,max=20"`
Metadata map[string]interface{}
}
type UpdateTableRequest struct {
TableName *string `validate:"omitempty,max=100"`
Status *constants.TableStatus `validate:"omitempty"`
PositionX *float64 `validate:"omitempty"`
PositionY *float64 `validate:"omitempty"`
Capacity *int `validate:"omitempty,min=1,max=20"`
IsActive *bool `validate:"omitempty"`
Metadata map[string]interface{}
}
type OccupyTableRequest struct {
OrderID uuid.UUID `validate:"required"`
StartTime time.Time `validate:"required"`
}
type ReleaseTableRequest struct {
PaymentAmount float64 `validate:"required,min=0"`
}
type TableResponse struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID uuid.UUID
TableName string
StartTime *time.Time
Status constants.TableStatus
OrderID *uuid.UUID
PaymentAmount float64
PositionX float64
PositionY float64
Capacity int
IsActive bool
Metadata map[string]interface{}
CreatedAt time.Time
UpdatedAt time.Time
Order *OrderResponse
}
type ListTablesRequest struct {
OrganizationID *uuid.UUID
OutletID *uuid.UUID
Status *constants.TableStatus
IsActive *bool
Search string
Page int `validate:"required,min=1"`
Limit int `validate:"required,min=1,max=100"`
}
type ListTablesResponse struct {
Tables []TableResponse
TotalCount int
Page int
Limit int
TotalPages int
}
func (t *Table) IsAvailable() bool {
return t.Status == constants.TableStatusAvailable
}
func (t *Table) IsOccupied() bool {
return t.Status == constants.TableStatusOccupied
}
func (t *Table) CanBeOccupied() bool {
return t.Status == constants.TableStatusAvailable || t.Status == constants.TableStatusCleaning
}
+4
View File
@@ -44,6 +44,10 @@ type ChangePasswordRequest struct {
NewPassword string `validate:"required,min=6"`
}
type UpdateUserOutletRequest struct {
OutletID uuid.UUID `validate:"required"`
}
type UserResponse struct {
ID uuid.UUID
OrganizationID uuid.UUID
@@ -0,0 +1,214 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"github.com/google/uuid"
)
type InventoryMovementProcessor interface {
CreateMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error)
GetMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error)
ListMovements(ctx context.Context, req *models.ListInventoryMovementsRequest) (*models.ListInventoryMovementsResponse, error)
GetMovementsByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) (*models.ListInventoryMovementsResponse, error)
GetMovementsByOrderID(ctx context.Context, orderID uuid.UUID) ([]models.InventoryMovementResponse, error)
GetMovementsByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]models.InventoryMovementResponse, error)
}
type InventoryMovementRepository interface {
Create(ctx context.Context, movement *entities.InventoryMovement) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error)
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error)
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.InventoryMovement, int64, error)
GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) ([]*entities.InventoryMovement, int64, error)
GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.InventoryMovement, error)
GetByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]*entities.InventoryMovement, error)
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
}
type InventoryMovementProcessorImpl struct {
movementRepo InventoryMovementRepository
inventoryRepo repository.InventoryRepository
}
func NewInventoryMovementProcessorImpl(
movementRepo InventoryMovementRepository,
inventoryRepo repository.InventoryRepository,
) *InventoryMovementProcessorImpl {
return &InventoryMovementProcessorImpl{
movementRepo: movementRepo,
inventoryRepo: inventoryRepo,
}
}
func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error) {
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID)
if err != nil {
return nil, fmt.Errorf("failed to get current inventory: %w", err)
}
previousQuantity := currentInventory.Quantity
newQuantity := previousQuantity + req.Quantity
movement := &entities.InventoryMovement{
OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
ProductID: req.ProductID,
MovementType: entities.InventoryMovementType(req.MovementType),
Quantity: req.Quantity,
PreviousQuantity: previousQuantity,
NewQuantity: newQuantity,
UnitCost: req.UnitCost,
TotalCost: float64(req.Quantity) * req.UnitCost,
ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType),
ReferenceID: req.ReferenceID,
OrderID: req.OrderID,
PaymentID: req.PaymentID,
UserID: req.UserID,
Reason: req.Reason,
Notes: req.Notes,
Metadata: entities.Metadata(req.Metadata),
}
if err := p.movementRepo.Create(ctx, movement); err != nil {
return nil, fmt.Errorf("failed to create inventory movement: %w", err)
}
movementWithRelations, err := p.movementRepo.GetWithRelations(ctx, movement.ID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve created movement: %w", err)
}
response := mappers.InventoryMovementEntityToResponse(movementWithRelations)
return response, nil
}
func (p *InventoryMovementProcessorImpl) GetMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error) {
movement, err := p.movementRepo.GetWithRelations(ctx, id)
if err != nil {
return nil, fmt.Errorf("movement not found: %w", err)
}
response := mappers.InventoryMovementEntityToResponse(movement)
return response, nil
}
func (p *InventoryMovementProcessorImpl) ListMovements(ctx context.Context, req *models.ListInventoryMovementsRequest) (*models.ListInventoryMovementsResponse, error) {
filters := make(map[string]interface{})
if req.OrganizationID != nil {
filters["organization_id"] = *req.OrganizationID
}
if req.OutletID != nil {
filters["outlet_id"] = *req.OutletID
}
if req.ProductID != nil {
filters["product_id"] = *req.ProductID
}
if req.MovementType != nil {
filters["movement_type"] = string(*req.MovementType)
}
if req.ReferenceType != nil {
filters["reference_type"] = string(*req.ReferenceType)
}
if req.ReferenceID != nil {
filters["reference_id"] = *req.ReferenceID
}
if req.OrderID != nil {
filters["order_id"] = *req.OrderID
}
if req.PaymentID != nil {
filters["payment_id"] = *req.PaymentID
}
if req.UserID != nil {
filters["user_id"] = *req.UserID
}
if req.DateFrom != nil {
filters["date_from"] = *req.DateFrom
}
if req.DateTo != nil {
filters["date_to"] = *req.DateTo
}
offset := (req.Page - 1) * req.Limit
movements, total, err := p.movementRepo.List(ctx, filters, req.Limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to list movements: %w", err)
}
// Convert to responses
movementResponses := make([]models.InventoryMovementResponse, len(movements))
for i, movement := range movements {
response := mappers.InventoryMovementEntityToResponse(movement)
if response != nil {
movementResponses[i] = *response
}
}
// Calculate total pages
totalPages := int(total) / req.Limit
if int(total)%req.Limit > 0 {
totalPages++
}
return &models.ListInventoryMovementsResponse{
Movements: movementResponses,
TotalCount: int(total),
Page: req.Page,
Limit: req.Limit,
TotalPages: totalPages,
}, nil
}
func (p *InventoryMovementProcessorImpl) GetMovementsByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) (*models.ListInventoryMovementsResponse, error) {
movements, total, err := p.movementRepo.GetByProductAndOutlet(ctx, productID, outletID, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to get movements by product and outlet: %w", err)
}
movementResponses := make([]models.InventoryMovementResponse, len(movements))
for i, movement := range movements {
response := mappers.InventoryMovementEntityToResponse(movement)
if response != nil {
movementResponses[i] = *response
}
}
totalPages := int(total) / limit
if int(total)%limit > 0 {
totalPages++
}
return &models.ListInventoryMovementsResponse{
Movements: movementResponses,
TotalCount: int(total),
Page: 1,
Limit: limit,
TotalPages: totalPages,
}, nil
}
func (p *InventoryMovementProcessorImpl) GetMovementsByOrderID(ctx context.Context, orderID uuid.UUID) ([]models.InventoryMovementResponse, error) {
movements, err := p.movementRepo.GetByOrderID(ctx, orderID)
if err != nil {
return nil, fmt.Errorf("failed to get movements by order ID: %w", err)
}
responses := mappers.InventoryMovementEntitiesToResponses(movements)
return responses, nil
}
func (p *InventoryMovementProcessorImpl) GetMovementsByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]models.InventoryMovementResponse, error) {
movements, err := p.movementRepo.GetByPaymentID(ctx, paymentID)
if err != nil {
return nil, fmt.Errorf("failed to get movements by payment ID: %w", err)
}
responses := mappers.InventoryMovementEntitiesToResponses(movements)
return responses, nil
}
+3 -24
View File
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"github.com/google/uuid"
)
@@ -22,35 +22,14 @@ type InventoryProcessor interface {
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error)
}
type InventoryRepository interface {
Create(ctx context.Context, inventory *entities.Inventory) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error)
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error)
GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error)
GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error)
GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
Update(ctx context.Context, inventory *entities.Inventory) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error)
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error)
SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error)
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error
BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error
BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error
GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error)
}
type InventoryProcessorImpl struct {
inventoryRepo InventoryRepository
inventoryRepo repository.InventoryRepository
productRepo ProductRepository
outletRepo OutletRepository
}
func NewInventoryProcessorImpl(
inventoryRepo InventoryRepository,
inventoryRepo repository.InventoryRepository,
productRepo ProductRepository,
outletRepo OutletRepository,
) *InventoryProcessorImpl {
+33 -100
View File
@@ -8,6 +8,7 @@ import (
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"github.com/google/uuid"
)
@@ -62,6 +63,8 @@ type PaymentRepository interface {
RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error
GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error)
CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error)
RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, order *entities.Payment) error
}
type PaymentMethodRepository interface {
@@ -89,15 +92,16 @@ func (r *SimplePaymentMethodRepository) GetByID(ctx context.Context, id uuid.UUI
}
type OrderProcessorImpl struct {
orderRepo OrderRepository
orderItemRepo OrderItemRepository
paymentRepo PaymentRepository
productRepo ProductRepository
paymentMethodRepo PaymentMethodRepository
inventoryRepo InventoryRepository
productVariantRepo ProductVariantRepository
outletRepo OutletRepository
customerRepo CustomerRepository
orderRepo OrderRepository
orderItemRepo OrderItemRepository
paymentRepo PaymentRepository
productRepo ProductRepository
paymentMethodRepo PaymentMethodRepository
inventoryRepo repository.InventoryRepository
inventoryMovementRepo repository.InventoryMovementRepository
productVariantRepo ProductVariantRepository
outletRepo OutletRepository
customerRepo CustomerRepository
}
func NewOrderProcessorImpl(
@@ -106,21 +110,23 @@ func NewOrderProcessorImpl(
paymentRepo PaymentRepository,
productRepo ProductRepository,
paymentMethodRepo PaymentMethodRepository,
inventoryRepo InventoryRepository,
inventoryRepo repository.InventoryRepository,
inventoryMovementRepo repository.InventoryMovementRepository,
productVariantRepo ProductVariantRepository,
outletRepo OutletRepository,
customerRepo CustomerRepository,
) *OrderProcessorImpl {
return &OrderProcessorImpl{
orderRepo: orderRepo,
orderItemRepo: orderItemRepo,
paymentRepo: paymentRepo,
productRepo: productRepo,
paymentMethodRepo: paymentMethodRepo,
inventoryRepo: inventoryRepo,
productVariantRepo: productVariantRepo,
outletRepo: outletRepo,
customerRepo: customerRepo,
orderRepo: orderRepo,
orderItemRepo: orderItemRepo,
paymentRepo: paymentRepo,
productRepo: productRepo,
paymentMethodRepo: paymentMethodRepo,
inventoryRepo: inventoryRepo,
inventoryMovementRepo: inventoryMovementRepo,
productVariantRepo: productVariantRepo,
outletRepo: outletRepo,
customerRepo: customerRepo,
}
}
@@ -619,7 +625,6 @@ func (p *OrderProcessorImpl) RefundOrder(ctx context.Context, id uuid.UUID, req
return fmt.Errorf("order not found: %w", err)
}
// Check if order can be refunded
if order.IsRefund {
return fmt.Errorf("order is already refunded")
}
@@ -738,69 +743,11 @@ func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.Crea
return nil, fmt.Errorf("payment amount exceeds remaining balance")
}
payment := &entities.Payment{
OrderID: req.OrderID,
PaymentMethodID: req.PaymentMethodID,
Amount: req.Amount,
Status: entities.PaymentTransactionStatusCompleted,
TransactionID: req.TransactionID,
SplitNumber: req.SplitNumber,
SplitTotal: req.SplitTotal,
SplitDescription: req.SplitDescription,
Metadata: entities.Metadata(req.Metadata),
payment, err := p.paymentRepo.CreatePaymentWithInventoryMovement(ctx, req, order, totalPaid)
if err != nil {
return nil, err
}
if err := p.paymentRepo.Create(ctx, payment); err != nil {
return nil, fmt.Errorf("failed to create payment: %w", err)
}
if len(req.PaymentOrderItems) > 0 {
for _, itemPayment := range req.PaymentOrderItems {
paymentOrderItem := &entities.PaymentOrderItem{
PaymentID: payment.ID,
OrderItemID: itemPayment.OrderItemID,
Amount: itemPayment.Amount,
}
fmt.Println(paymentOrderItem)
// TODO: Create payment order item in database
// This would require a PaymentOrderItemRepository
}
}
// Update order payment status if fully paid
newTotalPaid := totalPaid + req.Amount
orderJustCompleted := false
if newTotalPaid >= order.TotalAmount {
if order.PaymentStatus != entities.PaymentStatusCompleted {
orderJustCompleted = true
}
if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusCompleted); err != nil {
return nil, fmt.Errorf("failed to update order payment status: %w", err)
}
// Set order status to completed when fully paid
if err := p.orderRepo.UpdateStatus(ctx, req.OrderID, entities.OrderStatusCompleted); err != nil {
return nil, fmt.Errorf("failed to update order status: %w", err)
}
} else {
if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusPartiallyRefunded); err != nil {
return nil, fmt.Errorf("failed to update order payment status: %w", err)
}
}
if orderJustCompleted {
orderItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
if err != nil {
return nil, fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
}
for _, item := range orderItems {
if _, err := p.inventoryRepo.AdjustQuantity(ctx, item.ProductID, order.OutletID, -item.Quantity); err != nil {
return nil, fmt.Errorf("failed to adjust inventory for product %s: %w", item.ProductID, err)
}
}
}
// Get payment with relations for response
paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve created payment: %w", err)
@@ -810,14 +757,16 @@ func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.Crea
return response, nil
}
func stringPtr(s string) *string {
return &s
}
func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
// Get payment
payment, err := p.paymentRepo.GetByID(ctx, paymentID)
if err != nil {
return fmt.Errorf("payment not found: %w", err)
}
// Check if payment can be refunded
if payment.Status != entities.PaymentTransactionStatusCompleted {
return fmt.Errorf("payment is not completed, cannot refund")
}
@@ -826,23 +775,7 @@ func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.U
return fmt.Errorf("refund amount cannot exceed payment amount")
}
// Process refund
if err := p.paymentRepo.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
return fmt.Errorf("failed to refund payment: %w", err)
}
// Update order refund amount
order, err := p.orderRepo.GetByID(ctx, payment.OrderID)
if err != nil {
return fmt.Errorf("failed to get order: %w", err)
}
order.RefundAmount += refundAmount
if err := p.orderRepo.Update(ctx, order); err != nil {
return fmt.Errorf("failed to update order refund amount: %w", err)
}
return nil
return p.paymentRepo.RefundPaymentWithInventoryMovement(ctx, paymentID, refundAmount, reason, refundedBy, payment)
}
func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
+2 -2
View File
@@ -45,11 +45,11 @@ type ProductProcessorImpl struct {
productRepo ProductRepository
categoryRepo CategoryRepository
productVariantRepo repository.ProductVariantRepository
inventoryRepo InventoryRepository
inventoryRepo repository.InventoryRepository
outletRepo OutletRepository
}
func NewProductProcessorImpl(productRepo ProductRepository, categoryRepo CategoryRepository, productVariantRepo repository.ProductVariantRepository, inventoryRepo InventoryRepository, outletRepo OutletRepository) *ProductProcessorImpl {
func NewProductProcessorImpl(productRepo ProductRepository, categoryRepo CategoryRepository, productVariantRepo repository.ProductVariantRepository, inventoryRepo repository.InventoryRepository, outletRepo OutletRepository) *ProductProcessorImpl {
return &ProductProcessorImpl{
productRepo: productRepo,
categoryRepo: categoryRepo,
+262
View File
@@ -0,0 +1,262 @@
package processor
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
"context"
"errors"
"math"
"github.com/google/uuid"
)
type TableProcessor struct {
tableRepo *repository.TableRepository
orderRepo *repository.OrderRepository
}
func NewTableProcessor(tableRepo *repository.TableRepository, orderRepo *repository.OrderRepository) *TableProcessor {
return &TableProcessor{
tableRepo: tableRepo,
orderRepo: orderRepo,
}
}
func (p *TableProcessor) Create(ctx context.Context, req models.CreateTableRequest, organizationID uuid.UUID) (*models.TableResponse, error) {
table := &entities.Table{
OrganizationID: organizationID,
OutletID: req.OutletID,
TableName: req.TableName,
PositionX: req.PositionX,
PositionY: req.PositionY,
Capacity: req.Capacity,
Status: string(constants.TableStatusAvailable),
IsActive: true,
Metadata: req.Metadata,
}
err := p.tableRepo.Create(ctx, table)
if err != nil {
return nil, err
}
return p.mapTableToResponse(table), nil
}
func (p *TableProcessor) GetByID(ctx context.Context, id uuid.UUID) (*models.TableResponse, error) {
table, err := p.tableRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
return p.mapTableToResponse(table), nil
}
func (p *TableProcessor) Update(ctx context.Context, id uuid.UUID, req models.UpdateTableRequest) (*models.TableResponse, error) {
table, err := p.tableRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if req.TableName != nil {
table.TableName = *req.TableName
}
if req.Status != nil {
table.Status = string(*req.Status)
}
if req.PositionX != nil {
table.PositionX = *req.PositionX
}
if req.PositionY != nil {
table.PositionY = *req.PositionY
}
if req.Capacity != nil {
table.Capacity = *req.Capacity
}
if req.IsActive != nil {
table.IsActive = *req.IsActive
}
if req.Metadata != nil {
table.Metadata = req.Metadata
}
err = p.tableRepo.Update(ctx, table)
if err != nil {
return nil, err
}
return p.mapTableToResponse(table), nil
}
func (p *TableProcessor) Delete(ctx context.Context, id uuid.UUID) error {
table, err := p.tableRepo.GetByID(ctx, id)
if err != nil {
return err
}
if table.IsOccupied() {
return errors.New("cannot delete occupied table")
}
return p.tableRepo.Delete(ctx, id)
}
func (p *TableProcessor) List(ctx context.Context, req models.ListTablesRequest) (*models.ListTablesResponse, error) {
tables, total, err := p.tableRepo.List(ctx, req.OrganizationID, req.OutletID, (*string)(req.Status), req.IsActive, req.Search, req.Page, req.Limit)
if err != nil {
return nil, err
}
responses := make([]models.TableResponse, len(tables))
for i, table := range tables {
responses[i] = *p.mapTableToResponse(&table)
}
totalPages := int(math.Ceil(float64(total) / float64(req.Limit)))
return &models.ListTablesResponse{
Tables: responses,
TotalCount: int(total),
Page: req.Page,
Limit: req.Limit,
TotalPages: totalPages,
}, nil
}
func (p *TableProcessor) OccupyTable(ctx context.Context, tableID uuid.UUID, req models.OccupyTableRequest) (*models.TableResponse, error) {
table, err := p.tableRepo.GetByID(ctx, tableID)
if err != nil {
return nil, err
}
if !table.CanBeOccupied() {
return nil, errors.New("table is not available for occupation")
}
// Verify order exists
order, err := p.orderRepo.GetByID(ctx, req.OrderID)
if err != nil {
return nil, errors.New("order not found")
}
err = p.tableRepo.OccupyTable(ctx, tableID, req.OrderID, &req.StartTime)
if err != nil {
return nil, err
}
// Get updated table
updatedTable, err := p.tableRepo.GetByID(ctx, tableID)
if err != nil {
return nil, err
}
return p.mapTableToResponse(updatedTable), nil
}
func (p *TableProcessor) ReleaseTable(ctx context.Context, tableID uuid.UUID, req models.ReleaseTableRequest) (*models.TableResponse, error) {
table, err := p.tableRepo.GetByID(ctx, tableID)
if err != nil {
return nil, err
}
if !table.IsOccupied() {
return nil, errors.New("table is not occupied")
}
err = p.tableRepo.ReleaseTable(ctx, tableID, req.PaymentAmount)
if err != nil {
return nil, err
}
// Get updated table
updatedTable, err := p.tableRepo.GetByID(ctx, tableID)
if err != nil {
return nil, err
}
return p.mapTableToResponse(updatedTable), nil
}
func (p *TableProcessor) GetAvailableTables(ctx context.Context, outletID uuid.UUID) ([]models.TableResponse, error) {
tables, err := p.tableRepo.GetAvailableTables(ctx, outletID)
if err != nil {
return nil, err
}
responses := make([]models.TableResponse, len(tables))
for i, table := range tables {
responses[i] = *p.mapTableToResponse(&table)
}
return responses, nil
}
func (p *TableProcessor) GetOccupiedTables(ctx context.Context, outletID uuid.UUID) ([]models.TableResponse, error) {
tables, err := p.tableRepo.GetOccupiedTables(ctx, outletID)
if err != nil {
return nil, err
}
responses := make([]models.TableResponse, len(tables))
for i, table := range tables {
responses[i] = *p.mapTableToResponse(&table)
}
return responses, nil
}
func (p *TableProcessor) mapTableToResponse(table *entities.Table) *models.TableResponse {
response := &models.TableResponse{
ID: table.ID,
OrganizationID: table.OrganizationID,
OutletID: table.OutletID,
TableName: table.TableName,
StartTime: table.StartTime,
Status: constants.TableStatus(table.Status),
OrderID: table.OrderID,
PaymentAmount: table.PaymentAmount,
PositionX: table.PositionX,
PositionY: table.PositionY,
Capacity: table.Capacity,
IsActive: table.IsActive,
Metadata: table.Metadata,
CreatedAt: table.CreatedAt,
UpdatedAt: table.UpdatedAt,
}
if table.Order != nil {
response.Order = &models.OrderResponse{
ID: table.Order.ID,
OrganizationID: table.Order.OrganizationID,
OutletID: table.Order.OutletID,
UserID: table.Order.UserID,
CustomerID: table.Order.CustomerID,
OrderNumber: table.Order.OrderNumber,
TableNumber: table.Order.TableNumber,
OrderType: constants.OrderType(table.Order.OrderType),
Status: constants.OrderStatus(table.Order.Status),
Subtotal: table.Order.Subtotal,
TaxAmount: table.Order.TaxAmount,
DiscountAmount: table.Order.DiscountAmount,
TotalAmount: table.Order.TotalAmount,
TotalCost: table.Order.TotalCost,
PaymentStatus: constants.PaymentStatus(table.Order.PaymentStatus),
RefundAmount: table.Order.RefundAmount,
IsVoid: table.Order.IsVoid,
IsRefund: table.Order.IsRefund,
VoidReason: table.Order.VoidReason,
VoidedAt: table.Order.VoidedAt,
VoidedBy: table.Order.VoidedBy,
RefundReason: table.Order.RefundReason,
RefundedAt: table.Order.RefundedAt,
RefundedBy: table.Order.RefundedBy,
Metadata: table.Order.Metadata,
CreatedAt: table.Order.CreatedAt,
UpdatedAt: table.Order.UpdatedAt,
}
}
return response
}
+29
View File
@@ -224,3 +224,32 @@ func (p *UserProcessorImpl) DeactivateUser(ctx context.Context, userID uuid.UUID
return nil
}
func (p *UserProcessorImpl) UpdateUserOutlet(ctx context.Context, userID uuid.UUID, req *models.UpdateUserOutletRequest) (*models.UserResponse, error) {
// Get user first to validate existence and get organization_id
existingUser, err := p.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// Validate outlet exists
outlet, err := p.outletRepo.GetByID(ctx, req.OutletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
}
// Validate outlet belongs to user's organization
if outlet.OrganizationID != existingUser.OrganizationID {
return nil, fmt.Errorf("outlet does not belong to user's organization")
}
// Update user's outlet_id
existingUser.OutletID = &req.OutletID
err = p.userRepo.Update(ctx, existingUser)
if err != nil {
return nil, fmt.Errorf("failed to update user outlet: %w", err)
}
return mappers.UserEntityToResponse(existingUser), nil
}
@@ -0,0 +1,185 @@
package repository
import (
"context"
"apskel-pos-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type InventoryMovementRepository interface {
Create(ctx context.Context, movement *entities.InventoryMovement) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error)
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error)
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.InventoryMovement, int64, error)
GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) ([]*entities.InventoryMovement, int64, error)
GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.InventoryMovement, error)
GetByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]*entities.InventoryMovement, error)
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
CreateWithTransaction(tx *gorm.DB, movement *entities.InventoryMovement) error
}
type InventoryMovementRepositoryImpl struct {
db *gorm.DB
}
func NewInventoryMovementRepositoryImpl(db *gorm.DB) *InventoryMovementRepositoryImpl {
return &InventoryMovementRepositoryImpl{
db: db,
}
}
func (r *InventoryMovementRepositoryImpl) Create(ctx context.Context, movement *entities.InventoryMovement) error {
return r.db.WithContext(ctx).Create(movement).Error
}
func (r *InventoryMovementRepositoryImpl) CreateWithTransaction(tx *gorm.DB, movement *entities.InventoryMovement) error {
return tx.Create(movement).Error
}
func (r *InventoryMovementRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error) {
var movement entities.InventoryMovement
err := r.db.WithContext(ctx).First(&movement, "id = ?", id).Error
if err != nil {
return nil, err
}
return &movement, nil
}
func (r *InventoryMovementRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error) {
var movement entities.InventoryMovement
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Product").
Preload("Product.Category").
Preload("Order").
Preload("Payment").
Preload("User").
First(&movement, "id = ?", id).Error
if err != nil {
return nil, err
}
return &movement, nil
}
func (r *InventoryMovementRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.InventoryMovement, int64, error) {
var movements []*entities.InventoryMovement
var total int64
query := r.db.WithContext(ctx).Model(&entities.InventoryMovement{}).
Preload("Product").
Preload("Product.Category").
Preload("Outlet").
Preload("Order").
Preload("Payment").
Preload("User")
for key, value := range filters {
switch key {
case "search":
searchValue := "%" + value.(string) + "%"
query = query.Joins("JOIN products ON inventory_movements.product_id = products.id").
Where("products.name ILIKE ? OR inventory_movements.reason ILIKE ?", searchValue, searchValue)
case "date_from":
query = query.Where("created_at >= ?", value)
case "date_to":
query = query.Where("created_at <= ?", value)
case "movement_type":
query = query.Where("movement_type = ?", value)
case "reference_type":
query = query.Where("reference_type = ?", value)
default:
query = query.Where(key+" = ?", value)
}
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&movements).Error
return movements, total, err
}
func (r *InventoryMovementRepositoryImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) ([]*entities.InventoryMovement, int64, error) {
var movements []*entities.InventoryMovement
var total int64
query := r.db.WithContext(ctx).Model(&entities.InventoryMovement{}).
Preload("Product").
Preload("Product.Category").
Preload("Order").
Preload("Payment").
Preload("User").
Where("product_id = ? AND outlet_id = ?", productID, outletID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&movements).Error
return movements, total, err
}
func (r *InventoryMovementRepositoryImpl) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.InventoryMovement, error) {
var movements []*entities.InventoryMovement
err := r.db.WithContext(ctx).
Preload("Product").
Preload("Product.Category").
Where("order_id = ?", orderID).
Order("created_at DESC").
Find(&movements).Error
return movements, err
}
func (r *InventoryMovementRepositoryImpl) GetByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]*entities.InventoryMovement, error) {
var movements []*entities.InventoryMovement
err := r.db.WithContext(ctx).
Preload("Product").
Preload("Product.Category").
Where("payment_id = ?", paymentID).
Order("created_at DESC").
Find(&movements).Error
return movements, err
}
func (r *InventoryMovementRepositoryImpl) GetByReference(ctx context.Context, referenceType entities.InventoryMovementReferenceType, referenceID uuid.UUID) ([]*entities.InventoryMovement, error) {
var movements []*entities.InventoryMovement
err := r.db.WithContext(ctx).
Preload("Product").
Preload("Product.Category").
Where("reference_type = ? AND reference_id = ?", referenceType, referenceID).
Order("created_at DESC").
Find(&movements).Error
return movements, err
}
func (r *InventoryMovementRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&entities.InventoryMovement{})
for key, value := range filters {
switch key {
case "search":
searchValue := "%" + value.(string) + "%"
query = query.Joins("JOIN products ON inventory_movements.product_id = products.id").
Where("products.name ILIKE ? OR inventory_movements.reason ILIKE ?", searchValue, searchValue)
case "date_from":
query = query.Where("created_at >= ?", value)
case "date_to":
query = query.Where("created_at <= ?", value)
case "movement_type":
query = query.Where("movement_type = ?", value)
case "reference_type":
query = query.Where("reference_type = ?", value)
default:
query = query.Where(key+" = ?", value)
}
}
err := query.Count(&count).Error
return count, err
}
+21 -2
View File
@@ -11,6 +11,27 @@ import (
"gorm.io/gorm"
)
type InventoryRepository interface {
Create(ctx context.Context, inventory *entities.Inventory) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error)
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error)
GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error)
GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error)
GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
Update(ctx context.Context, inventory *entities.Inventory) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error)
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error)
SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error)
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error
BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error
BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error
GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error)
}
type InventoryRepositoryImpl struct {
db *gorm.DB
}
@@ -209,10 +230,8 @@ func (r *InventoryRepositoryImpl) SetQuantity(ctx context.Context, productID, ou
var inventory entities.Inventory
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// Get current inventory
if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Inventory doesn't exist, create it with the specified quantity
inventory = entities.Inventory{
ProductID: productID,
OutletID: outletID,
+213
View File
@@ -2,9 +2,12 @@ package repository
import (
"context"
"errors"
"fmt"
"time"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
@@ -92,3 +95,213 @@ func (r *PaymentRepositoryImpl) GetTotalPaidByOrderID(ctx context.Context, order
Scan(&total).Error
return total, err
}
func (r *PaymentRepositoryImpl) CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error) {
var payment *entities.Payment
var orderJustCompleted bool
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
payment = &entities.Payment{
OrderID: req.OrderID,
PaymentMethodID: req.PaymentMethodID,
Amount: req.Amount,
Status: entities.PaymentTransactionStatusCompleted,
TransactionID: req.TransactionID,
SplitNumber: req.SplitNumber,
SplitTotal: req.SplitTotal,
SplitDescription: req.SplitDescription,
Metadata: entities.Metadata(req.Metadata),
}
if err := tx.Create(payment).Error; err != nil {
return fmt.Errorf("failed to create payment: %w", err)
}
newTotalPaid := totalPaid + req.Amount
if newTotalPaid >= order.TotalAmount {
if order.PaymentStatus != entities.PaymentStatusCompleted {
orderJustCompleted = true
}
if err := tx.Model(&entities.Order{}).Where("id = ?", req.OrderID).Update("payment_status", entities.PaymentStatusCompleted).Error; err != nil {
return fmt.Errorf("failed to update order payment status: %w", err)
}
if err := tx.Model(&entities.Order{}).Where("id = ?", req.OrderID).Update("status", entities.OrderStatusCompleted).Error; err != nil {
return fmt.Errorf("failed to update order status: %w", err)
}
} else {
if err := tx.Model(&entities.Order{}).Where("id = ?", req.OrderID).Update("payment_status", entities.PaymentStatusPartiallyRefunded).Error; err != nil {
return fmt.Errorf("failed to update order payment status: %w", err)
}
}
if orderJustCompleted {
orderItems, err := r.getOrderItemsWithTransaction(tx, req.OrderID)
if err != nil {
return fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
}
for _, item := range orderItems {
updatedInventory, err := r.adjustInventoryWithTransaction(tx, item.ProductID, order.OutletID, -item.Quantity)
if err != nil {
return fmt.Errorf("failed to adjust inventory for product %s: %w", item.ProductID, err)
}
movement := &entities.InventoryMovement{
OrganizationID: order.OrganizationID,
OutletID: order.OutletID,
ProductID: item.ProductID,
MovementType: entities.InventoryMovementTypeSale,
Quantity: -item.Quantity,
PreviousQuantity: updatedInventory.Quantity + item.Quantity, // Add back the quantity that was subtracted
NewQuantity: updatedInventory.Quantity,
UnitCost: item.UnitCost,
TotalCost: float64(item.Quantity) * item.UnitCost,
ReferenceType: func() *entities.InventoryMovementReferenceType {
t := entities.InventoryMovementReferenceTypePayment
return &t
}(),
ReferenceID: &payment.ID,
OrderID: &order.ID,
PaymentID: &payment.ID,
UserID: order.UserID,
Reason: stringPtr("Sale from order payment"),
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s", order.OrderNumber, payment.ID)),
Metadata: entities.Metadata{"order_item_id": item.ID},
}
if err := r.createInventoryMovementWithTransaction(tx, movement); err != nil {
return fmt.Errorf("failed to create inventory movement for product %s: %w", item.ProductID, err)
}
}
}
return nil
})
if err != nil {
return nil, err
}
return payment, nil
}
func (r *PaymentRepositoryImpl) RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, payment *entities.Payment) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := r.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
return fmt.Errorf("failed to refund payment: %w", err)
}
// Get order for inventory management
order, err := r.getOrderWithTransaction(tx, payment.OrderID)
if err != nil {
return fmt.Errorf("failed to get order: %w", err)
}
// Update order refund amount
order.RefundAmount += refundAmount
if err := tx.Model(&entities.Order{}).Where("id = ?", order.ID).Update("refund_amount", order.RefundAmount).Error; err != nil {
return fmt.Errorf("failed to update order refund amount: %w", err)
}
refundRatio := refundAmount / payment.Amount
orderItems, err := r.getOrderItemsWithTransaction(tx, order.ID)
if err != nil {
return fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
}
for _, item := range orderItems {
refundedQuantity := int(float64(item.Quantity) * refundRatio)
if refundedQuantity > 0 {
updatedInventory, err := r.adjustInventoryWithTransaction(tx, item.ProductID, order.OutletID, refundedQuantity)
if err != nil {
return fmt.Errorf("failed to restore inventory for product %s: %w", item.ProductID, err)
}
movement := &entities.InventoryMovement{
OrganizationID: order.OrganizationID,
OutletID: order.OutletID,
ProductID: item.ProductID,
MovementType: entities.InventoryMovementTypeRefund,
Quantity: refundedQuantity,
PreviousQuantity: updatedInventory.Quantity - refundedQuantity, // Subtract the quantity that was added
NewQuantity: updatedInventory.Quantity,
UnitCost: item.UnitCost,
TotalCost: float64(refundedQuantity) * item.UnitCost,
ReferenceType: func() *entities.InventoryMovementReferenceType {
t := entities.InventoryMovementReferenceTypeRefund
return &t
}(),
ReferenceID: &paymentID,
OrderID: &order.ID,
PaymentID: &paymentID,
UserID: refundedBy,
Reason: stringPtr(fmt.Sprintf("Refund: %s", reason)),
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Refund Amount: %.2f", order.OrderNumber, paymentID, refundAmount)),
Metadata: entities.Metadata{"order_item_id": item.ID, "refund_ratio": refundRatio},
}
if err := r.createInventoryMovementWithTransaction(tx, movement); err != nil {
return fmt.Errorf("failed to create inventory movement for refund product %s: %w", item.ProductID, err)
}
}
}
return nil
})
}
// Helper methods for transaction operations
func (r *PaymentRepositoryImpl) getOrderWithTransaction(tx *gorm.DB, orderID uuid.UUID) (*entities.Order, error) {
var order entities.Order
err := tx.First(&order, "id = ?", orderID).Error
if err != nil {
return nil, err
}
return &order, nil
}
func (r *PaymentRepositoryImpl) getOrderItemsWithTransaction(tx *gorm.DB, orderID uuid.UUID) ([]*entities.OrderItem, error) {
var orderItems []*entities.OrderItem
err := tx.Where("order_id = ?", orderID).Find(&orderItems).Error
return orderItems, err
}
func (r *PaymentRepositoryImpl) adjustInventoryWithTransaction(tx *gorm.DB, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) {
var inventory entities.Inventory
// Try to find existing inventory
if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Inventory doesn't exist, create it with initial quantity
inventory = entities.Inventory{
ProductID: productID,
OutletID: outletID,
Quantity: 0,
ReorderLevel: 0,
}
if err := tx.Create(&inventory).Error; err != nil {
return nil, fmt.Errorf("failed to create inventory record: %w", err)
}
} else {
return nil, err
}
}
inventory.UpdateQuantity(delta)
if err := tx.Save(&inventory).Error; err != nil {
return nil, err
}
return &inventory, nil
}
func (r *PaymentRepositoryImpl) createInventoryMovementWithTransaction(tx *gorm.DB, movement *entities.InventoryMovement) error {
return tx.Create(movement).Error
}
// Helper function to create string pointer
func stringPtr(s string) *string {
return &s
}
+172
View File
@@ -0,0 +1,172 @@
package repository
import (
"apskel-pos-be/internal/entities"
"context"
"fmt"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type TableRepository struct {
db *gorm.DB
}
func NewTableRepository(db *gorm.DB) *TableRepository {
return &TableRepository{db: db}
}
func (r *TableRepository) Create(ctx context.Context, table *entities.Table) error {
return r.db.WithContext(ctx).Create(table).Error
}
func (r *TableRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Table, error) {
var table entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Order").
Where("id = ?", id).
First(&table).Error
if err != nil {
return nil, err
}
return &table, nil
}
func (r *TableRepository) GetByOutletID(ctx context.Context, outletID uuid.UUID) ([]entities.Table, error) {
var tables []entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Order").
Where("outlet_id = ?", outletID).
Order("table_name").
Find(&tables).Error
return tables, err
}
func (r *TableRepository) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]entities.Table, error) {
var tables []entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Order").
Where("organization_id = ?", organizationID).
Order("table_name").
Find(&tables).Error
return tables, err
}
func (r *TableRepository) Update(ctx context.Context, table *entities.Table) error {
return r.db.WithContext(ctx).Save(table).Error
}
func (r *TableRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Table{}, id).Error
}
func (r *TableRepository) List(ctx context.Context, organizationID, outletID *uuid.UUID, status *string, isActive *bool, search string, page, limit int) ([]entities.Table, int64, error) {
var tables []entities.Table
var total int64
query := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Order")
if organizationID != nil {
query = query.Where("organization_id = ?", *organizationID)
}
if outletID != nil {
query = query.Where("outlet_id = ?", *outletID)
}
if status != nil {
query = query.Where("status = ?", *status)
}
if isActive != nil {
query = query.Where("is_active = ?", *isActive)
}
if search != "" {
searchTerm := fmt.Sprintf("%%%s%%", search)
query = query.Where("table_name ILIKE ?", searchTerm)
}
// Count total
err := query.Model(&entities.Table{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Order("table_name").Find(&tables).Error
return tables, total, err
}
func (r *TableRepository) GetAvailableTables(ctx context.Context, outletID uuid.UUID) ([]entities.Table, error) {
var tables []entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Where("outlet_id = ? AND status = ? AND is_active = ?", outletID, "available", true).
Order("table_name").
Find(&tables).Error
return tables, err
}
func (r *TableRepository) GetOccupiedTables(ctx context.Context, outletID uuid.UUID) ([]entities.Table, error) {
var tables []entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Order").
Where("outlet_id = ? AND status = ? AND is_active = ?", outletID, "occupied", true).
Order("table_name").
Find(&tables).Error
return tables, err
}
func (r *TableRepository) OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error {
return r.db.WithContext(ctx).
Model(&entities.Table{}).
Where("id = ?", tableID).
Updates(map[string]interface{}{
"status": "occupied",
"order_id": orderID,
"start_time": startTime,
}).Error
}
func (r *TableRepository) ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error {
return r.db.WithContext(ctx).
Model(&entities.Table{}).
Where("id = ?", tableID).
Updates(map[string]interface{}{
"status": "available",
"order_id": nil,
"start_time": nil,
"payment_amount": paymentAmount,
}).Error
}
func (r *TableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error) {
var table entities.Table
err := r.db.WithContext(ctx).
Preload("Organization").
Preload("Outlet").
Preload("Order").
Where("order_id = ?", orderID).
First(&table).Error
if err != nil {
return nil, err
}
return &table, nil
}
+28 -11
View File
@@ -28,6 +28,7 @@ type Router struct {
customerHandler *handler.CustomerHandler
paymentMethodHandler *handler.PaymentMethodHandler
analyticsHandler *handler.AnalyticsHandler
tableHandler *handler.TableHandler
authMiddleware *middleware.AuthMiddleware
}
@@ -58,7 +59,8 @@ func NewRouter(cfg *config.Config,
customerValidator validator.CustomerValidator,
paymentMethodService service.PaymentMethodService,
paymentMethodValidator validator.PaymentMethodValidator,
analyticsService *service.AnalyticsServiceImpl) *Router {
analyticsService *service.AnalyticsServiceImpl,
tableService *service.TableService) *Router {
return &Router{
config: cfg,
@@ -76,6 +78,7 @@ func NewRouter(cfg *config.Config,
customerHandler: handler.NewCustomerHandler(customerService, customerValidator),
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
tableHandler: handler.NewTableHandler(tableService),
authMiddleware: authMiddleware,
}
}
@@ -220,16 +223,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
files.PUT("/:id", r.fileHandler.UpdateFile)
}
outlets := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager())
{
outlets.GET("/list", r.outletHandler.ListOutlets)
outlets.GET("/:id", r.outletHandler.GetOutlet)
outlets.PUT("/:id", r.outletHandler.UpdateOutlet)
outlets.GET("/printer-setting/:outlet_id", r.outletSettingHandler.GetPrinterSettings)
outlets.PUT("/printer-setting/:outlet_id", r.outletSettingHandler.UpdatePrinterSettings)
}
customers := protected.Group("/customers")
customers.Use(r.authMiddleware.RequireAdminOrManager())
{
@@ -252,6 +245,30 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
}
tables := protected.Group("/tables")
tables.Use(r.authMiddleware.RequireAdminOrManager())
{
tables.POST("", r.tableHandler.Create)
tables.GET("", r.tableHandler.List)
tables.GET("/:id", r.tableHandler.GetByID)
tables.PUT("/:id", r.tableHandler.Update)
tables.DELETE("/:id", r.tableHandler.Delete)
tables.POST("/:id/occupy", r.tableHandler.OccupyTable)
tables.POST("/:id/release", r.tableHandler.ReleaseTable)
}
outlets := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager())
{
outlets.GET("/list", r.outletHandler.ListOutlets)
outlets.GET("/:id", r.outletHandler.GetOutlet)
outlets.PUT("/:id", r.outletHandler.UpdateOutlet)
outlets.GET("/printer-setting/:outlet_id", r.outletSettingHandler.GetPrinterSettings)
outlets.PUT("/printer-setting/:outlet_id", r.outletSettingHandler.UpdatePrinterSettings)
outlets.GET("/:outlet_id/tables/available", r.tableHandler.GetAvailableTables)
outlets.GET("/:outlet_id/tables/occupied", r.tableHandler.GetOccupiedTables)
}
//outletPrinterSettings := protected.Group("/outlets/:outlet_id/settings")
//outletPrinterSettings.Use(r.authMiddleware.RequireAdminOrManager())
//{
+182
View File
@@ -0,0 +1,182 @@
package service
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/transformer"
"context"
"strconv"
"github.com/google/uuid"
)
type TableService struct {
tableProcessor *processor.TableProcessor
tableTransformer *transformer.TableTransformer
}
func NewTableService(tableProcessor *processor.TableProcessor, tableTransformer *transformer.TableTransformer) *TableService {
return &TableService{
tableProcessor: tableProcessor,
tableTransformer: tableTransformer,
}
}
func (s *TableService) Create(ctx context.Context, req contract.CreateTableRequest, organizationID uuid.UUID) (*contract.TableResponse, error) {
modelReq := models.CreateTableRequest{
OutletID: req.OutletID,
TableName: req.TableName,
PositionX: req.PositionX,
PositionY: req.PositionY,
Capacity: req.Capacity,
Metadata: req.Metadata,
}
response, err := s.tableProcessor.Create(ctx, modelReq, organizationID)
if err != nil {
return nil, err
}
return s.tableTransformer.ToContract(*response), nil
}
func (s *TableService) GetByID(ctx context.Context, id uuid.UUID) (*contract.TableResponse, error) {
response, err := s.tableProcessor.GetByID(ctx, id)
if err != nil {
return nil, err
}
return s.tableTransformer.ToContract(*response), nil
}
func (s *TableService) Update(ctx context.Context, id uuid.UUID, req contract.UpdateTableRequest) (*contract.TableResponse, error) {
modelReq := models.UpdateTableRequest{
TableName: req.TableName,
PositionX: req.PositionX,
PositionY: req.PositionY,
Capacity: req.Capacity,
IsActive: req.IsActive,
Metadata: req.Metadata,
}
if req.Status != nil {
status := models.TableStatus(*req.Status)
modelReq.Status = &status
}
response, err := s.tableProcessor.Update(ctx, id, modelReq)
if err != nil {
return nil, err
}
return s.tableTransformer.ToContract(*response), nil
}
func (s *TableService) Delete(ctx context.Context, id uuid.UUID) error {
return s.tableProcessor.Delete(ctx, id)
}
func (s *TableService) List(ctx context.Context, query contract.ListTablesQuery) (*contract.ListTablesResponse, error) {
req := models.ListTablesRequest{
Page: query.Page,
Limit: query.Limit,
Search: query.Search,
}
if query.OrganizationID != "" {
if orgID, err := uuid.Parse(query.OrganizationID); err == nil {
req.OrganizationID = &orgID
}
}
if query.OutletID != "" {
if outletID, err := uuid.Parse(query.OutletID); err == nil {
req.OutletID = &outletID
}
}
if query.Status != "" {
status := models.TableStatus(query.Status)
req.Status = &status
}
if query.IsActive != "" {
if isActive, err := strconv.ParseBool(query.IsActive); err == nil {
req.IsActive = &isActive
}
}
response, err := s.tableProcessor.List(ctx, req)
if err != nil {
return nil, err
}
contractTables := make([]contract.TableResponse, len(response.Tables))
for i, table := range response.Tables {
contractTables[i] = *s.tableTransformer.ToContract(table)
}
return &contract.ListTablesResponse{
Tables: contractTables,
TotalCount: response.TotalCount,
Page: response.Page,
Limit: response.Limit,
TotalPages: response.TotalPages,
}, nil
}
func (s *TableService) OccupyTable(ctx context.Context, tableID uuid.UUID, req contract.OccupyTableRequest) (*contract.TableResponse, error) {
modelReq := models.OccupyTableRequest{
OrderID: req.OrderID,
StartTime: req.StartTime,
}
response, err := s.tableProcessor.OccupyTable(ctx, tableID, modelReq)
if err != nil {
return nil, err
}
return s.tableTransformer.ToContract(*response), nil
}
func (s *TableService) ReleaseTable(ctx context.Context, tableID uuid.UUID, req contract.ReleaseTableRequest) (*contract.TableResponse, error) {
modelReq := models.ReleaseTableRequest{
PaymentAmount: req.PaymentAmount,
}
response, err := s.tableProcessor.ReleaseTable(ctx, tableID, modelReq)
if err != nil {
return nil, err
}
return s.tableTransformer.ToContract(*response), nil
}
func (s *TableService) GetAvailableTables(ctx context.Context, outletID uuid.UUID) ([]contract.TableResponse, error) {
tables, err := s.tableProcessor.GetAvailableTables(ctx, outletID)
if err != nil {
return nil, err
}
responses := make([]contract.TableResponse, len(tables))
for i, table := range tables {
responses[i] = *s.tableTransformer.ToContract(table)
}
return responses, nil
}
func (s *TableService) GetOccupiedTables(ctx context.Context, outletID uuid.UUID) ([]contract.TableResponse, error) {
tables, err := s.tableProcessor.GetOccupiedTables(ctx, outletID)
if err != nil {
return nil, err
}
responses := make([]contract.TableResponse, len(tables))
for i, table := range tables {
responses[i] = *s.tableTransformer.ToContract(table)
}
return responses, nil
}
+1
View File
@@ -19,4 +19,5 @@ type UserProcessor interface {
ChangePassword(ctx context.Context, userID uuid.UUID, req *models.ChangePasswordRequest) error
ActivateUser(ctx context.Context, userID uuid.UUID) error
DeactivateUser(ctx context.Context, userID uuid.UUID) error
UpdateUserOutlet(ctx context.Context, userID uuid.UUID, req *models.UpdateUserOutletRequest) (*models.UserResponse, error)
}
+12
View File
@@ -99,3 +99,15 @@ func (s *UserServiceImpl) ActivateUser(ctx context.Context, userID uuid.UUID) er
func (s *UserServiceImpl) DeactivateUser(ctx context.Context, userID uuid.UUID) error {
return s.userProcessor.DeactivateUser(ctx, userID)
}
func (s *UserServiceImpl) UpdateUserOutlet(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserOutletRequest) (*contract.UserResponse, error) {
modelReq := transformer.UpdateUserOutletRequestToModel(req)
userResponse, err := s.userProcessor.UpdateUserOutlet(ctx, userID, modelReq)
if err != nil {
return nil, err
}
contractResponse := transformer.UserModelResponseToResponse(userResponse)
return contractResponse, nil
}
+8 -9
View File
@@ -36,9 +36,6 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
if metadata == nil {
metadata = make(map[string]interface{})
}
if req.Image != nil {
metadata["image"] = *req.Image
}
return &models.CreateProductRequest{
OrganizationID: apctx.OrganizationID,
@@ -49,6 +46,8 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
Price: req.Price,
Cost: cost,
BusinessType: businessType,
ImageURL: req.ImageURL,
PrinterType: req.PrinterType,
Metadata: metadata,
Variants: variants,
}
@@ -59,9 +58,7 @@ func UpdateProductRequestToModel(req *contract.UpdateProductRequest) *models.Upd
if metadata == nil {
metadata = make(map[string]interface{})
}
if req.Image != nil {
metadata["image"] = *req.Image
}
return &models.UpdateProductRequest{
CategoryID: req.CategoryID,
SKU: req.SKU,
@@ -69,6 +66,8 @@ func UpdateProductRequestToModel(req *contract.UpdateProductRequest) *models.Upd
Description: req.Description,
Price: req.Price,
Cost: req.Cost,
ImageURL: req.ImageURL,
PrinterType: req.PrinterType,
Metadata: metadata,
IsActive: req.IsActive,
}
@@ -98,7 +97,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
}
}
response := &contract.ProductResponse{
return &contract.ProductResponse{
ID: prod.ID,
OrganizationID: prod.OrganizationID,
CategoryID: prod.CategoryID,
@@ -108,14 +107,14 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
Price: prod.Price,
Cost: prod.Cost,
BusinessType: string(prod.BusinessType),
ImageURL: prod.ImageURL,
PrinterType: prod.PrinterType,
Metadata: prod.Metadata,
IsActive: prod.IsActive,
CreatedAt: prod.CreatedAt,
UpdatedAt: prod.UpdatedAt,
Variants: variantResponses,
}
return response
}
// Slice conversions
+120
View File
@@ -0,0 +1,120 @@
package transformer
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
)
type TableTransformer struct{}
func NewTableTransformer() *TableTransformer {
return &TableTransformer{}
}
func (t *TableTransformer) ToContract(model models.TableResponse) *contract.TableResponse {
response := &contract.TableResponse{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
TableName: model.TableName,
StartTime: model.StartTime,
Status: string(model.Status),
OrderID: model.OrderID,
PaymentAmount: model.PaymentAmount,
PositionX: model.PositionX,
PositionY: model.PositionY,
Capacity: model.Capacity,
IsActive: model.IsActive,
Metadata: model.Metadata,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
if model.Order != nil {
response.Order = &contract.OrderResponse{
ID: model.Order.ID,
OrganizationID: model.Order.OrganizationID,
OutletID: model.Order.OutletID,
UserID: model.Order.UserID,
CustomerID: model.Order.CustomerID,
OrderNumber: model.Order.OrderNumber,
TableNumber: model.Order.TableNumber,
OrderType: string(model.Order.OrderType),
Status: string(model.Order.Status),
Subtotal: model.Order.Subtotal,
TaxAmount: model.Order.TaxAmount,
DiscountAmount: model.Order.DiscountAmount,
TotalAmount: model.Order.TotalAmount,
TotalCost: model.Order.TotalCost,
PaymentStatus: string(model.Order.PaymentStatus),
RefundAmount: model.Order.RefundAmount,
IsVoid: model.Order.IsVoid,
IsRefund: model.Order.IsRefund,
VoidReason: model.Order.VoidReason,
VoidedAt: model.Order.VoidedAt,
VoidedBy: model.Order.VoidedBy,
RefundReason: model.Order.RefundReason,
RefundedAt: model.Order.RefundedAt,
RefundedBy: model.Order.RefundedBy,
Metadata: model.Order.Metadata,
CreatedAt: model.Order.CreatedAt,
UpdatedAt: model.Order.UpdatedAt,
}
}
return response
}
func (t *TableTransformer) ToModel(contract contract.TableResponse) *models.TableResponse {
response := &models.TableResponse{
ID: contract.ID,
OrganizationID: contract.OrganizationID,
OutletID: contract.OutletID,
TableName: contract.TableName,
StartTime: contract.StartTime,
Status: models.TableStatus(contract.Status),
OrderID: contract.OrderID,
PaymentAmount: contract.PaymentAmount,
PositionX: contract.PositionX,
PositionY: contract.PositionY,
Capacity: contract.Capacity,
IsActive: contract.IsActive,
Metadata: contract.Metadata,
CreatedAt: contract.CreatedAt,
UpdatedAt: contract.UpdatedAt,
}
if contract.Order != nil {
response.Order = &models.OrderResponse{
ID: contract.Order.ID,
OrganizationID: contract.Order.OrganizationID,
OutletID: contract.Order.OutletID,
UserID: contract.Order.UserID,
CustomerID: contract.Order.CustomerID,
OrderNumber: contract.Order.OrderNumber,
TableNumber: contract.Order.TableNumber,
OrderType: models.OrderType(contract.Order.OrderType),
Status: models.OrderStatus(contract.Order.Status),
Subtotal: contract.Order.Subtotal,
TaxAmount: contract.Order.TaxAmount,
DiscountAmount: contract.Order.DiscountAmount,
TotalAmount: contract.Order.TotalAmount,
TotalCost: contract.Order.TotalCost,
PaymentStatus: models.PaymentStatus(contract.Order.PaymentStatus),
RefundAmount: contract.Order.RefundAmount,
IsVoid: contract.Order.IsVoid,
IsRefund: contract.Order.IsRefund,
VoidReason: contract.Order.VoidReason,
VoidedAt: contract.Order.VoidedAt,
VoidedBy: contract.Order.VoidedBy,
RefundReason: contract.Order.RefundReason,
RefundedAt: contract.Order.RefundedAt,
RefundedBy: contract.Order.RefundedBy,
Metadata: contract.Order.Metadata,
CreatedAt: contract.Order.CreatedAt,
UpdatedAt: contract.Order.UpdatedAt,
}
}
return response
}
+6
View File
@@ -51,6 +51,12 @@ func ChangePasswordRequestToModel(req *contract.ChangePasswordRequest) *models.C
}
}
func UpdateUserOutletRequestToModel(req *contract.UpdateUserOutletRequest) *models.UpdateUserOutletRequest {
return &models.UpdateUserOutletRequest{
OutletID: req.OutletID,
}
}
// Model to Contract conversions
func UserModelToResponse(user *models.User) *contract.UserResponse {
return &contract.UserResponse{
+18 -1
View File
@@ -55,6 +55,14 @@ func (v *ProductValidatorImpl) ValidateCreateProductRequest(req *contract.Create
return errors.New("description cannot exceed 1000 characters"), constants.MalformedFieldErrorCode
}
if req.ImageURL != nil && len(*req.ImageURL) > 500 {
return errors.New("image_url cannot exceed 500 characters"), constants.MalformedFieldErrorCode
}
if req.PrinterType != nil && len(*req.PrinterType) > 50 {
return errors.New("printer_type cannot exceed 50 characters"), constants.MalformedFieldErrorCode
}
return nil, ""
}
@@ -65,7 +73,8 @@ func (v *ProductValidatorImpl) ValidateUpdateProductRequest(req *contract.Update
// At least one field should be provided for update
if req.CategoryID == nil && req.SKU == nil && req.Name == nil && req.Description == nil &&
req.Price == nil && req.Cost == nil && req.BusinessType == nil && req.Metadata == nil && req.IsActive == nil {
req.Price == nil && req.Cost == nil && req.BusinessType == nil && req.ImageURL == nil &&
req.PrinterType == nil && req.Metadata == nil && req.IsActive == nil {
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
}
@@ -94,6 +103,14 @@ func (v *ProductValidatorImpl) ValidateUpdateProductRequest(req *contract.Update
return errors.New("description cannot exceed 1000 characters"), constants.MalformedFieldErrorCode
}
if req.ImageURL != nil && len(*req.ImageURL) > 500 {
return errors.New("image_url cannot exceed 500 characters"), constants.MalformedFieldErrorCode
}
if req.PrinterType != nil && len(*req.PrinterType) > 50 {
return errors.New("printer_type cannot exceed 50 characters"), constants.MalformedFieldErrorCode
}
return nil, ""
}
+173
View File
@@ -0,0 +1,173 @@
package validator
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
type TableValidator struct {
validate *validator.Validate
}
func NewTableValidator() *TableValidator {
return &TableValidator{
validate: validator.New(),
}
}
func (v *TableValidator) ValidateCreateTableRequest(req contract.CreateTableRequest) error {
if err := v.validate.Struct(req); err != nil {
return formatValidationError(err)
}
// Additional custom validations
if req.OutletID == uuid.Nil {
return fmt.Errorf("outlet_id is required")
}
if strings.TrimSpace(req.TableName) == "" {
return fmt.Errorf("table_name cannot be empty")
}
if req.Capacity < 1 || req.Capacity > 20 {
return fmt.Errorf("capacity must be between 1 and 20")
}
return nil
}
func (v *TableValidator) ValidateUpdateTableRequest(req contract.UpdateTableRequest) error {
if err := v.validate.Struct(req); err != nil {
return formatValidationError(err)
}
// Additional custom validations
if req.TableName != nil && strings.TrimSpace(*req.TableName) == "" {
return fmt.Errorf("table_name cannot be empty")
}
if req.Capacity != nil && (*req.Capacity < 1 || *req.Capacity > 20) {
return fmt.Errorf("capacity must be between 1 and 20")
}
if req.Status != nil {
if !isValidTableStatus(*req.Status) {
return fmt.Errorf("invalid table status: %s", *req.Status)
}
}
return nil
}
func (v *TableValidator) ValidateOccupyTableRequest(req contract.OccupyTableRequest) error {
if err := v.validate.Struct(req); err != nil {
return formatValidationError(err)
}
// Additional custom validations
if req.OrderID == uuid.Nil {
return fmt.Errorf("order_id is required")
}
return nil
}
func (v *TableValidator) ValidateReleaseTableRequest(req contract.ReleaseTableRequest) error {
if err := v.validate.Struct(req); err != nil {
return formatValidationError(err)
}
// Additional custom validations
if req.PaymentAmount < 0 {
return fmt.Errorf("payment_amount cannot be negative")
}
return nil
}
func (v *TableValidator) ValidateTableID(id string) error {
if id == "" {
return fmt.Errorf("table ID is required")
}
if _, err := uuid.Parse(id); err != nil {
return fmt.Errorf("invalid table ID format")
}
return nil
}
func (v *TableValidator) ValidateOutletID(id string) error {
if id == "" {
return fmt.Errorf("outlet ID is required")
}
if _, err := uuid.Parse(id); err != nil {
return fmt.Errorf("invalid outlet ID format")
}
return nil
}
func (v *TableValidator) ValidateListTablesQuery(query contract.ListTablesQuery) error {
// Validate pagination
if query.Page < 1 {
return fmt.Errorf("page must be greater than 0")
}
if query.Limit < 1 || query.Limit > 100 {
return fmt.Errorf("limit must be between 1 and 100")
}
// Validate organization_id if provided
if query.OrganizationID != "" {
if _, err := uuid.Parse(query.OrganizationID); err != nil {
return fmt.Errorf("invalid organization_id format")
}
}
// Validate outlet_id if provided
if query.OutletID != "" {
if _, err := uuid.Parse(query.OutletID); err != nil {
return fmt.Errorf("invalid outlet_id format")
}
}
// Validate status if provided
if query.Status != "" {
if !isValidTableStatus(query.Status) {
return fmt.Errorf("invalid table status: %s", query.Status)
}
}
// Validate is_active if provided
if query.IsActive != "" {
if query.IsActive != "true" && query.IsActive != "false" {
return fmt.Errorf("is_active must be 'true' or 'false'")
}
}
return nil
}
func isValidTableStatus(status string) bool {
validStatuses := []string{
string(constants.TableStatusAvailable),
string(constants.TableStatusOccupied),
string(constants.TableStatusReserved),
string(constants.TableStatusCleaning),
string(constants.TableStatusMaintenance),
}
for _, validStatus := range validStatuses {
if status == validStatus {
return true
}
}
return false
}
+7 -1
View File
@@ -138,7 +138,6 @@ func (v *UserValidatorImpl) ValidateUserID(userID uuid.UUID) (error, string) {
return nil, ""
}
func isValidUserRole(role string) bool {
validRoles := map[string]bool{
string(constants.RoleAdmin): true,
@@ -149,3 +148,10 @@ func isValidUserRole(role string) bool {
return validRoles[role]
}
func (v *UserValidatorImpl) ValidateUpdateUserOutletRequest(req *contract.UpdateUserOutletRequest) (error, string) {
if req.OutletID == uuid.Nil {
return errors.New("outlet_id is required"), constants.MissingFieldErrorCode
}
return nil, ""
}