Update users
This commit is contained in:
parent
96743cf50b
commit
ebe9999793
@ -76,6 +76,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
services.analyticsService,
|
||||
services.tableService,
|
||||
validators.tableValidator,
|
||||
services.unitService,
|
||||
services.ingredientService,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -138,9 +140,16 @@ type repositories struct {
|
||||
customerRepo *repository.CustomerRepository
|
||||
analyticsRepo *repository.AnalyticsRepositoryImpl
|
||||
tableRepo *repository.TableRepository
|
||||
unitRepo *repository.UnitRepository
|
||||
ingredientRepo *repository.IngredientRepository
|
||||
}
|
||||
|
||||
func (a *App) initRepositories() *repositories {
|
||||
sqlDB, err := a.db.DB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &repositories{
|
||||
userRepo: repository.NewUserRepository(a.db),
|
||||
organizationRepo: repository.NewOrganizationRepositoryImpl(a.db),
|
||||
@ -159,6 +168,8 @@ func (a *App) initRepositories() *repositories {
|
||||
customerRepo: repository.NewCustomerRepository(a.db),
|
||||
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
|
||||
tableRepo: repository.NewTableRepository(a.db),
|
||||
unitRepo: repository.NewUnitRepository(sqlDB),
|
||||
ingredientRepo: repository.NewIngredientRepository(sqlDB),
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +188,8 @@ type processors struct {
|
||||
customerProcessor *processor.CustomerProcessor
|
||||
analyticsProcessor *processor.AnalyticsProcessorImpl
|
||||
tableProcessor *processor.TableProcessor
|
||||
unitProcessor *processor.UnitProcessorImpl
|
||||
ingredientProcessor *processor.IngredientProcessorImpl
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
@ -197,6 +210,8 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
||||
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
|
||||
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
||||
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,6 +231,8 @@ type services struct {
|
||||
customerService service.CustomerService
|
||||
analyticsService *service.AnalyticsServiceImpl
|
||||
tableService *service.TableServiceImpl
|
||||
unitService *service.UnitServiceImpl
|
||||
ingredientService *service.IngredientServiceImpl
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, cfg *config.Config) *services {
|
||||
@ -235,6 +252,8 @@ func (a *App) initServices(processors *processors, cfg *config.Config) *services
|
||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||
analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor)
|
||||
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
|
||||
unitService := service.NewUnitService(processors.unitProcessor)
|
||||
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
|
||||
|
||||
return &services{
|
||||
userService: service.NewUserService(processors.userProcessor),
|
||||
@ -252,6 +271,8 @@ func (a *App) initServices(processors *processors, cfg *config.Config) *services
|
||||
customerService: customerService,
|
||||
analyticsService: analyticsService,
|
||||
tableService: tableService,
|
||||
unitService: unitService,
|
||||
ingredientService: ingredientService,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
internal/contract/ingredient_composition_contract.go
Normal file
16
internal/contract/ingredient_composition_contract.go
Normal file
@ -0,0 +1,16 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientCompositionContract interface {
|
||||
Create(request *models.CreateIngredientCompositionRequest, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
|
||||
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
|
||||
GetByParentIngredientID(parentIngredientID uuid.UUID, organizationID uuid.UUID) ([]*models.IngredientCompositionResponse, error)
|
||||
GetByChildIngredientID(childIngredientID uuid.UUID, organizationID uuid.UUID) ([]*models.IngredientCompositionResponse, error)
|
||||
Update(id uuid.UUID, request *models.UpdateIngredientCompositionRequest, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
|
||||
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||
}
|
||||
16
internal/contract/ingredient_contract.go
Normal file
16
internal/contract/ingredient_contract.go
Normal file
@ -0,0 +1,16 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientContract interface {
|
||||
Create(request *models.CreateIngredientRequest, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||
GetAll(organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||
Update(id uuid.UUID, request *models.UpdateIngredientRequest, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||
UpdateStock(id uuid.UUID, quantity float64, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||
}
|
||||
@ -9,6 +9,7 @@ import (
|
||||
type CreateOrderRequest struct {
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
||||
CustomerID *uuid.UUID `json:"customer_id"`
|
||||
TableNumber *string `json:"table_number,omitempty" validate:"omitempty,max=50"`
|
||||
OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"`
|
||||
|
||||
17
internal/contract/product_ingredient_contract.go
Normal file
17
internal/contract/product_ingredient_contract.go
Normal file
@ -0,0 +1,17 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductIngredientContract interface {
|
||||
Create(request *models.CreateProductIngredientRequest, organizationID uuid.UUID) (*models.ProductIngredientResponse, error)
|
||||
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.ProductIngredientResponse, error)
|
||||
GetByProductID(productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductIngredientResponse, error)
|
||||
GetByIngredientID(ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductIngredientResponse, error)
|
||||
Update(id uuid.UUID, request *models.UpdateProductIngredientRequest, organizationID uuid.UUID) (*models.ProductIngredientResponse, error)
|
||||
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||
BulkCreate(productID uuid.UUID, ingredients []models.CreateProductIngredientRequest, organizationID uuid.UUID) ([]*models.ProductIngredientResponse, error)
|
||||
}
|
||||
15
internal/contract/unit_contract.go
Normal file
15
internal/contract/unit_contract.go
Normal file
@ -0,0 +1,15 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitContract interface {
|
||||
Create(request *models.CreateUnitRequest, organizationID uuid.UUID) (*models.UnitResponse, error)
|
||||
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.UnitResponse, error)
|
||||
GetAll(organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error)
|
||||
Update(id uuid.UUID, request *models.UpdateUnitRequest, organizationID uuid.UUID) (*models.UnitResponse, error)
|
||||
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||
}
|
||||
25
internal/entities/ingredient.go
Normal file
25
internal/entities/ingredient.go
Normal file
@ -0,0 +1,25 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Ingredient struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id" db:"unit_id"`
|
||||
Cost float64 `json:"cost" db:"cost"`
|
||||
Stock float64 `json:"stock" db:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished" db:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
Metadata map[string]any `json:"metadata" db:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Unit *Unit `json:"unit,omitempty"`
|
||||
}
|
||||
22
internal/entities/ingredient_composition.go
Normal file
22
internal/entities/ingredient_composition.go
Normal file
@ -0,0 +1,22 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientComposition struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id" db:"parent_ingredient_id"`
|
||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id" db:"child_ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
// Relations
|
||||
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
||||
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
||||
}
|
||||
@ -38,13 +38,14 @@ 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"`
|
||||
ItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"item_id" validate:"required"`
|
||||
ItemType string `gorm:"not null;size:20" json:"item_type" validate:"required"` // "PRODUCT" or "INGREDIENT"
|
||||
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"`
|
||||
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
|
||||
PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
|
||||
NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
|
||||
UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
|
||||
TotalCost float64 `gorm:"type:decimal(12,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"`
|
||||
@ -57,7 +58,8 @@ type InventoryMovement struct {
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
Product *Product `gorm:"foreignKey:ItemID" json:"product,omitempty"`
|
||||
Ingredient *Ingredient `gorm:"foreignKey:ItemID" json:"ingredient,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"`
|
||||
|
||||
@ -8,27 +8,31 @@ import (
|
||||
)
|
||||
|
||||
type Product 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"`
|
||||
CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
|
||||
SKU *string `gorm:"size:100;index" json:"sku"`
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
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"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
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"`
|
||||
CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
|
||||
SKU *string `gorm:"size:100;index" json:"sku"`
|
||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `gorm:"type:text" json:"description"`
|
||||
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"`
|
||||
UnitID *uuid.UUID `gorm:"type:uuid;index" json:"unit_id"`
|
||||
HasIngredients bool `gorm:"default:false" json:"has_ingredients"`
|
||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||
ProductIngredients []ProductIngredient `gorm:"foreignKey:ProductID" json:"product_ingredients,omitempty"`
|
||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Product) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
22
internal/entities/product_ingredient.go
Normal file
22
internal/entities/product_ingredient.go
Normal file
@ -0,0 +1,22 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductIngredient struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||
}
|
||||
18
internal/entities/unit.go
Normal file
18
internal/entities/unit.go
Normal file
@ -0,0 +1,18 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Unit struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Abbreviation *string `json:"abbreviation" db:"abbreviation"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
177
internal/handler/ingredient_handler.go
Normal file
177
internal/handler/ingredient_handler.go
Normal file
@ -0,0 +1,177 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/util"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientHandler struct {
|
||||
ingredientService IngredientService
|
||||
}
|
||||
|
||||
func NewIngredientHandler(ingredientService IngredientService) *IngredientHandler {
|
||||
return &IngredientHandler{
|
||||
ingredientService: ingredientService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *IngredientHandler) Create(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var request models.CreateIngredientRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("IngredientHandler::Create -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
|
||||
return
|
||||
}
|
||||
|
||||
request.OrganizationID = contextInfo.OrganizationID
|
||||
|
||||
ingredientResponse, err := h.ingredientService.CreateIngredient(ctx, &request)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> Failed to create ingredient from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Create")
|
||||
}
|
||||
|
||||
func (h *IngredientHandler) GetByID(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Invalid ingredient ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
|
||||
return
|
||||
}
|
||||
|
||||
ingredientResponse, err := h.ingredientService.GetIngredientByID(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Failed to get ingredient from service")
|
||||
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Ingredient not found")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetByID")
|
||||
}
|
||||
|
||||
func (h *IngredientHandler) GetAll(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
// Get query parameters
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
search := c.Query("search")
|
||||
outletIDStr := c.Query("outlet_id")
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid page parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid limit parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||
return
|
||||
}
|
||||
|
||||
var outletID *uuid.UUID
|
||||
if outletIDStr != "" {
|
||||
parsedOutletID, err := uuid.Parse(outletIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid outlet ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||
return
|
||||
}
|
||||
outletID = &parsedOutletID
|
||||
}
|
||||
|
||||
ingredientResponse, err := h.ingredientService.ListIngredients(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Failed to get ingredients from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get ingredients")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetAll")
|
||||
}
|
||||
|
||||
func (h *IngredientHandler) Update(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Invalid ingredient ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
var request models.UpdateIngredientRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
ingredientResponse, err := h.ingredientService.UpdateIngredient(ctx, id, &request)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Failed to update ingredient from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Update")
|
||||
}
|
||||
|
||||
func (h *IngredientHandler) Delete(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Invalid ingredient ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.ingredientService.DeleteIngredient(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Failed to delete ingredient from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]interface{}{
|
||||
"message": "Ingredient deleted successfully",
|
||||
}), "IngredientHandler::Delete")
|
||||
}
|
||||
16
internal/handler/ingredient_service.go
Normal file
16
internal/handler/ingredient_service.go
Normal file
@ -0,0 +1,16 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientService interface {
|
||||
CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error)
|
||||
UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error)
|
||||
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
||||
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
|
||||
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||
}
|
||||
14
internal/handler/inventory_movement_service.go
Normal file
14
internal/handler/inventory_movement_service.go
Normal file
@ -0,0 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type InventoryMovementService interface {
|
||||
CreateInventoryMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error)
|
||||
GetInventoryMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error)
|
||||
ListInventoryMovements(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.InventoryMovementResponse], error)
|
||||
}
|
||||
200
internal/handler/unit_handler.go
Normal file
200
internal/handler/unit_handler.go
Normal file
@ -0,0 +1,200 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/util"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitHandler struct {
|
||||
unitService UnitService
|
||||
}
|
||||
|
||||
func NewUnitHandler(unitService UnitService) *UnitHandler {
|
||||
return &UnitHandler{
|
||||
unitService: unitService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *UnitHandler) Create(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var request models.CreateUnitRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("UnitHandler::Create -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Create")
|
||||
return
|
||||
}
|
||||
|
||||
request.OrganizationID = contextInfo.OrganizationID
|
||||
|
||||
unitResponse, err := h.unitService.CreateUnit(ctx, &request)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::Create -> Failed to create unit from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Create")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(unitResponse), "UnitHandler::Create")
|
||||
}
|
||||
|
||||
func (h *UnitHandler) GetByID(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::GetByID -> Invalid unit ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid unit ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::GetByID")
|
||||
return
|
||||
}
|
||||
|
||||
unitResponse, err := h.unitService.GetUnitByID(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::GetByID -> Failed to get unit from service")
|
||||
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Unit not found")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::GetByID")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(unitResponse), "UnitHandler::GetByID")
|
||||
}
|
||||
|
||||
func (h *UnitHandler) GetAll(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
// Get query parameters
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
search := c.Query("search")
|
||||
outletIDStr := c.Query("outlet_id")
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::GetAll -> Invalid page parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::GetAll")
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::GetAll -> Invalid limit parameter")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::GetAll")
|
||||
return
|
||||
}
|
||||
|
||||
var outletID *uuid.UUID
|
||||
if outletIDStr != "" {
|
||||
parsedOutletID, err := uuid.Parse(outletIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::GetAll -> Invalid outlet ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::GetAll")
|
||||
return
|
||||
}
|
||||
outletID = &parsedOutletID
|
||||
}
|
||||
|
||||
unitResponse, err := h.unitService.ListUnits(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::GetAll -> Failed to get units from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get units")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::GetAll")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(unitResponse), "UnitHandler::GetAll")
|
||||
}
|
||||
|
||||
// UpdateUnit godoc
|
||||
// @Summary Update unit
|
||||
// @Description Update an existing unit
|
||||
// @Tags units
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Unit ID"
|
||||
// @Param request body models.UpdateUnitRequest true "Update unit request"
|
||||
// @Success 200 {object} contract.Response{data=models.UnitResponse}
|
||||
// @Failure 400 {object} contract.Response
|
||||
// @Failure 404 {object} contract.Response
|
||||
// @Failure 500 {object} contract.Response
|
||||
// @Router /units/{id} [put]
|
||||
func (h *UnitHandler) Update(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::Update -> Invalid unit ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid unit ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
var request models.UpdateUnitRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::Update -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
unitResponse, err := h.unitService.UpdateUnit(ctx, id, &request)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::Update -> Failed to update unit from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(unitResponse), "UnitHandler::Update")
|
||||
}
|
||||
|
||||
// DeleteUnit godoc
|
||||
// @Summary Delete unit
|
||||
// @Description Delete a unit by its ID
|
||||
// @Tags units
|
||||
// @Produce json
|
||||
// @Param id path string true "Unit ID"
|
||||
// @Success 200 {object} contract.Response
|
||||
// @Failure 404 {object} contract.Response
|
||||
// @Failure 500 {object} contract.Response
|
||||
// @Router /units/{id} [delete]
|
||||
func (h *UnitHandler) Delete(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::Delete -> Invalid unit ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid unit ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.unitService.DeleteUnit(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("UnitHandler::Delete -> Failed to delete unit from service")
|
||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "UnitHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]interface{}{
|
||||
"message": "Unit deleted successfully",
|
||||
}), "UnitHandler::Delete")
|
||||
}
|
||||
16
internal/handler/unit_service.go
Normal file
16
internal/handler/unit_service.go
Normal file
@ -0,0 +1,16 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitService interface {
|
||||
CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error)
|
||||
UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error)
|
||||
DeleteUnit(ctx context.Context, id uuid.UUID) error
|
||||
GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error)
|
||||
ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error)
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@ -294,20 +295,8 @@ func (h *UserHandler) DeactivateUser(c *gin.Context) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.UpdateUserOutletRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@ -316,14 +305,7 @@ func (h *UserHandler) UpdateUserOutlet(c *gin.Context) {
|
||||
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)
|
||||
userResponse, err := h.userService.UpdateUserOutlet(c.Request.Context(), contextInfo.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)
|
||||
|
||||
70
internal/mappers/ingredient_composition_mapper.go
Normal file
70
internal/mappers/ingredient_composition_mapper.go
Normal file
@ -0,0 +1,70 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func MapIngredientCompositionEntityToModel(entity *entities.IngredientComposition) *models.IngredientComposition {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.IngredientComposition{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ParentIngredientID: entity.ParentIngredientID,
|
||||
ChildIngredientID: entity.ChildIngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
ParentIngredient: MapIngredientEntityToModel(entity.ParentIngredient),
|
||||
ChildIngredient: MapIngredientEntityToModel(entity.ChildIngredient),
|
||||
}
|
||||
}
|
||||
|
||||
func MapIngredientCompositionModelToEntity(model *models.IngredientComposition) *entities.IngredientComposition {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.IngredientComposition{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ParentIngredientID: model.ParentIngredientID,
|
||||
ChildIngredientID: model.ChildIngredientID,
|
||||
Quantity: model.Quantity,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
ParentIngredient: MapIngredientModelToEntity(model.ParentIngredient),
|
||||
ChildIngredient: MapIngredientModelToEntity(model.ChildIngredient),
|
||||
}
|
||||
}
|
||||
|
||||
func MapIngredientCompositionEntitiesToModels(entities []*entities.IngredientComposition) []*models.IngredientComposition {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*models.IngredientComposition, len(entities))
|
||||
for i, entity := range entities {
|
||||
models[i] = MapIngredientCompositionEntityToModel(entity)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func MapIngredientCompositionModelsToEntities(models []*models.IngredientComposition) []*entities.IngredientComposition {
|
||||
if models == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entities := make([]*entities.IngredientComposition, len(models))
|
||||
for i, model := range models {
|
||||
entities[i] = MapIngredientCompositionModelToEntity(model)
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
76
internal/mappers/ingredient_mapper.go
Normal file
76
internal/mappers/ingredient_mapper.go
Normal file
@ -0,0 +1,76 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func MapIngredientEntityToModel(entity *entities.Ingredient) *models.Ingredient {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.Ingredient{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
Name: entity.Name,
|
||||
UnitID: entity.UnitID,
|
||||
Cost: entity.Cost,
|
||||
Stock: entity.Stock,
|
||||
IsSemiFinished: entity.IsSemiFinished,
|
||||
IsActive: entity.IsActive,
|
||||
Metadata: entity.Metadata,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Unit: MapUnitEntityToModel(entity.Unit),
|
||||
}
|
||||
}
|
||||
|
||||
func MapIngredientModelToEntity(model *models.Ingredient) *entities.Ingredient {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.Ingredient{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
Name: model.Name,
|
||||
UnitID: model.UnitID,
|
||||
Cost: model.Cost,
|
||||
Stock: model.Stock,
|
||||
IsSemiFinished: model.IsSemiFinished,
|
||||
IsActive: model.IsActive,
|
||||
Metadata: model.Metadata,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Unit: MapUnitModelToEntity(model.Unit),
|
||||
}
|
||||
}
|
||||
|
||||
func MapIngredientEntitiesToModels(entities []*entities.Ingredient) []*models.Ingredient {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*models.Ingredient, len(entities))
|
||||
for i, entity := range entities {
|
||||
models[i] = MapIngredientEntityToModel(entity)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func MapIngredientModelsToEntities(models []*models.Ingredient) []*entities.Ingredient {
|
||||
if models == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entities := make([]*entities.Ingredient, len(models))
|
||||
for i, model := range models {
|
||||
entities[i] = MapIngredientModelToEntity(model)
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
@ -14,11 +14,12 @@ func InventoryMovementEntityToModel(entity *entities.InventoryMovement) *models.
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
ItemID: entity.ItemID,
|
||||
ItemType: entity.ItemType,
|
||||
MovementType: models.InventoryMovementType(entity.MovementType),
|
||||
Quantity: entity.Quantity,
|
||||
PreviousQuantity: entity.PreviousQuantity,
|
||||
NewQuantity: entity.NewQuantity,
|
||||
Quantity: int(entity.Quantity),
|
||||
PreviousQuantity: int(entity.PreviousQuantity),
|
||||
NewQuantity: int(entity.NewQuantity),
|
||||
UnitCost: entity.UnitCost,
|
||||
TotalCost: entity.TotalCost,
|
||||
ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType),
|
||||
@ -42,11 +43,12 @@ func InventoryMovementModelToEntity(model *models.InventoryMovement) *entities.I
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ProductID: model.ProductID,
|
||||
ItemID: model.ItemID,
|
||||
ItemType: model.ItemType,
|
||||
MovementType: entities.InventoryMovementType(model.MovementType),
|
||||
Quantity: model.Quantity,
|
||||
PreviousQuantity: model.PreviousQuantity,
|
||||
NewQuantity: model.NewQuantity,
|
||||
Quantity: float64(model.Quantity),
|
||||
PreviousQuantity: float64(model.PreviousQuantity),
|
||||
NewQuantity: float64(model.NewQuantity),
|
||||
UnitCost: model.UnitCost,
|
||||
TotalCost: model.TotalCost,
|
||||
ReferenceType: (*entities.InventoryMovementReferenceType)(model.ReferenceType),
|
||||
@ -70,11 +72,12 @@ func InventoryMovementEntityToResponse(entity *entities.InventoryMovement) *mode
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
ItemID: entity.ItemID,
|
||||
ItemType: entity.ItemType,
|
||||
MovementType: models.InventoryMovementType(entity.MovementType),
|
||||
Quantity: entity.Quantity,
|
||||
PreviousQuantity: entity.PreviousQuantity,
|
||||
NewQuantity: entity.NewQuantity,
|
||||
Quantity: int(entity.Quantity),
|
||||
PreviousQuantity: int(entity.PreviousQuantity),
|
||||
NewQuantity: int(entity.NewQuantity),
|
||||
UnitCost: entity.UnitCost,
|
||||
TotalCost: entity.TotalCost,
|
||||
ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType),
|
||||
@ -116,11 +119,12 @@ func CreateInventoryMovementRequestToEntity(req *models.CreateInventoryMovementR
|
||||
return &entities.InventoryMovement{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
ProductID: req.ProductID,
|
||||
ItemID: req.ItemID,
|
||||
ItemType: req.ItemType,
|
||||
MovementType: entities.InventoryMovementType(req.MovementType),
|
||||
Quantity: req.Quantity,
|
||||
PreviousQuantity: previousQuantity,
|
||||
NewQuantity: newQuantity,
|
||||
Quantity: float64(req.Quantity),
|
||||
PreviousQuantity: float64(previousQuantity),
|
||||
NewQuantity: float64(newQuantity),
|
||||
UnitCost: req.UnitCost,
|
||||
TotalCost: float64(req.Quantity) * req.UnitCost,
|
||||
ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType),
|
||||
|
||||
70
internal/mappers/product_ingredient_mapper.go
Normal file
70
internal/mappers/product_ingredient_mapper.go
Normal file
@ -0,0 +1,70 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *models.ProductIngredient {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.ProductIngredient{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
ProductID: entity.ProductID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Product: ProductEntityToModel(entity.Product),
|
||||
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||
}
|
||||
}
|
||||
|
||||
func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entities.ProductIngredient {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.ProductIngredient{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
ProductID: model.ProductID,
|
||||
IngredientID: model.IngredientID,
|
||||
Quantity: model.Quantity,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Product: ProductModelToEntity(model.Product),
|
||||
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||
}
|
||||
}
|
||||
|
||||
func MapProductIngredientEntitiesToModels(entities []*entities.ProductIngredient) []*models.ProductIngredient {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*models.ProductIngredient, len(entities))
|
||||
for i, entity := range entities {
|
||||
models[i] = MapProductIngredientEntityToModel(entity)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func MapProductIngredientModelsToEntities(models []*models.ProductIngredient) []*entities.ProductIngredient {
|
||||
if models == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entities := make([]*entities.ProductIngredient, len(models))
|
||||
for i, model := range models {
|
||||
entities[i] = MapProductIngredientModelToEntity(model)
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
66
internal/mappers/unit_mapper.go
Normal file
66
internal/mappers/unit_mapper.go
Normal file
@ -0,0 +1,66 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
)
|
||||
|
||||
func MapUnitEntityToModel(entity *entities.Unit) *models.Unit {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.Unit{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
Name: entity.Name,
|
||||
Abbreviation: entity.Abbreviation,
|
||||
IsActive: entity.IsActive,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func MapUnitModelToEntity(model *models.Unit) *entities.Unit {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.Unit{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
Name: model.Name,
|
||||
Abbreviation: model.Abbreviation,
|
||||
IsActive: model.IsActive,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func MapUnitEntitiesToModels(entities []*entities.Unit) []*models.Unit {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*models.Unit, len(entities))
|
||||
for i, entity := range entities {
|
||||
models[i] = MapUnitEntityToModel(entity)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func MapUnitModelsToEntities(models []*models.Unit) []*entities.Unit {
|
||||
if models == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entities := make([]*entities.Unit, len(models))
|
||||
for i, model := range models {
|
||||
entities[i] = MapUnitModelToEntity(model)
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
66
internal/models/ingredient.go
Normal file
66
internal/models/ingredient.go
Normal file
@ -0,0 +1,66 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Ingredient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Cost float64 `json:"cost"`
|
||||
Stock float64 `json:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Unit *Unit `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
type CreateIngredientRequest struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||
Cost float64 `json:"cost" validate:"min=0"`
|
||||
Stock float64 `json:"stock" validate:"min=0"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type UpdateIngredientRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||
Cost float64 `json:"cost" validate:"min=0"`
|
||||
Stock float64 `json:"stock" validate:"min=0"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type IngredientResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
UnitID uuid.UUID `json:"unit_id"`
|
||||
Cost float64 `json:"cost"`
|
||||
Stock float64 `json:"stock"`
|
||||
IsSemiFinished bool `json:"is_semi_finished"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Unit *Unit `json:"unit,omitempty"`
|
||||
}
|
||||
49
internal/models/ingredient_composition.go
Normal file
49
internal/models/ingredient_composition.go
Normal file
@ -0,0 +1,49 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientComposition struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
|
||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
||||
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
||||
}
|
||||
|
||||
type CreateIngredientCompositionRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id" validate:"required"`
|
||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type UpdateIngredientCompositionRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type IngredientCompositionResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
|
||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
||||
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
||||
}
|
||||
@ -37,7 +37,8 @@ type InventoryMovement struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
ItemID uuid.UUID
|
||||
ItemType string
|
||||
MovementType InventoryMovementType
|
||||
Quantity int
|
||||
PreviousQuantity int
|
||||
@ -58,7 +59,8 @@ type InventoryMovement struct {
|
||||
type CreateInventoryMovementRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
ItemID uuid.UUID
|
||||
ItemType string
|
||||
MovementType InventoryMovementType
|
||||
Quantity int
|
||||
UnitCost float64
|
||||
@ -76,7 +78,8 @@ type InventoryMovementResponse struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
ItemID uuid.UUID
|
||||
ItemType string
|
||||
MovementType InventoryMovementType
|
||||
Quantity int
|
||||
PreviousQuantity int
|
||||
@ -98,7 +101,8 @@ type InventoryMovementResponse struct {
|
||||
type ListInventoryMovementsRequest struct {
|
||||
OrganizationID *uuid.UUID
|
||||
OutletID *uuid.UUID
|
||||
ProductID *uuid.UUID
|
||||
ItemID *uuid.UUID
|
||||
ItemType *string
|
||||
MovementType *InventoryMovementType
|
||||
ReferenceType *InventoryMovementReferenceType
|
||||
ReferenceID *uuid.UUID
|
||||
|
||||
@ -19,6 +19,8 @@ type Product struct {
|
||||
BusinessType constants.BusinessType
|
||||
ImageURL *string
|
||||
PrinterType string
|
||||
UnitID *uuid.UUID
|
||||
HasIngredients bool
|
||||
Metadata map[string]interface{}
|
||||
IsActive bool
|
||||
CreatedAt time.Time
|
||||
@ -47,6 +49,8 @@ type CreateProductRequest struct {
|
||||
BusinessType constants.BusinessType `validate:"required"`
|
||||
ImageURL *string `validate:"omitempty,max=500"`
|
||||
PrinterType *string `validate:"omitempty,max=50"`
|
||||
UnitID *uuid.UUID `validate:"omitempty"`
|
||||
HasIngredients bool `validate:"omitempty"`
|
||||
Metadata map[string]interface{}
|
||||
Variants []CreateProductVariantRequest `validate:"omitempty,dive"`
|
||||
// Stock management fields
|
||||
@ -56,16 +60,18 @@ type CreateProductRequest struct {
|
||||
}
|
||||
|
||||
type UpdateProductRequest struct {
|
||||
CategoryID *uuid.UUID
|
||||
SKU *string `validate:"omitempty,max=100"`
|
||||
Name *string `validate:"omitempty,min=1,max=255"`
|
||||
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
|
||||
CategoryID *uuid.UUID `validate:"omitempty"`
|
||||
SKU *string `validate:"omitempty,max=100"`
|
||||
Name *string `validate:"omitempty,min=1,max=255"`
|
||||
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"`
|
||||
UnitID *uuid.UUID `validate:"omitempty"`
|
||||
HasIngredients *bool `validate:"omitempty"`
|
||||
Metadata map[string]interface{}
|
||||
IsActive *bool
|
||||
// Stock management fields
|
||||
ReorderLevel *int `validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
|
||||
}
|
||||
@ -97,6 +103,8 @@ type ProductResponse struct {
|
||||
BusinessType constants.BusinessType
|
||||
ImageURL *string
|
||||
PrinterType string
|
||||
UnitID *uuid.UUID
|
||||
HasIngredients bool
|
||||
Metadata map[string]interface{}
|
||||
IsActive bool
|
||||
CreatedAt time.Time
|
||||
|
||||
49
internal/models/product_ingredient.go
Normal file
49
internal/models/product_ingredient.go
Normal file
@ -0,0 +1,49 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductIngredient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||
}
|
||||
|
||||
type CreateProductIngredientRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type UpdateProductIngredientRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type ProductIngredientResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Product *Product `json:"product,omitempty"`
|
||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||
}
|
||||
44
internal/models/unit.go
Normal file
44
internal/models/unit.go
Normal file
@ -0,0 +1,44 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Unit struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
Abbreviation *string `json:"abbreviation"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateUnitRequest struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=50"`
|
||||
Abbreviation *string `json:"abbreviation" validate:"omitempty,max=10"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UpdateUnitRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=50"`
|
||||
Abbreviation *string `json:"abbreviation" validate:"omitempty,max=10"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type UnitResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Name string `json:"name"`
|
||||
Abbreviation *string `json:"abbreviation"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
238
internal/processor/ingredient_processor.go
Normal file
238
internal/processor/ingredient_processor.go
Normal file
@ -0,0 +1,238 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientProcessorImpl struct {
|
||||
ingredientRepo IngredientRepository
|
||||
unitRepo UnitRepository
|
||||
}
|
||||
|
||||
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository) *IngredientProcessorImpl {
|
||||
return &IngredientProcessorImpl{
|
||||
ingredientRepo: ingredientRepo,
|
||||
unitRepo: unitRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
||||
// Validate unit exists
|
||||
_, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create ingredient entity
|
||||
ingredient := &entities.Ingredient{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
Name: req.Name,
|
||||
UnitID: req.UnitID,
|
||||
Cost: req.Cost,
|
||||
Stock: req.Stock,
|
||||
IsSemiFinished: req.IsSemiFinished,
|
||||
IsActive: req.IsActive,
|
||||
Metadata: req.Metadata,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err = p.ingredientRepo.Create(ctx, ingredient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get with relations
|
||||
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, ingredient.ID, req.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map to response
|
||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
||||
response := &models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||
// For now, we'll need to get organizationID from context or request
|
||||
// This is a limitation of the current interface design
|
||||
organizationID := uuid.Nil // This should come from context
|
||||
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredient)
|
||||
response := &models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error) {
|
||||
// Set default values
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
ingredients, total, err := p.ingredientRepo.GetAll(ctx, organizationID, outletID, page, limit, search, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map to response models
|
||||
ingredientModels := mappers.MapIngredientEntitiesToModels(ingredients)
|
||||
ingredientResponses := make([]models.IngredientResponse, len(ingredientModels))
|
||||
|
||||
for i, ingredientModel := range ingredientModels {
|
||||
ingredientResponses[i] = models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
}
|
||||
|
||||
// Create paginated response
|
||||
paginatedResponse := &models.PaginatedResponse[models.IngredientResponse]{
|
||||
Data: ingredientResponses,
|
||||
Pagination: models.Pagination{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Total: int64(total),
|
||||
TotalPages: (total + limit - 1) / limit,
|
||||
},
|
||||
}
|
||||
|
||||
return paginatedResponse, nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
||||
// For now, we'll need to get organizationID from context or request
|
||||
// This is a limitation of the current interface design
|
||||
organizationID := uuid.Nil // This should come from context
|
||||
|
||||
// Get existing ingredient
|
||||
existingIngredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate unit exists if changed
|
||||
if req.UnitID != existingIngredient.UnitID {
|
||||
_, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update fields
|
||||
existingIngredient.OutletID = req.OutletID
|
||||
existingIngredient.Name = req.Name
|
||||
existingIngredient.UnitID = req.UnitID
|
||||
existingIngredient.Cost = req.Cost
|
||||
existingIngredient.Stock = req.Stock
|
||||
existingIngredient.IsSemiFinished = req.IsSemiFinished
|
||||
existingIngredient.IsActive = req.IsActive
|
||||
existingIngredient.Metadata = req.Metadata
|
||||
existingIngredient.UpdatedAt = time.Now()
|
||||
|
||||
// Save to database
|
||||
err = p.ingredientRepo.Update(ctx, existingIngredient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get with relations
|
||||
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map to response
|
||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
||||
response := &models.IngredientResponse{
|
||||
ID: ingredientModel.ID,
|
||||
OrganizationID: ingredientModel.OrganizationID,
|
||||
OutletID: ingredientModel.OutletID,
|
||||
Name: ingredientModel.Name,
|
||||
UnitID: ingredientModel.UnitID,
|
||||
Cost: ingredientModel.Cost,
|
||||
Stock: ingredientModel.Stock,
|
||||
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||
IsActive: ingredientModel.IsActive,
|
||||
Metadata: ingredientModel.Metadata,
|
||||
CreatedAt: ingredientModel.CreatedAt,
|
||||
UpdatedAt: ingredientModel.UpdatedAt,
|
||||
Unit: ingredientModel.Unit,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *IngredientProcessorImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
||||
// For now, we'll need to get organizationID from context or request
|
||||
// This is a limitation of the current interface design
|
||||
organizationID := uuid.Nil // This should come from context
|
||||
|
||||
err := p.ingredientRepo.Delete(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
17
internal/processor/ingredient_repository.go
Normal file
17
internal/processor/ingredient_repository.go
Normal file
@ -0,0 +1,17 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientRepository interface {
|
||||
Create(ctx context.Context, ingredient *entities.Ingredient) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error)
|
||||
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error)
|
||||
Update(ctx context.Context, ingredient *entities.Ingredient) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error
|
||||
}
|
||||
@ -47,8 +47,8 @@ func NewInventoryMovementProcessorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error) {
|
||||
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID)
|
||||
func (p *InventoryMovementProcessorImpl) CreateInventoryMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error) {
|
||||
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ItemID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current inventory: %w", err)
|
||||
}
|
||||
@ -59,11 +59,12 @@ func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req
|
||||
movement := &entities.InventoryMovement{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
ProductID: req.ProductID,
|
||||
ItemID: req.ItemID,
|
||||
ItemType: req.ItemType,
|
||||
MovementType: entities.InventoryMovementType(req.MovementType),
|
||||
Quantity: req.Quantity,
|
||||
PreviousQuantity: previousQuantity,
|
||||
NewQuantity: newQuantity,
|
||||
Quantity: float64(req.Quantity),
|
||||
PreviousQuantity: float64(previousQuantity),
|
||||
NewQuantity: float64(newQuantity),
|
||||
UnitCost: req.UnitCost,
|
||||
TotalCost: float64(req.Quantity) * req.UnitCost,
|
||||
ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType),
|
||||
@ -89,7 +90,7 @@ func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *InventoryMovementProcessorImpl) GetMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error) {
|
||||
func (p *InventoryMovementProcessorImpl) GetInventoryMovementByID(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)
|
||||
@ -99,44 +100,29 @@ func (p *InventoryMovementProcessorImpl) GetMovementByID(ctx context.Context, id
|
||||
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
|
||||
func (p *InventoryMovementProcessorImpl) ListInventoryMovements(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.InventoryMovementResponse], error) {
|
||||
// Set default values
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if req.OutletID != nil {
|
||||
filters["outlet_id"] = *req.OutletID
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
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
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
movements, total, err := p.movementRepo.List(ctx, filters, req.Limit, offset)
|
||||
filters := make(map[string]interface{})
|
||||
filters["organization_id"] = organizationID
|
||||
if outletID != nil {
|
||||
filters["outlet_id"] = *outletID
|
||||
}
|
||||
if search != "" {
|
||||
filters["search"] = search
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
movements, total, err := p.movementRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list movements: %w", err)
|
||||
}
|
||||
@ -150,19 +136,18 @@ func (p *InventoryMovementProcessorImpl) ListMovements(ctx context.Context, req
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := int(total) / req.Limit
|
||||
if int(total)%req.Limit > 0 {
|
||||
totalPages++
|
||||
// Create paginated response
|
||||
paginatedResponse := &models.PaginatedResponse[models.InventoryMovementResponse]{
|
||||
Data: movementResponses,
|
||||
Pagination: models.Pagination{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Total: total,
|
||||
TotalPages: int((total + int64(limit) - 1) / int64(limit)),
|
||||
},
|
||||
}
|
||||
|
||||
return &models.ListInventoryMovementsResponse{
|
||||
Movements: movementResponses,
|
||||
TotalCount: int(total),
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
return paginatedResponse, nil
|
||||
}
|
||||
|
||||
func (p *InventoryMovementProcessorImpl) GetMovementsByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) (*models.ListInventoryMovementsResponse, error) {
|
||||
|
||||
166
internal/processor/unit_processor.go
Normal file
166
internal/processor/unit_processor.go
Normal file
@ -0,0 +1,166 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitProcessorImpl struct {
|
||||
unitRepo UnitRepository
|
||||
}
|
||||
|
||||
func NewUnitProcessor(unitRepo UnitRepository) *UnitProcessorImpl {
|
||||
return &UnitProcessorImpl{
|
||||
unitRepo: unitRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UnitProcessorImpl) CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error) {
|
||||
unit := &entities.Unit{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
Name: req.Name,
|
||||
Abbreviation: req.Abbreviation,
|
||||
IsActive: req.IsActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := p.unitRepo.Create(ctx, unit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unitModel := mappers.MapUnitEntityToModel(unit)
|
||||
response := &models.UnitResponse{
|
||||
ID: unitModel.ID,
|
||||
OrganizationID: unitModel.OrganizationID,
|
||||
OutletID: unitModel.OutletID,
|
||||
Name: unitModel.Name,
|
||||
Abbreviation: unitModel.Abbreviation,
|
||||
IsActive: unitModel.IsActive,
|
||||
CreatedAt: unitModel.CreatedAt,
|
||||
UpdatedAt: unitModel.UpdatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *UnitProcessorImpl) GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error) {
|
||||
organizationID := uuid.Nil // This should come from context
|
||||
|
||||
unit, err := p.unitRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unitModel := mappers.MapUnitEntityToModel(unit)
|
||||
response := &models.UnitResponse{
|
||||
ID: unitModel.ID,
|
||||
OrganizationID: unitModel.OrganizationID,
|
||||
OutletID: unitModel.OutletID,
|
||||
Name: unitModel.Name,
|
||||
Abbreviation: unitModel.Abbreviation,
|
||||
IsActive: unitModel.IsActive,
|
||||
CreatedAt: unitModel.CreatedAt,
|
||||
UpdatedAt: unitModel.UpdatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *UnitProcessorImpl) ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
units, total, err := p.unitRepo.GetAll(ctx, organizationID, outletID, page, limit, search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unitModels := mappers.MapUnitEntitiesToModels(units)
|
||||
unitResponses := make([]models.UnitResponse, len(unitModels))
|
||||
|
||||
for i, unitModel := range unitModels {
|
||||
unitResponses[i] = models.UnitResponse{
|
||||
ID: unitModel.ID,
|
||||
OrganizationID: unitModel.OrganizationID,
|
||||
OutletID: unitModel.OutletID,
|
||||
Name: unitModel.Name,
|
||||
Abbreviation: unitModel.Abbreviation,
|
||||
IsActive: unitModel.IsActive,
|
||||
CreatedAt: unitModel.CreatedAt,
|
||||
UpdatedAt: unitModel.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
paginatedResponse := &models.PaginatedResponse[models.UnitResponse]{
|
||||
Data: unitResponses,
|
||||
Pagination: models.Pagination{
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Total: int64(total),
|
||||
TotalPages: (total + limit - 1) / limit,
|
||||
},
|
||||
}
|
||||
|
||||
return paginatedResponse, nil
|
||||
}
|
||||
|
||||
func (p *UnitProcessorImpl) UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error) {
|
||||
organizationID := uuid.Nil
|
||||
|
||||
existingUnit, err := p.unitRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingUnit.OutletID = req.OutletID
|
||||
existingUnit.Name = req.Name
|
||||
existingUnit.Abbreviation = req.Abbreviation
|
||||
existingUnit.IsActive = req.IsActive
|
||||
existingUnit.UpdatedAt = time.Now()
|
||||
|
||||
err = p.unitRepo.Update(ctx, existingUnit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unitModel := mappers.MapUnitEntityToModel(existingUnit)
|
||||
response := &models.UnitResponse{
|
||||
ID: unitModel.ID,
|
||||
OrganizationID: unitModel.OrganizationID,
|
||||
OutletID: unitModel.OutletID,
|
||||
Name: unitModel.Name,
|
||||
Abbreviation: unitModel.Abbreviation,
|
||||
IsActive: unitModel.IsActive,
|
||||
CreatedAt: unitModel.CreatedAt,
|
||||
UpdatedAt: unitModel.UpdatedAt,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *UnitProcessorImpl) DeleteUnit(ctx context.Context, id uuid.UUID) error {
|
||||
organizationID := uuid.Nil
|
||||
|
||||
err := p.unitRepo.Delete(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
153
internal/processor/unit_processor_test.go
Normal file
153
internal/processor/unit_processor_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockUnitRepository is a mock implementation of the unit repository
|
||||
type MockUnitRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockUnitRepository) Create(ctx context.Context, unit *models.Unit) error {
|
||||
args := m.Called(ctx, unit)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUnitRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.Unit, error) {
|
||||
args := m.Called(ctx, id, organizationID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*models.Unit), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUnitRepository) GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*models.Unit, int, error) {
|
||||
args := m.Called(ctx, organizationID, outletID, page, limit, search)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Int(1), args.Error(2)
|
||||
}
|
||||
return args.Get(0).([]*models.Unit), args.Int(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *MockUnitRepository) Update(ctx context.Context, unit *models.Unit) error {
|
||||
args := m.Called(ctx, unit)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUnitRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
args := m.Called(ctx, id, organizationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestUnitProcessor_Create(t *testing.T) {
|
||||
// Create mock repository
|
||||
mockRepo := &MockUnitRepository{}
|
||||
|
||||
// Create processor
|
||||
processor := NewUnitProcessor(mockRepo)
|
||||
|
||||
// Test data
|
||||
organizationID := uuid.New()
|
||||
request := &models.CreateUnitRequest{
|
||||
Name: "Gram",
|
||||
Abbreviation: "g",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*models.Unit")).Return(nil)
|
||||
|
||||
// Execute
|
||||
result, err := processor.Create(request, organizationID)
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, request.Name, result.Name)
|
||||
assert.Equal(t, request.Abbreviation, result.Abbreviation)
|
||||
assert.Equal(t, request.IsActive, result.IsActive)
|
||||
assert.Equal(t, organizationID, result.OrganizationID)
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUnitProcessor_GetByID(t *testing.T) {
|
||||
// Create mock repository
|
||||
mockRepo := &MockUnitRepository{}
|
||||
|
||||
// Create processor
|
||||
processor := NewUnitProcessor(mockRepo)
|
||||
|
||||
// Test data
|
||||
unitID := uuid.New()
|
||||
organizationID := uuid.New()
|
||||
expectedUnit := &models.Unit{
|
||||
ID: unitID,
|
||||
OrganizationID: organizationID,
|
||||
Name: "Gram",
|
||||
Abbreviation: "g",
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
mockRepo.On("GetByID", mock.Anything, unitID, organizationID).Return(expectedUnit, nil)
|
||||
|
||||
// Execute
|
||||
result, err := processor.GetByID(unitID, organizationID)
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, expectedUnit.ID, result.ID)
|
||||
assert.Equal(t, expectedUnit.Name, result.Name)
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUnitProcessor_GetAll(t *testing.T) {
|
||||
// Create mock repository
|
||||
mockRepo := &MockUnitRepository{}
|
||||
|
||||
// Create processor
|
||||
processor := NewUnitProcessor(mockRepo)
|
||||
|
||||
// Test data
|
||||
organizationID := uuid.New()
|
||||
expectedUnits := []*models.Unit{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
Name: "Gram",
|
||||
Abbreviation: "g",
|
||||
IsActive: true,
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
Name: "Liter",
|
||||
Abbreviation: "L",
|
||||
IsActive: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
mockRepo.On("GetAll", mock.Anything, organizationID, (*uuid.UUID)(nil), 1, 10, "").Return(expectedUnits, 2, nil)
|
||||
|
||||
// Execute
|
||||
result, err := processor.GetAll(organizationID, nil, 1, 10, "")
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Len(t, result.Data, 2)
|
||||
assert.Equal(t, 2, result.Pagination.Total)
|
||||
|
||||
mockRepo.AssertExpectations(t)
|
||||
}
|
||||
16
internal/processor/unit_repository.go
Normal file
16
internal/processor/unit_repository.go
Normal file
@ -0,0 +1,16 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitRepository interface {
|
||||
Create(ctx context.Context, unit *entities.Unit) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error)
|
||||
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error)
|
||||
Update(ctx context.Context, unit *entities.Unit) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
}
|
||||
287
internal/repository/ingredient_composition_repository.go
Normal file
287
internal/repository/ingredient_composition_repository.go
Normal file
@ -0,0 +1,287 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientCompositionRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewIngredientCompositionRepository(db *sql.DB) *IngredientCompositionRepository {
|
||||
return &IngredientCompositionRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *IngredientCompositionRepository) Create(ctx context.Context, composition *entities.IngredientComposition) error {
|
||||
query := `
|
||||
INSERT INTO ingredient_compositions (id, organization_id, outlet_id, parent_ingredient_id, child_ingredient_id, quantity, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
composition.ID,
|
||||
composition.OrganizationID,
|
||||
composition.OutletID,
|
||||
composition.ParentIngredientID,
|
||||
composition.ChildIngredientID,
|
||||
composition.Quantity,
|
||||
composition.CreatedAt,
|
||||
composition.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *IngredientCompositionRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientComposition, error) {
|
||||
query := `
|
||||
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
||||
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
||||
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
||||
FROM ingredient_compositions ic
|
||||
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
||||
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
||||
WHERE ic.id = $1 AND ic.organization_id = $2
|
||||
`
|
||||
|
||||
composition := &entities.IngredientComposition{}
|
||||
parentIngredient := &entities.Ingredient{}
|
||||
childIngredient := &entities.Ingredient{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id, organizationID).Scan(
|
||||
&composition.ID,
|
||||
&composition.OrganizationID,
|
||||
&composition.OutletID,
|
||||
&composition.ParentIngredientID,
|
||||
&composition.ChildIngredientID,
|
||||
&composition.Quantity,
|
||||
&composition.CreatedAt,
|
||||
&composition.UpdatedAt,
|
||||
&parentIngredient.ID,
|
||||
&parentIngredient.OrganizationID,
|
||||
&parentIngredient.OutletID,
|
||||
&parentIngredient.Name,
|
||||
&parentIngredient.UnitID,
|
||||
&parentIngredient.Cost,
|
||||
&parentIngredient.Stock,
|
||||
&parentIngredient.IsSemiFinished,
|
||||
&parentIngredient.IsActive,
|
||||
&parentIngredient.Metadata,
|
||||
&parentIngredient.CreatedAt,
|
||||
&parentIngredient.UpdatedAt,
|
||||
&childIngredient.ID,
|
||||
&childIngredient.OrganizationID,
|
||||
&childIngredient.OutletID,
|
||||
&childIngredient.Name,
|
||||
&childIngredient.UnitID,
|
||||
&childIngredient.Cost,
|
||||
&childIngredient.Stock,
|
||||
&childIngredient.IsSemiFinished,
|
||||
&childIngredient.IsActive,
|
||||
&childIngredient.Metadata,
|
||||
&childIngredient.CreatedAt,
|
||||
&childIngredient.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
composition.ParentIngredient = parentIngredient
|
||||
composition.ChildIngredient = childIngredient
|
||||
return composition, nil
|
||||
}
|
||||
|
||||
func (r *IngredientCompositionRepository) GetByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error) {
|
||||
query := `
|
||||
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
||||
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
||||
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
||||
FROM ingredient_compositions ic
|
||||
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
||||
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
||||
WHERE ic.parent_ingredient_id = $1 AND ic.organization_id = $2
|
||||
ORDER BY ic.created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, parentIngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var compositions []*entities.IngredientComposition
|
||||
for rows.Next() {
|
||||
composition := &entities.IngredientComposition{}
|
||||
parentIngredient := &entities.Ingredient{}
|
||||
childIngredient := &entities.Ingredient{}
|
||||
|
||||
err := rows.Scan(
|
||||
&composition.ID,
|
||||
&composition.OrganizationID,
|
||||
&composition.OutletID,
|
||||
&composition.ParentIngredientID,
|
||||
&composition.ChildIngredientID,
|
||||
&composition.Quantity,
|
||||
&composition.CreatedAt,
|
||||
&composition.UpdatedAt,
|
||||
&parentIngredient.ID,
|
||||
&parentIngredient.OrganizationID,
|
||||
&parentIngredient.OutletID,
|
||||
&parentIngredient.Name,
|
||||
&parentIngredient.UnitID,
|
||||
&parentIngredient.Cost,
|
||||
&parentIngredient.Stock,
|
||||
&parentIngredient.IsSemiFinished,
|
||||
&parentIngredient.IsActive,
|
||||
&parentIngredient.Metadata,
|
||||
&parentIngredient.CreatedAt,
|
||||
&parentIngredient.UpdatedAt,
|
||||
&childIngredient.ID,
|
||||
&childIngredient.OrganizationID,
|
||||
&childIngredient.OutletID,
|
||||
&childIngredient.Name,
|
||||
&childIngredient.UnitID,
|
||||
&childIngredient.Cost,
|
||||
&childIngredient.Stock,
|
||||
&childIngredient.IsSemiFinished,
|
||||
&childIngredient.IsActive,
|
||||
&childIngredient.Metadata,
|
||||
&childIngredient.CreatedAt,
|
||||
&childIngredient.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
composition.ParentIngredient = parentIngredient
|
||||
composition.ChildIngredient = childIngredient
|
||||
compositions = append(compositions, composition)
|
||||
}
|
||||
|
||||
return compositions, nil
|
||||
}
|
||||
|
||||
func (r *IngredientCompositionRepository) GetByChildIngredientID(ctx context.Context, childIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error) {
|
||||
query := `
|
||||
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
||||
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
||||
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
||||
FROM ingredient_compositions ic
|
||||
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
||||
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
||||
WHERE ic.child_ingredient_id = $1 AND ic.organization_id = $2
|
||||
ORDER BY ic.created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, childIngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var compositions []*entities.IngredientComposition
|
||||
for rows.Next() {
|
||||
composition := &entities.IngredientComposition{}
|
||||
parentIngredient := &entities.Ingredient{}
|
||||
childIngredient := &entities.Ingredient{}
|
||||
|
||||
err := rows.Scan(
|
||||
&composition.ID,
|
||||
&composition.OrganizationID,
|
||||
&composition.OutletID,
|
||||
&composition.ParentIngredientID,
|
||||
&composition.ChildIngredientID,
|
||||
&composition.Quantity,
|
||||
&composition.CreatedAt,
|
||||
&composition.UpdatedAt,
|
||||
&parentIngredient.ID,
|
||||
&parentIngredient.OrganizationID,
|
||||
&parentIngredient.OutletID,
|
||||
&parentIngredient.Name,
|
||||
&parentIngredient.UnitID,
|
||||
&parentIngredient.Cost,
|
||||
&parentIngredient.Stock,
|
||||
&parentIngredient.IsSemiFinished,
|
||||
&parentIngredient.IsActive,
|
||||
&parentIngredient.Metadata,
|
||||
&parentIngredient.CreatedAt,
|
||||
&parentIngredient.UpdatedAt,
|
||||
&childIngredient.ID,
|
||||
&childIngredient.OrganizationID,
|
||||
&childIngredient.OutletID,
|
||||
&childIngredient.Name,
|
||||
&childIngredient.UnitID,
|
||||
&childIngredient.Cost,
|
||||
&childIngredient.Stock,
|
||||
&childIngredient.IsSemiFinished,
|
||||
&childIngredient.IsActive,
|
||||
&childIngredient.Metadata,
|
||||
&childIngredient.CreatedAt,
|
||||
&childIngredient.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
composition.ParentIngredient = parentIngredient
|
||||
composition.ChildIngredient = childIngredient
|
||||
compositions = append(compositions, composition)
|
||||
}
|
||||
|
||||
return compositions, nil
|
||||
}
|
||||
|
||||
func (r *IngredientCompositionRepository) Update(ctx context.Context, composition *entities.IngredientComposition) error {
|
||||
query := `
|
||||
UPDATE ingredient_compositions
|
||||
SET outlet_id = $1, quantity = $2, updated_at = $3
|
||||
WHERE id = $4 AND organization_id = $5
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query,
|
||||
composition.OutletID,
|
||||
composition.Quantity,
|
||||
composition.UpdatedAt,
|
||||
composition.ID,
|
||||
composition.OrganizationID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *IngredientCompositionRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
query := `DELETE FROM ingredient_compositions WHERE id = $1 AND organization_id = $2`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
258
internal/repository/ingredient_repository.go
Normal file
258
internal/repository/ingredient_repository.go
Normal file
@ -0,0 +1,258 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewIngredientRepository(db *sql.DB) *IngredientRepository {
|
||||
return &IngredientRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *IngredientRepository) Create(ctx context.Context, ingredient *entities.Ingredient) error {
|
||||
query := `
|
||||
INSERT INTO ingredients (id, organization_id, outlet_id, name, unit_id, cost, stock, is_semi_finished, is_active, metadata, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
ingredient.ID,
|
||||
ingredient.OrganizationID,
|
||||
ingredient.OutletID,
|
||||
ingredient.Name,
|
||||
ingredient.UnitID,
|
||||
ingredient.Cost,
|
||||
ingredient.Stock,
|
||||
ingredient.IsSemiFinished,
|
||||
ingredient.IsActive,
|
||||
ingredient.Metadata,
|
||||
ingredient.CreatedAt,
|
||||
ingredient.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *IngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error) {
|
||||
query := `
|
||||
SELECT i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at,
|
||||
u.id, u.organization_id, u.outlet_id, u.name, u.abbreviation, u.is_active, u.created_at, u.updated_at
|
||||
FROM ingredients i
|
||||
LEFT JOIN units u ON i.unit_id = u.id
|
||||
WHERE i.id = $1 AND i.organization_id = $2
|
||||
`
|
||||
|
||||
ingredient := &entities.Ingredient{}
|
||||
unit := &entities.Unit{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id, organizationID).Scan(
|
||||
&ingredient.ID,
|
||||
&ingredient.OrganizationID,
|
||||
&ingredient.OutletID,
|
||||
&ingredient.Name,
|
||||
&ingredient.UnitID,
|
||||
&ingredient.Cost,
|
||||
&ingredient.Stock,
|
||||
&ingredient.IsSemiFinished,
|
||||
&ingredient.IsActive,
|
||||
&ingredient.Metadata,
|
||||
&ingredient.CreatedAt,
|
||||
&ingredient.UpdatedAt,
|
||||
&unit.ID,
|
||||
&unit.OrganizationID,
|
||||
&unit.OutletID,
|
||||
&unit.Name,
|
||||
&unit.Abbreviation,
|
||||
&unit.IsActive,
|
||||
&unit.CreatedAt,
|
||||
&unit.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ingredient.Unit = unit
|
||||
return ingredient, nil
|
||||
}
|
||||
|
||||
func (r *IngredientRepository) GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Build WHERE clause
|
||||
whereClause := "WHERE i.organization_id = $1"
|
||||
args := []interface{}{organizationID}
|
||||
argCount := 1
|
||||
|
||||
if outletID != nil {
|
||||
argCount++
|
||||
whereClause += fmt.Sprintf(" AND i.outlet_id = $%d", argCount)
|
||||
args = append(args, *outletID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
argCount++
|
||||
whereClause += fmt.Sprintf(" AND i.name ILIKE $%d", argCount)
|
||||
args = append(args, "%"+search+"%")
|
||||
}
|
||||
|
||||
if isSemiFinished != nil {
|
||||
argCount++
|
||||
whereClause += fmt.Sprintf(" AND i.is_semi_finished = $%d", argCount)
|
||||
args = append(args, *isSemiFinished)
|
||||
}
|
||||
|
||||
// Count query
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM ingredients i %s", whereClause)
|
||||
var total int
|
||||
err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Data query
|
||||
argCount++
|
||||
query := fmt.Sprintf(`
|
||||
SELECT i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at,
|
||||
u.id, u.organization_id, u.outlet_id, u.name, u.abbreviation, u.is_active, u.created_at, u.updated_at
|
||||
FROM ingredients i
|
||||
LEFT JOIN units u ON i.unit_id = u.id
|
||||
%s
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT $%d OFFSET $%d
|
||||
`, whereClause, argCount, argCount+1)
|
||||
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ingredients []*entities.Ingredient
|
||||
for rows.Next() {
|
||||
ingredient := &entities.Ingredient{}
|
||||
unit := &entities.Unit{}
|
||||
|
||||
err := rows.Scan(
|
||||
&ingredient.ID,
|
||||
&ingredient.OrganizationID,
|
||||
&ingredient.OutletID,
|
||||
&ingredient.Name,
|
||||
&ingredient.UnitID,
|
||||
&ingredient.Cost,
|
||||
&ingredient.Stock,
|
||||
&ingredient.IsSemiFinished,
|
||||
&ingredient.IsActive,
|
||||
&ingredient.Metadata,
|
||||
&ingredient.CreatedAt,
|
||||
&ingredient.UpdatedAt,
|
||||
&unit.ID,
|
||||
&unit.OrganizationID,
|
||||
&unit.OutletID,
|
||||
&unit.Name,
|
||||
&unit.Abbreviation,
|
||||
&unit.IsActive,
|
||||
&unit.CreatedAt,
|
||||
&unit.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
ingredient.Unit = unit
|
||||
ingredients = append(ingredients, ingredient)
|
||||
}
|
||||
|
||||
return ingredients, total, nil
|
||||
}
|
||||
|
||||
func (r *IngredientRepository) Update(ctx context.Context, ingredient *entities.Ingredient) error {
|
||||
query := `
|
||||
UPDATE ingredients
|
||||
SET outlet_id = $1, name = $2, unit_id = $3, cost = $4, stock = $5, is_semi_finished = $6, is_active = $7, metadata = $8, updated_at = $9
|
||||
WHERE id = $10 AND organization_id = $11
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query,
|
||||
ingredient.OutletID,
|
||||
ingredient.Name,
|
||||
ingredient.UnitID,
|
||||
ingredient.Cost,
|
||||
ingredient.Stock,
|
||||
ingredient.IsSemiFinished,
|
||||
ingredient.IsActive,
|
||||
ingredient.Metadata,
|
||||
ingredient.UpdatedAt,
|
||||
ingredient.ID,
|
||||
ingredient.OrganizationID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *IngredientRepository) UpdateStock(ctx context.Context, id uuid.UUID, quantity float64, organizationID uuid.UUID) error {
|
||||
query := `
|
||||
UPDATE ingredients
|
||||
SET stock = stock + $1, updated_at = NOW()
|
||||
WHERE id = $2 AND organization_id = $3
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, quantity, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *IngredientRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
query := `DELETE FROM ingredients WHERE id = $1 AND organization_id = $2`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -150,11 +150,12 @@ func (r *PaymentRepositoryImpl) CreatePaymentWithInventoryMovement(ctx context.C
|
||||
movement := &entities.InventoryMovement{
|
||||
OrganizationID: order.OrganizationID,
|
||||
OutletID: order.OutletID,
|
||||
ProductID: item.ProductID,
|
||||
ItemID: item.ProductID,
|
||||
ItemType: "PRODUCT",
|
||||
MovementType: entities.InventoryMovementTypeSale,
|
||||
Quantity: -item.Quantity,
|
||||
PreviousQuantity: updatedInventory.Quantity + item.Quantity, // Add back the quantity that was subtracted
|
||||
NewQuantity: updatedInventory.Quantity,
|
||||
Quantity: float64(-item.Quantity),
|
||||
PreviousQuantity: float64(updatedInventory.Quantity + item.Quantity), // Add back the quantity that was subtracted
|
||||
NewQuantity: float64(updatedInventory.Quantity),
|
||||
UnitCost: item.UnitCost,
|
||||
TotalCost: float64(item.Quantity) * item.UnitCost,
|
||||
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||
@ -222,11 +223,12 @@ func (r *PaymentRepositoryImpl) RefundPaymentWithInventoryMovement(ctx context.C
|
||||
movement := &entities.InventoryMovement{
|
||||
OrganizationID: order.OrganizationID,
|
||||
OutletID: order.OutletID,
|
||||
ProductID: item.ProductID,
|
||||
ItemID: item.ProductID,
|
||||
ItemType: "PRODUCT",
|
||||
MovementType: entities.InventoryMovementTypeRefund,
|
||||
Quantity: refundedQuantity,
|
||||
PreviousQuantity: updatedInventory.Quantity - refundedQuantity, // Subtract the quantity that was added
|
||||
NewQuantity: updatedInventory.Quantity,
|
||||
Quantity: float64(refundedQuantity),
|
||||
PreviousQuantity: float64(updatedInventory.Quantity - refundedQuantity), // Subtract the quantity that was added
|
||||
NewQuantity: float64(updatedInventory.Quantity),
|
||||
UnitCost: item.UnitCost,
|
||||
TotalCost: float64(refundedQuantity) * item.UnitCost,
|
||||
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||
|
||||
309
internal/repository/product_ingredient_repository.go
Normal file
309
internal/repository/product_ingredient_repository.go
Normal file
@ -0,0 +1,309 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductIngredientRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewProductIngredientRepository(db *sql.DB) *ProductIngredientRepository {
|
||||
return &ProductIngredientRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) Create(ctx context.Context, productIngredient *entities.ProductIngredient) error {
|
||||
query := `
|
||||
INSERT INTO product_ingredients (id, organization_id, outlet_id, product_id, ingredient_id, quantity, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
productIngredient.ID,
|
||||
productIngredient.OrganizationID,
|
||||
productIngredient.OutletID,
|
||||
productIngredient.ProductID,
|
||||
productIngredient.IngredientID,
|
||||
productIngredient.Quantity,
|
||||
productIngredient.CreatedAt,
|
||||
productIngredient.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductIngredient, error) {
|
||||
query := `
|
||||
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||
FROM product_ingredients pi
|
||||
LEFT JOIN products p ON pi.product_id = p.id
|
||||
LEFT JOIN ingredients i ON pi.ingredient_id = i.id
|
||||
WHERE pi.id = $1 AND pi.organization_id = $2
|
||||
`
|
||||
|
||||
productIngredient := &entities.ProductIngredient{}
|
||||
product := &entities.Product{}
|
||||
ingredient := &entities.Ingredient{}
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query, id, organizationID).Scan(
|
||||
&productIngredient.ID,
|
||||
&productIngredient.OrganizationID,
|
||||
&productIngredient.OutletID,
|
||||
&productIngredient.ProductID,
|
||||
&productIngredient.IngredientID,
|
||||
&productIngredient.Quantity,
|
||||
&productIngredient.CreatedAt,
|
||||
&productIngredient.UpdatedAt,
|
||||
&product.ID,
|
||||
&product.OrganizationID,
|
||||
&product.CategoryID,
|
||||
&product.SKU,
|
||||
&product.Name,
|
||||
&product.Description,
|
||||
&product.Price,
|
||||
&product.Cost,
|
||||
&product.BusinessType,
|
||||
&product.ImageURL,
|
||||
&product.PrinterType,
|
||||
&product.UnitID,
|
||||
&product.HasIngredients,
|
||||
&product.Metadata,
|
||||
&product.IsActive,
|
||||
&product.CreatedAt,
|
||||
&product.UpdatedAt,
|
||||
&ingredient.ID,
|
||||
&ingredient.OrganizationID,
|
||||
&ingredient.OutletID,
|
||||
&ingredient.Name,
|
||||
&ingredient.UnitID,
|
||||
&ingredient.Cost,
|
||||
&ingredient.Stock,
|
||||
&ingredient.IsSemiFinished,
|
||||
&ingredient.IsActive,
|
||||
&ingredient.Metadata,
|
||||
&ingredient.CreatedAt,
|
||||
&ingredient.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
productIngredient.Product = product
|
||||
productIngredient.Ingredient = ingredient
|
||||
return productIngredient, nil
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) {
|
||||
query := `
|
||||
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||
FROM product_ingredients pi
|
||||
LEFT JOIN products p ON pi.product_id = p.id
|
||||
LEFT JOIN ingredients i ON pi.ingredient_id = i.id
|
||||
WHERE pi.product_id = $1 AND pi.organization_id = $2
|
||||
ORDER BY pi.created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, productID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var productIngredients []*entities.ProductIngredient
|
||||
for rows.Next() {
|
||||
productIngredient := &entities.ProductIngredient{}
|
||||
product := &entities.Product{}
|
||||
ingredient := &entities.Ingredient{}
|
||||
|
||||
err := rows.Scan(
|
||||
&productIngredient.ID,
|
||||
&productIngredient.OrganizationID,
|
||||
&productIngredient.OutletID,
|
||||
&productIngredient.ProductID,
|
||||
&productIngredient.IngredientID,
|
||||
&productIngredient.Quantity,
|
||||
&productIngredient.CreatedAt,
|
||||
&productIngredient.UpdatedAt,
|
||||
&product.ID,
|
||||
&product.OrganizationID,
|
||||
&product.CategoryID,
|
||||
&product.SKU,
|
||||
&product.Name,
|
||||
&product.Description,
|
||||
&product.Price,
|
||||
&product.Cost,
|
||||
&product.BusinessType,
|
||||
&product.ImageURL,
|
||||
&product.PrinterType,
|
||||
&product.UnitID,
|
||||
&product.HasIngredients,
|
||||
&product.Metadata,
|
||||
&product.IsActive,
|
||||
&product.CreatedAt,
|
||||
&product.UpdatedAt,
|
||||
&ingredient.ID,
|
||||
&ingredient.OrganizationID,
|
||||
&ingredient.OutletID,
|
||||
&ingredient.Name,
|
||||
&ingredient.UnitID,
|
||||
&ingredient.Cost,
|
||||
&ingredient.Stock,
|
||||
&ingredient.IsSemiFinished,
|
||||
&ingredient.IsActive,
|
||||
&ingredient.Metadata,
|
||||
&ingredient.CreatedAt,
|
||||
&ingredient.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
productIngredient.Product = product
|
||||
productIngredient.Ingredient = ingredient
|
||||
productIngredients = append(productIngredients, productIngredient)
|
||||
}
|
||||
|
||||
return productIngredients, nil
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) {
|
||||
query := `
|
||||
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||
FROM product_ingredients pi
|
||||
LEFT JOIN products p ON pi.product_id = p.id
|
||||
LEFT JOIN ingredients i ON pi.ingredient_id = i.id
|
||||
WHERE pi.ingredient_id = $1 AND pi.organization_id = $2
|
||||
ORDER BY pi.created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var productIngredients []*entities.ProductIngredient
|
||||
for rows.Next() {
|
||||
productIngredient := &entities.ProductIngredient{}
|
||||
product := &entities.Product{}
|
||||
ingredient := &entities.Ingredient{}
|
||||
|
||||
err := rows.Scan(
|
||||
&productIngredient.ID,
|
||||
&productIngredient.OrganizationID,
|
||||
&productIngredient.OutletID,
|
||||
&productIngredient.ProductID,
|
||||
&productIngredient.IngredientID,
|
||||
&productIngredient.Quantity,
|
||||
&productIngredient.CreatedAt,
|
||||
&productIngredient.UpdatedAt,
|
||||
&product.ID,
|
||||
&product.OrganizationID,
|
||||
&product.CategoryID,
|
||||
&product.SKU,
|
||||
&product.Name,
|
||||
&product.Description,
|
||||
&product.Price,
|
||||
&product.Cost,
|
||||
&product.BusinessType,
|
||||
&product.ImageURL,
|
||||
&product.PrinterType,
|
||||
&product.UnitID,
|
||||
&product.HasIngredients,
|
||||
&product.Metadata,
|
||||
&product.IsActive,
|
||||
&product.CreatedAt,
|
||||
&product.UpdatedAt,
|
||||
&ingredient.ID,
|
||||
&ingredient.OrganizationID,
|
||||
&ingredient.OutletID,
|
||||
&ingredient.Name,
|
||||
&ingredient.UnitID,
|
||||
&ingredient.Cost,
|
||||
&ingredient.Stock,
|
||||
&ingredient.IsSemiFinished,
|
||||
&ingredient.IsActive,
|
||||
&ingredient.Metadata,
|
||||
&ingredient.CreatedAt,
|
||||
&ingredient.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
productIngredient.Product = product
|
||||
productIngredient.Ingredient = ingredient
|
||||
productIngredients = append(productIngredients, productIngredient)
|
||||
}
|
||||
|
||||
return productIngredients, nil
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) Update(ctx context.Context, productIngredient *entities.ProductIngredient) error {
|
||||
query := `
|
||||
UPDATE product_ingredients
|
||||
SET outlet_id = $1, quantity = $2, updated_at = $3
|
||||
WHERE id = $4 AND organization_id = $5
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query,
|
||||
productIngredient.OutletID,
|
||||
productIngredient.Quantity,
|
||||
productIngredient.UpdatedAt,
|
||||
productIngredient.ID,
|
||||
productIngredient.OrganizationID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
query := `DELETE FROM product_ingredients WHERE id = $1 AND organization_id = $2`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProductIngredientRepository) DeleteByProductID(ctx context.Context, productID, organizationID uuid.UUID) error {
|
||||
query := `DELETE FROM product_ingredients WHERE product_id = $1 AND organization_id = $2`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query, productID, organizationID)
|
||||
return err
|
||||
}
|
||||
185
internal/repository/unit_repository.go
Normal file
185
internal/repository/unit_repository.go
Normal file
@ -0,0 +1,185 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewUnitRepository(db *sql.DB) *UnitRepository {
|
||||
return &UnitRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UnitRepository) Create(ctx context.Context, unit *entities.Unit) error {
|
||||
query := `
|
||||
INSERT INTO units (id, organization_id, outlet_id, name, abbreviation, is_active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query,
|
||||
unit.ID,
|
||||
unit.OrganizationID,
|
||||
unit.OutletID,
|
||||
unit.Name,
|
||||
unit.Abbreviation,
|
||||
unit.IsActive,
|
||||
unit.CreatedAt,
|
||||
unit.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *UnitRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error) {
|
||||
query := `
|
||||
SELECT id, organization_id, outlet_id, name, abbreviation, is_active, created_at, updated_at
|
||||
FROM units
|
||||
WHERE id = $1 AND organization_id = $2
|
||||
`
|
||||
|
||||
unit := &entities.Unit{}
|
||||
err := r.db.QueryRowContext(ctx, query, id, organizationID).Scan(
|
||||
&unit.ID,
|
||||
&unit.OrganizationID,
|
||||
&unit.OutletID,
|
||||
&unit.Name,
|
||||
&unit.Abbreviation,
|
||||
&unit.IsActive,
|
||||
&unit.CreatedAt,
|
||||
&unit.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unit, nil
|
||||
}
|
||||
|
||||
func (r *UnitRepository) GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Build WHERE clause
|
||||
whereClause := "WHERE organization_id = $1"
|
||||
args := []interface{}{organizationID}
|
||||
argCount := 1
|
||||
|
||||
if outletID != nil {
|
||||
argCount++
|
||||
whereClause += fmt.Sprintf(" AND outlet_id = $%d", argCount)
|
||||
args = append(args, *outletID)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
argCount++
|
||||
whereClause += fmt.Sprintf(" AND (name ILIKE $%d OR abbreviation ILIKE $%d)", argCount, argCount)
|
||||
args = append(args, "%"+search+"%")
|
||||
}
|
||||
|
||||
// Count query
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM units %s", whereClause)
|
||||
var total int
|
||||
err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Data query
|
||||
argCount++
|
||||
query := fmt.Sprintf(`
|
||||
SELECT id, organization_id, outlet_id, name, abbreviation, is_active, created_at, updated_at
|
||||
FROM units
|
||||
%s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $%d OFFSET $%d
|
||||
`, whereClause, argCount, argCount+1)
|
||||
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var units []*entities.Unit
|
||||
for rows.Next() {
|
||||
unit := &entities.Unit{}
|
||||
err := rows.Scan(
|
||||
&unit.ID,
|
||||
&unit.OrganizationID,
|
||||
&unit.OutletID,
|
||||
&unit.Name,
|
||||
&unit.Abbreviation,
|
||||
&unit.IsActive,
|
||||
&unit.CreatedAt,
|
||||
&unit.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
units = append(units, unit)
|
||||
}
|
||||
|
||||
return units, total, nil
|
||||
}
|
||||
|
||||
func (r *UnitRepository) Update(ctx context.Context, unit *entities.Unit) error {
|
||||
query := `
|
||||
UPDATE units
|
||||
SET outlet_id = $1, name = $2, abbreviation = $3, is_active = $4, updated_at = $5
|
||||
WHERE id = $6 AND organization_id = $7
|
||||
`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query,
|
||||
unit.OutletID,
|
||||
unit.Name,
|
||||
unit.Abbreviation,
|
||||
unit.IsActive,
|
||||
unit.UpdatedAt,
|
||||
unit.ID,
|
||||
unit.OrganizationID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UnitRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
query := `DELETE FROM units WHERE id = $1 AND organization_id = $2`
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -29,6 +29,8 @@ type Router struct {
|
||||
paymentMethodHandler *handler.PaymentMethodHandler
|
||||
analyticsHandler *handler.AnalyticsHandler
|
||||
tableHandler *handler.TableHandler
|
||||
unitHandler *handler.UnitHandler
|
||||
ingredientHandler *handler.IngredientHandler
|
||||
authMiddleware *middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
@ -61,7 +63,9 @@ func NewRouter(cfg *config.Config,
|
||||
paymentMethodValidator validator.PaymentMethodValidator,
|
||||
analyticsService *service.AnalyticsServiceImpl,
|
||||
tableService *service.TableServiceImpl,
|
||||
tableValidator *validator.TableValidator) *Router {
|
||||
tableValidator *validator.TableValidator,
|
||||
unitService handler.UnitService,
|
||||
ingredientService handler.IngredientService) *Router {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -80,6 +84,8 @@ func NewRouter(cfg *config.Config,
|
||||
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
|
||||
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
|
||||
tableHandler: handler.NewTableHandler(tableService, tableValidator),
|
||||
unitHandler: handler.NewUnitHandler(unitService),
|
||||
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
||||
authMiddleware: authMiddleware,
|
||||
}
|
||||
}
|
||||
@ -133,6 +139,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
adminUsers.DELETE("/:id", r.userHandler.DeleteUser)
|
||||
adminUsers.PUT("/:id/activate", r.userHandler.ActivateUser)
|
||||
adminUsers.PUT("/:id/deactivate", r.userHandler.DeactivateUser)
|
||||
adminUsers.POST("/select-outlet", r.userHandler.UpdateUserOutlet)
|
||||
}
|
||||
|
||||
users.PUT("/:id/password", r.userHandler.ChangePassword)
|
||||
@ -160,6 +167,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
categories.DELETE("/:id", r.categoryHandler.DeleteCategory)
|
||||
}
|
||||
|
||||
units := protected.Group("/units")
|
||||
units.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
units.POST("", r.unitHandler.Create)
|
||||
units.GET("", r.unitHandler.GetAll)
|
||||
units.GET("/:id", r.unitHandler.GetByID)
|
||||
units.PUT("/:id", r.unitHandler.Update)
|
||||
units.DELETE("/:id", r.unitHandler.Delete)
|
||||
}
|
||||
|
||||
products := protected.Group("/products")
|
||||
products.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
@ -258,6 +275,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
tables.POST("/:id/release", r.tableHandler.ReleaseTable)
|
||||
}
|
||||
|
||||
ingredients := protected.Group("/ingredients")
|
||||
ingredients.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
ingredients.POST("", r.ingredientHandler.Create)
|
||||
ingredients.GET("", r.ingredientHandler.GetAll)
|
||||
ingredients.GET("/:id", r.ingredientHandler.GetByID)
|
||||
ingredients.PUT("/:id", r.ingredientHandler.Update)
|
||||
ingredients.DELETE("/:id", r.ingredientHandler.Delete)
|
||||
}
|
||||
|
||||
outlets := protected.Group("/outlets")
|
||||
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
|
||||
16
internal/service/ingredient_processor.go
Normal file
16
internal/service/ingredient_processor.go
Normal file
@ -0,0 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientProcessor interface {
|
||||
CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error)
|
||||
UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error)
|
||||
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
||||
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
|
||||
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||
}
|
||||
38
internal/service/ingredient_service.go
Normal file
38
internal/service/ingredient_service.go
Normal file
@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientServiceImpl struct {
|
||||
ingredientProcessor IngredientProcessor
|
||||
}
|
||||
|
||||
func NewIngredientService(ingredientProcessor IngredientProcessor) *IngredientServiceImpl {
|
||||
return &IngredientServiceImpl{
|
||||
ingredientProcessor: ingredientProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IngredientServiceImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
||||
return s.ingredientProcessor.CreateIngredient(ctx, req)
|
||||
}
|
||||
|
||||
func (s *IngredientServiceImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
||||
return s.ingredientProcessor.UpdateIngredient(ctx, id, req)
|
||||
}
|
||||
|
||||
func (s *IngredientServiceImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
||||
return s.ingredientProcessor.DeleteIngredient(ctx, id)
|
||||
}
|
||||
|
||||
func (s *IngredientServiceImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||
return s.ingredientProcessor.GetIngredientByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *IngredientServiceImpl) ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error) {
|
||||
return s.ingredientProcessor.ListIngredients(ctx, organizationID, outletID, page, limit, search)
|
||||
}
|
||||
14
internal/service/inventory_movement_processor.go
Normal file
14
internal/service/inventory_movement_processor.go
Normal file
@ -0,0 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type InventoryMovementProcessor interface {
|
||||
CreateInventoryMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error)
|
||||
GetInventoryMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error)
|
||||
ListInventoryMovements(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.InventoryMovementResponse], error)
|
||||
}
|
||||
96
internal/service/inventory_movement_service.go
Normal file
96
internal/service/inventory_movement_service.go
Normal file
@ -0,0 +1,96 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type InventoryMovementService interface {
|
||||
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||
}
|
||||
|
||||
type InventoryMovementServiceImpl struct {
|
||||
inventoryMovementRepo repository.InventoryMovementRepository
|
||||
ingredientRepo *repository.IngredientRepository
|
||||
}
|
||||
|
||||
func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovementRepository, ingredientRepo *repository.IngredientRepository) InventoryMovementService {
|
||||
return &InventoryMovementServiceImpl{
|
||||
inventoryMovementRepo: inventoryMovementRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
||||
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
previousQuantity := ingredient.Stock
|
||||
newQuantity := previousQuantity + quantity
|
||||
|
||||
movement := &entities.InventoryMovement{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ItemID: ingredientID,
|
||||
ItemType: "INGREDIENT",
|
||||
MovementType: movementType,
|
||||
Quantity: quantity,
|
||||
PreviousQuantity: previousQuantity,
|
||||
NewQuantity: newQuantity,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: unitCost * quantity,
|
||||
ReferenceType: referenceType,
|
||||
ReferenceID: referenceID,
|
||||
UserID: userID,
|
||||
Reason: &reason,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = s.inventoryMovementRepo.Create(ctx, movement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.ingredientRepo.UpdateStock(ctx, ingredientID, quantity, organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InventoryMovementServiceImpl) CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
||||
movement := &entities.InventoryMovement{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: outletID,
|
||||
ItemID: productID,
|
||||
ItemType: "PRODUCT",
|
||||
MovementType: movementType,
|
||||
Quantity: quantity,
|
||||
PreviousQuantity: 0, // TODO This would be fetched from product inventory
|
||||
NewQuantity: 0, // TODO This would be calculated
|
||||
UnitCost: unitCost,
|
||||
TotalCost: unitCost * quantity,
|
||||
ReferenceType: referenceType,
|
||||
ReferenceID: referenceID,
|
||||
UserID: userID,
|
||||
Reason: &reason,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := s.inventoryMovementRepo.Create(ctx, movement)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
internal/service/unit_processor.go
Normal file
16
internal/service/unit_processor.go
Normal file
@ -0,0 +1,16 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitProcessor interface {
|
||||
CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error)
|
||||
UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error)
|
||||
DeleteUnit(ctx context.Context, id uuid.UUID) error
|
||||
GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error)
|
||||
ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error)
|
||||
}
|
||||
38
internal/service/unit_service.go
Normal file
38
internal/service/unit_service.go
Normal file
@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitServiceImpl struct {
|
||||
unitProcessor UnitProcessor
|
||||
}
|
||||
|
||||
func NewUnitService(unitProcessor UnitProcessor) *UnitServiceImpl {
|
||||
return &UnitServiceImpl{
|
||||
unitProcessor: unitProcessor,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UnitServiceImpl) CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error) {
|
||||
return s.unitProcessor.CreateUnit(ctx, req)
|
||||
}
|
||||
|
||||
func (s *UnitServiceImpl) UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error) {
|
||||
return s.unitProcessor.UpdateUnit(ctx, id, req)
|
||||
}
|
||||
|
||||
func (s *UnitServiceImpl) DeleteUnit(ctx context.Context, id uuid.UUID) error {
|
||||
return s.unitProcessor.DeleteUnit(ctx, id)
|
||||
}
|
||||
|
||||
func (s *UnitServiceImpl) GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error) {
|
||||
return s.unitProcessor.GetUnitByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *UnitServiceImpl) ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error) {
|
||||
return s.unitProcessor.ListUnits(ctx, organizationID, outletID, page, limit, search)
|
||||
}
|
||||
1
migrations/000026_create_units_table.down.sql
Normal file
1
migrations/000026_create_units_table.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS units;
|
||||
17
migrations/000026_create_units_table.up.sql
Normal file
17
migrations/000026_create_units_table.up.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- Units table
|
||||
CREATE TABLE units (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
abbreviation VARCHAR(10),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_units_organization_id ON units(organization_id);
|
||||
CREATE INDEX idx_units_outlet_id ON units(outlet_id);
|
||||
CREATE INDEX idx_units_is_active ON units(is_active);
|
||||
CREATE INDEX idx_units_created_at ON units(created_at);
|
||||
1
migrations/000027_create_ingredients_table.down.sql
Normal file
1
migrations/000027_create_ingredients_table.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ingredients;
|
||||
23
migrations/000027_create_ingredients_table.up.sql
Normal file
23
migrations/000027_create_ingredients_table.up.sql
Normal file
@ -0,0 +1,23 @@
|
||||
-- Ingredients table
|
||||
CREATE TABLE ingredients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
unit_id UUID NOT NULL REFERENCES units(id) ON DELETE CASCADE,
|
||||
cost DECIMAL(12,2) DEFAULT 0,
|
||||
stock DECIMAL(12,3) DEFAULT 0,
|
||||
is_semi_finished BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_ingredients_organization_id ON ingredients(organization_id);
|
||||
CREATE INDEX idx_ingredients_outlet_id ON ingredients(outlet_id);
|
||||
CREATE INDEX idx_ingredients_unit_id ON ingredients(unit_id);
|
||||
CREATE INDEX idx_ingredients_is_semi_finished ON ingredients(is_semi_finished);
|
||||
CREATE INDEX idx_ingredients_is_active ON ingredients(is_active);
|
||||
CREATE INDEX idx_ingredients_created_at ON ingredients(created_at);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ingredient_compositions;
|
||||
@ -0,0 +1,21 @@
|
||||
-- Ingredient compositions table
|
||||
CREATE TABLE ingredient_compositions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||
parent_ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
child_ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
quantity DECIMAL(12,3) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_ingredient_compositions_organization_id ON ingredient_compositions(organization_id);
|
||||
CREATE INDEX idx_ingredient_compositions_outlet_id ON ingredient_compositions(outlet_id);
|
||||
CREATE INDEX idx_ingredient_compositions_parent_ingredient_id ON ingredient_compositions(parent_ingredient_id);
|
||||
CREATE INDEX idx_ingredient_compositions_child_ingredient_id ON ingredient_compositions(child_ingredient_id);
|
||||
CREATE INDEX idx_ingredient_compositions_created_at ON ingredient_compositions(created_at);
|
||||
|
||||
-- Unique constraint to prevent duplicate compositions
|
||||
CREATE UNIQUE INDEX idx_ingredient_compositions_unique ON ingredient_compositions(parent_ingredient_id, child_ingredient_id);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS product_ingredients;
|
||||
21
migrations/000029_create_product_ingredients_table.up.sql
Normal file
21
migrations/000029_create_product_ingredients_table.up.sql
Normal file
@ -0,0 +1,21 @@
|
||||
-- Product ingredients table
|
||||
CREATE TABLE product_ingredients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
quantity DECIMAL(12,3) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_product_ingredients_organization_id ON product_ingredients(organization_id);
|
||||
CREATE INDEX idx_product_ingredients_outlet_id ON product_ingredients(outlet_id);
|
||||
CREATE INDEX idx_product_ingredients_product_id ON product_ingredients(product_id);
|
||||
CREATE INDEX idx_product_ingredients_ingredient_id ON product_ingredients(ingredient_id);
|
||||
CREATE INDEX idx_product_ingredients_created_at ON product_ingredients(created_at);
|
||||
|
||||
-- Unique constraint to prevent duplicate product-ingredient combinations
|
||||
CREATE UNIQUE INDEX idx_product_ingredients_unique ON product_ingredients(product_id, ingredient_id);
|
||||
@ -0,0 +1,4 @@
|
||||
-- Remove unit_id and has_ingredients from products table
|
||||
ALTER TABLE products
|
||||
DROP COLUMN IF EXISTS unit_id,
|
||||
DROP COLUMN IF EXISTS has_ingredients;
|
||||
@ -0,0 +1,8 @@
|
||||
-- Add unit_id and has_ingredients to products table
|
||||
ALTER TABLE products
|
||||
ADD COLUMN unit_id UUID REFERENCES units(id) ON DELETE SET NULL,
|
||||
ADD COLUMN has_ingredients BOOLEAN DEFAULT false;
|
||||
|
||||
-- Index for unit_id
|
||||
CREATE INDEX idx_products_unit_id ON products(unit_id);
|
||||
CREATE INDEX idx_products_has_ingredients ON products(has_ingredients);
|
||||
@ -0,0 +1,30 @@
|
||||
-- Revert inventory_movements table changes
|
||||
-- Add back product_id column
|
||||
ALTER TABLE inventory_movements
|
||||
ADD COLUMN product_id UUID;
|
||||
|
||||
-- Copy item_id data back to product_id where item_type is 'PRODUCT'
|
||||
UPDATE inventory_movements
|
||||
SET product_id = item_id
|
||||
WHERE item_type = 'PRODUCT';
|
||||
|
||||
-- Drop the new columns
|
||||
ALTER TABLE inventory_movements
|
||||
DROP COLUMN item_id,
|
||||
DROP COLUMN item_type;
|
||||
|
||||
-- Revert quantity columns to integer
|
||||
ALTER TABLE inventory_movements
|
||||
ALTER COLUMN quantity TYPE INTEGER USING quantity::integer,
|
||||
ALTER COLUMN previous_quantity TYPE INTEGER USING previous_quantity::integer,
|
||||
ALTER COLUMN new_quantity TYPE INTEGER USING new_quantity::integer;
|
||||
|
||||
-- Revert cost columns to original precision
|
||||
ALTER TABLE inventory_movements
|
||||
ALTER COLUMN unit_cost TYPE DECIMAL(10,2),
|
||||
ALTER COLUMN total_cost TYPE DECIMAL(10,2);
|
||||
|
||||
-- Drop the new indexes
|
||||
DROP INDEX IF EXISTS idx_inventory_movements_item_id;
|
||||
DROP INDEX IF EXISTS idx_inventory_movements_item_type;
|
||||
DROP INDEX IF EXISTS idx_inventory_movements_item_id_type;
|
||||
@ -0,0 +1,35 @@
|
||||
-- Update inventory_movements table to support ingredients
|
||||
ALTER TABLE inventory_movements
|
||||
ADD COLUMN item_id UUID,
|
||||
ADD COLUMN item_type VARCHAR(20);
|
||||
|
||||
-- Copy existing product_id data to item_id
|
||||
UPDATE inventory_movements
|
||||
SET item_id = product_id,
|
||||
item_type = 'PRODUCT'
|
||||
WHERE product_id IS NOT NULL;
|
||||
|
||||
-- Make item_id and item_type NOT NULL after data migration
|
||||
ALTER TABLE inventory_movements
|
||||
ALTER COLUMN item_id SET NOT NULL,
|
||||
ALTER COLUMN item_type SET NOT NULL;
|
||||
|
||||
-- Drop the old product_id column
|
||||
ALTER TABLE inventory_movements
|
||||
DROP COLUMN product_id;
|
||||
|
||||
-- Update quantity columns to support decimal
|
||||
ALTER TABLE inventory_movements
|
||||
ALTER COLUMN quantity TYPE DECIMAL(12,3),
|
||||
ALTER COLUMN previous_quantity TYPE DECIMAL(12,3),
|
||||
ALTER COLUMN new_quantity TYPE DECIMAL(12,3);
|
||||
|
||||
-- Update cost columns to support higher precision
|
||||
ALTER TABLE inventory_movements
|
||||
ALTER COLUMN unit_cost TYPE DECIMAL(12,2),
|
||||
ALTER COLUMN total_cost TYPE DECIMAL(12,2);
|
||||
|
||||
-- Add indexes for the new structure
|
||||
CREATE INDEX idx_inventory_movements_item_id ON inventory_movements(item_id);
|
||||
CREATE INDEX idx_inventory_movements_item_type ON inventory_movements(item_type);
|
||||
CREATE INDEX idx_inventory_movements_item_id_type ON inventory_movements(item_id, item_type);
|
||||
Loading…
x
Reference in New Issue
Block a user