Add Customer users

This commit is contained in:
Aditya Siregar 2025-08-03 23:55:51 +07:00
parent 96743cf50b
commit 5741243425
69 changed files with 3005 additions and 158 deletions

View File

@ -76,6 +76,8 @@ func (a *App) Initialize(cfg *config.Config) error {
services.analyticsService, services.analyticsService,
services.tableService, services.tableService,
validators.tableValidator, validators.tableValidator,
services.unitService,
services.ingredientService,
) )
return nil return nil
@ -138,6 +140,8 @@ type repositories struct {
customerRepo *repository.CustomerRepository customerRepo *repository.CustomerRepository
analyticsRepo *repository.AnalyticsRepositoryImpl analyticsRepo *repository.AnalyticsRepositoryImpl
tableRepo *repository.TableRepository tableRepo *repository.TableRepository
unitRepo *repository.UnitRepository
ingredientRepo *repository.IngredientRepository
} }
func (a *App) initRepositories() *repositories { func (a *App) initRepositories() *repositories {
@ -159,6 +163,8 @@ func (a *App) initRepositories() *repositories {
customerRepo: repository.NewCustomerRepository(a.db), customerRepo: repository.NewCustomerRepository(a.db),
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db), analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
tableRepo: repository.NewTableRepository(a.db), tableRepo: repository.NewTableRepository(a.db),
unitRepo: repository.NewUnitRepository(a.db),
ingredientRepo: repository.NewIngredientRepository(a.db),
} }
} }
@ -177,6 +183,8 @@ type processors struct {
customerProcessor *processor.CustomerProcessor customerProcessor *processor.CustomerProcessor
analyticsProcessor *processor.AnalyticsProcessorImpl analyticsProcessor *processor.AnalyticsProcessorImpl
tableProcessor *processor.TableProcessor tableProcessor *processor.TableProcessor
unitProcessor *processor.UnitProcessorImpl
ingredientProcessor *processor.IngredientProcessorImpl
} }
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
@ -197,6 +205,8 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo), customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo), analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo), tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
} }
} }
@ -216,6 +226,8 @@ type services struct {
customerService service.CustomerService customerService service.CustomerService
analyticsService *service.AnalyticsServiceImpl analyticsService *service.AnalyticsServiceImpl
tableService *service.TableServiceImpl tableService *service.TableServiceImpl
unitService *service.UnitServiceImpl
ingredientService *service.IngredientServiceImpl
} }
func (a *App) initServices(processors *processors, cfg *config.Config) *services { func (a *App) initServices(processors *processors, cfg *config.Config) *services {
@ -235,6 +247,8 @@ func (a *App) initServices(processors *processors, cfg *config.Config) *services
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor) var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor) analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor)
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer()) tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
unitService := service.NewUnitService(processors.unitProcessor)
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
return &services{ return &services{
userService: service.NewUserService(processors.userProcessor), userService: service.NewUserService(processors.userProcessor),
@ -252,6 +266,8 @@ func (a *App) initServices(processors *processors, cfg *config.Config) *services
customerService: customerService, customerService: customerService,
analyticsService: analyticsService, analyticsService: analyticsService,
tableService: tableService, tableService: tableService,
unitService: unitService,
ingredientService: ingredientService,
} }
} }

View File

@ -20,7 +20,7 @@ type ContextInfo struct {
CorrelationID string CorrelationID string
UserID uuid.UUID UserID uuid.UUID
OrganizationID uuid.UUID OrganizationID uuid.UUID
OutletID string OutletID uuid.UUID
AppVersion string AppVersion string
AppID string AppID string
AppType string AppType string
@ -61,7 +61,7 @@ func FromGinContext(ctx context.Context) *ContextInfo {
return &ContextInfo{ return &ContextInfo{
CorrelationID: value(ctx, CorrelationIDKey), CorrelationID: value(ctx, CorrelationIDKey),
UserID: uuidValue(ctx, UserIDKey), UserID: uuidValue(ctx, UserIDKey),
OutletID: value(ctx, OutletIDKey), OutletID: uuidValue(ctx, OutletIDKey),
OrganizationID: uuidValue(ctx, OrganizationIDKey), OrganizationID: uuidValue(ctx, OrganizationIDKey),
AppVersion: value(ctx, AppVersionKey), AppVersion: value(ctx, AppVersionKey),
AppID: value(ctx, AppIDKey), AppID: value(ctx, AppIDKey),

View 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
}

View 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)
}

View File

@ -9,6 +9,7 @@ import (
type CreateOrderRequest struct { type CreateOrderRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"` OutletID uuid.UUID `json:"outlet_id" validate:"required"`
UserID uuid.UUID `json:"user_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"` TableNumber *string `json:"table_number,omitempty" validate:"omitempty,max=50"`
OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"` OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"`

View File

@ -7,7 +7,8 @@ import (
) )
type CreatePaymentMethodRequest struct { type CreatePaymentMethodRequest struct {
OrganizationID uuid.UUID `json:"organization_id" validate:"required"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID uuid.UUID `json:"outlet_id""'`
Name string `json:"name" validate:"required,min=1,max=100"` Name string `json:"name" validate:"required,min=1,max=100"`
Type string `json:"type" validate:"required,oneof=cash card digital_wallet qr edc"` Type string `json:"type" validate:"required,oneof=cash card digital_wallet qr edc"`
Processor *string `json:"processor,omitempty" validate:"omitempty,max=100"` Processor *string `json:"processor,omitempty" validate:"omitempty,max=100"`

View 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)
}

View 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
}

View File

@ -0,0 +1,23 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type Ingredient 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"`
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
Name string `gorm:"not null;size:255" json:"name"`
UnitID uuid.UUID `gorm:"type:uuid;not null;index" json:"unit_id"`
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
IsActive bool `gorm:"default:true" json:"is_active"`
Metadata map[string]any `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
}

View File

@ -0,0 +1,20 @@
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"`
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
}

View File

@ -38,13 +38,14 @@ type InventoryMovement struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` 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"` 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"` 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"` MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
Quantity int `gorm:"not null" json:"quantity" validate:"required"` Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity" validate:"required"`
PreviousQuantity int `gorm:"not null" json:"previous_quantity" validate:"required"` PreviousQuantity float64 `gorm:"type:decimal(12,3)" json:"previous_quantity"`
NewQuantity int `gorm:"not null" json:"new_quantity" validate:"required"` NewQuantity float64 `gorm:"type:decimal(12,3)" json:"new_quantity"`
UnitCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"unit_cost"` UnitCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"unit_cost"`
TotalCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"total_cost"` TotalCost float64 `gorm:"type:decimal(12,2);default:0.00" json:"total_cost"`
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"` ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"` ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_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"` Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,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"` Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"` Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"`

View File

@ -8,27 +8,31 @@ import (
) )
type Product struct { type Product struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` 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"` 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"` CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
SKU *string `gorm:"size:100;index" json:"sku"` SKU *string `gorm:"size:100;index" json:"sku"`
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
Description *string `gorm:"type:text" json:"description"` Description *string `gorm:"type:text" json:"description"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price" validate:"required,min=0"` 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"` 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"` BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
ImageURL *string `gorm:"size:500" json:"image_url"` ImageURL *string `gorm:"size:500" json:"image_url"`
PrinterType string `gorm:"size:50;default:'kitchen'" json:"printer_type"` PrinterType string `gorm:"size:50;default:'kitchen'" json:"printer_type"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` UnitID *uuid.UUID `gorm:"type:uuid;index" json:"unit_id"`
IsActive bool `gorm:"default:true" json:"is_active"` HasIngredients bool `gorm:"default:false" json:"has_ingredients"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` 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"` Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"` Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"` Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"` ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,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 { func (p *Product) BeforeCreate(tx *gorm.DB) error {

View 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
View File

@ -0,0 +1,18 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type Unit 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"`
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
Name string `gorm:"not null;size:255" json:"name"`
Abbreviation *string `gorm:"size:50" json:"abbreviation"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}

View 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")
}

View 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)
}

View 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)
}

View File

@ -49,6 +49,9 @@ func (h *PaymentMethodHandler) CreatePaymentMethod(c *gin.Context) {
return return
} }
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = contextInfo.OutletID
paymentMethodResponse := h.paymentMethodService.CreatePaymentMethod(ctx, contextInfo, &req) paymentMethodResponse := h.paymentMethodService.CreatePaymentMethod(ctx, contextInfo, &req)
if paymentMethodResponse.HasErrors() { if paymentMethodResponse.HasErrors() {
errorResp := paymentMethodResponse.GetErrors()[0] errorResp := paymentMethodResponse.GetErrors()[0]

View File

@ -0,0 +1,199 @@
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)
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")
}

View 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)
}

View File

@ -1,6 +1,7 @@
package handler package handler
import ( import (
"apskel-pos-be/internal/appcontext"
"net/http" "net/http"
"strconv" "strconv"
@ -294,20 +295,8 @@ func (h *UserHandler) DeactivateUser(c *gin.Context) {
} }
func (h *UserHandler) UpdateUserOutlet(c *gin.Context) { func (h *UserHandler) UpdateUserOutlet(c *gin.Context) {
userIDStr := c.Param("id") ctx := c.Request.Context()
userID, err := uuid.Parse(userIDStr) contextInfo := appcontext.FromGinContext(ctx)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUserOutlet -> Invalid user ID")
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
return
}
validationError, validationErrorCode := h.userValidator.ValidateUserID(userID)
if validationError != nil {
logger.FromContext(c).WithError(validationError).Error("UserHandler::UpdateUserOutlet -> user ID validation failed")
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
return
}
var req contract.UpdateUserOutletRequest var req contract.UpdateUserOutletRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@ -316,14 +305,7 @@ func (h *UserHandler) UpdateUserOutlet(c *gin.Context) {
return return
} }
validationError, validationErrorCode = h.userValidator.ValidateUpdateUserOutletRequest(&req) userResponse, err := h.userService.UpdateUserOutlet(c.Request.Context(), contextInfo.UserID, &req)
if validationError != nil {
logger.FromContext(c).WithError(validationError).Error("UserHandler::UpdateUserOutlet -> request validation failed")
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
return
}
userResponse, err := h.userService.UpdateUserOutlet(c.Request.Context(), userID, &req)
if err != nil { if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUserOutlet -> Failed to update user outlet from service") logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUserOutlet -> Failed to update user outlet from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)

View 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
}

View 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
}

View File

@ -14,11 +14,12 @@ func InventoryMovementEntityToModel(entity *entities.InventoryMovement) *models.
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID, OutletID: entity.OutletID,
ProductID: entity.ProductID, ItemID: entity.ItemID,
ItemType: entity.ItemType,
MovementType: models.InventoryMovementType(entity.MovementType), MovementType: models.InventoryMovementType(entity.MovementType),
Quantity: entity.Quantity, Quantity: int(entity.Quantity),
PreviousQuantity: entity.PreviousQuantity, PreviousQuantity: int(entity.PreviousQuantity),
NewQuantity: entity.NewQuantity, NewQuantity: int(entity.NewQuantity),
UnitCost: entity.UnitCost, UnitCost: entity.UnitCost,
TotalCost: entity.TotalCost, TotalCost: entity.TotalCost,
ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType), ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType),
@ -42,11 +43,12 @@ func InventoryMovementModelToEntity(model *models.InventoryMovement) *entities.I
ID: model.ID, ID: model.ID,
OrganizationID: model.OrganizationID, OrganizationID: model.OrganizationID,
OutletID: model.OutletID, OutletID: model.OutletID,
ProductID: model.ProductID, ItemID: model.ItemID,
ItemType: model.ItemType,
MovementType: entities.InventoryMovementType(model.MovementType), MovementType: entities.InventoryMovementType(model.MovementType),
Quantity: model.Quantity, Quantity: float64(model.Quantity),
PreviousQuantity: model.PreviousQuantity, PreviousQuantity: float64(model.PreviousQuantity),
NewQuantity: model.NewQuantity, NewQuantity: float64(model.NewQuantity),
UnitCost: model.UnitCost, UnitCost: model.UnitCost,
TotalCost: model.TotalCost, TotalCost: model.TotalCost,
ReferenceType: (*entities.InventoryMovementReferenceType)(model.ReferenceType), ReferenceType: (*entities.InventoryMovementReferenceType)(model.ReferenceType),
@ -70,11 +72,12 @@ func InventoryMovementEntityToResponse(entity *entities.InventoryMovement) *mode
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID, OutletID: entity.OutletID,
ProductID: entity.ProductID, ItemID: entity.ItemID,
ItemType: entity.ItemType,
MovementType: models.InventoryMovementType(entity.MovementType), MovementType: models.InventoryMovementType(entity.MovementType),
Quantity: entity.Quantity, Quantity: int(entity.Quantity),
PreviousQuantity: entity.PreviousQuantity, PreviousQuantity: int(entity.PreviousQuantity),
NewQuantity: entity.NewQuantity, NewQuantity: int(entity.NewQuantity),
UnitCost: entity.UnitCost, UnitCost: entity.UnitCost,
TotalCost: entity.TotalCost, TotalCost: entity.TotalCost,
ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType), ReferenceType: (*models.InventoryMovementReferenceType)(entity.ReferenceType),
@ -116,11 +119,12 @@ func CreateInventoryMovementRequestToEntity(req *models.CreateInventoryMovementR
return &entities.InventoryMovement{ return &entities.InventoryMovement{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
ProductID: req.ProductID, ItemID: req.ItemID,
ItemType: req.ItemType,
MovementType: entities.InventoryMovementType(req.MovementType), MovementType: entities.InventoryMovementType(req.MovementType),
Quantity: req.Quantity, Quantity: float64(req.Quantity),
PreviousQuantity: previousQuantity, PreviousQuantity: float64(previousQuantity),
NewQuantity: newQuantity, NewQuantity: float64(newQuantity),
UnitCost: req.UnitCost, UnitCost: req.UnitCost,
TotalCost: float64(req.Quantity) * req.UnitCost, TotalCost: float64(req.Quantity) * req.UnitCost,
ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType), ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType),

View File

@ -100,6 +100,7 @@ func PaymentMethodModelToEntity(model *models.PaymentMethod) *entities.PaymentMe
func CreatePaymentMethodContractToModel(req *contract.CreatePaymentMethodRequest) *models.CreatePaymentMethodRequest { func CreatePaymentMethodContractToModel(req *contract.CreatePaymentMethodRequest) *models.CreatePaymentMethodRequest {
return &models.CreatePaymentMethodRequest{ return &models.CreatePaymentMethodRequest{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
Name: req.Name, Name: req.Name,
Type: constants.PaymentMethodType(req.Type), Type: constants.PaymentMethodType(req.Type),
Processor: req.Processor, Processor: req.Processor,

View 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
}

View 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
}

View 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"`
}

View 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"`
}

View File

@ -37,7 +37,8 @@ type InventoryMovement struct {
ID uuid.UUID ID uuid.UUID
OrganizationID uuid.UUID OrganizationID uuid.UUID
OutletID uuid.UUID OutletID uuid.UUID
ProductID uuid.UUID ItemID uuid.UUID
ItemType string
MovementType InventoryMovementType MovementType InventoryMovementType
Quantity int Quantity int
PreviousQuantity int PreviousQuantity int
@ -58,7 +59,8 @@ type InventoryMovement struct {
type CreateInventoryMovementRequest struct { type CreateInventoryMovementRequest struct {
OrganizationID uuid.UUID OrganizationID uuid.UUID
OutletID uuid.UUID OutletID uuid.UUID
ProductID uuid.UUID ItemID uuid.UUID
ItemType string
MovementType InventoryMovementType MovementType InventoryMovementType
Quantity int Quantity int
UnitCost float64 UnitCost float64
@ -76,7 +78,8 @@ type InventoryMovementResponse struct {
ID uuid.UUID ID uuid.UUID
OrganizationID uuid.UUID OrganizationID uuid.UUID
OutletID uuid.UUID OutletID uuid.UUID
ProductID uuid.UUID ItemID uuid.UUID
ItemType string
MovementType InventoryMovementType MovementType InventoryMovementType
Quantity int Quantity int
PreviousQuantity int PreviousQuantity int
@ -98,7 +101,8 @@ type InventoryMovementResponse struct {
type ListInventoryMovementsRequest struct { type ListInventoryMovementsRequest struct {
OrganizationID *uuid.UUID OrganizationID *uuid.UUID
OutletID *uuid.UUID OutletID *uuid.UUID
ProductID *uuid.UUID ItemID *uuid.UUID
ItemType *string
MovementType *InventoryMovementType MovementType *InventoryMovementType
ReferenceType *InventoryMovementReferenceType ReferenceType *InventoryMovementReferenceType
ReferenceID *uuid.UUID ReferenceID *uuid.UUID

View File

@ -21,6 +21,7 @@ type PaymentMethod struct {
type CreatePaymentMethodRequest struct { type CreatePaymentMethodRequest struct {
OrganizationID uuid.UUID `validate:"required"` OrganizationID uuid.UUID `validate:"required"`
OutletID uuid.UUID `validate:"required"`
Name string `validate:"required,min=1,max=100"` Name string `validate:"required,min=1,max=100"`
Type constants.PaymentMethodType `validate:"required"` Type constants.PaymentMethodType `validate:"required"`
Processor *string `validate:"omitempty,max=100"` Processor *string `validate:"omitempty,max=100"`

View File

@ -19,6 +19,8 @@ type Product struct {
BusinessType constants.BusinessType BusinessType constants.BusinessType
ImageURL *string ImageURL *string
PrinterType string PrinterType string
UnitID *uuid.UUID
HasIngredients bool
Metadata map[string]interface{} Metadata map[string]interface{}
IsActive bool IsActive bool
CreatedAt time.Time CreatedAt time.Time
@ -47,6 +49,8 @@ type CreateProductRequest struct {
BusinessType constants.BusinessType `validate:"required"` BusinessType constants.BusinessType `validate:"required"`
ImageURL *string `validate:"omitempty,max=500"` ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"` PrinterType *string `validate:"omitempty,max=50"`
UnitID *uuid.UUID `validate:"omitempty"`
HasIngredients bool `validate:"omitempty"`
Metadata map[string]interface{} Metadata map[string]interface{}
Variants []CreateProductVariantRequest `validate:"omitempty,dive"` Variants []CreateProductVariantRequest `validate:"omitempty,dive"`
// Stock management fields // Stock management fields
@ -56,16 +60,18 @@ type CreateProductRequest struct {
} }
type UpdateProductRequest struct { type UpdateProductRequest struct {
CategoryID *uuid.UUID CategoryID *uuid.UUID `validate:"omitempty"`
SKU *string `validate:"omitempty,max=100"` SKU *string `validate:"omitempty,max=100"`
Name *string `validate:"omitempty,min=1,max=255"` Name *string `validate:"omitempty,min=1,max=255"`
Description *string `validate:"omitempty,max=1000"` Description *string `validate:"omitempty,max=1000"`
Price *float64 `validate:"omitempty,min=0"` Price *float64 `validate:"omitempty,min=0"`
Cost *float64 `validate:"omitempty,min=0"` Cost *float64 `validate:"omitempty,min=0"`
ImageURL *string `validate:"omitempty,max=500"` ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"` PrinterType *string `validate:"omitempty,max=50"`
Metadata map[string]interface{} UnitID *uuid.UUID `validate:"omitempty"`
IsActive *bool HasIngredients *bool `validate:"omitempty"`
Metadata map[string]interface{}
IsActive *bool
// Stock management fields // Stock management fields
ReorderLevel *int `validate:"omitempty,min=0"` // Update reorder level for all existing inventory records ReorderLevel *int `validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
} }
@ -97,6 +103,8 @@ type ProductResponse struct {
BusinessType constants.BusinessType BusinessType constants.BusinessType
ImageURL *string ImageURL *string
PrinterType string PrinterType string
UnitID *uuid.UUID
HasIngredients bool
Metadata map[string]interface{} Metadata map[string]interface{}
IsActive bool IsActive bool
CreatedAt time.Time CreatedAt time.Time

View 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
View 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"`
}

View 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
}

View 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
}

View File

@ -47,8 +47,8 @@ func NewInventoryMovementProcessorImpl(
} }
} }
func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error) { func (p *InventoryMovementProcessorImpl) CreateInventoryMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error) {
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID) currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ItemID, req.OutletID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get current inventory: %w", err) 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{ movement := &entities.InventoryMovement{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
ProductID: req.ProductID, ItemID: req.ItemID,
ItemType: req.ItemType,
MovementType: entities.InventoryMovementType(req.MovementType), MovementType: entities.InventoryMovementType(req.MovementType),
Quantity: req.Quantity, Quantity: float64(req.Quantity),
PreviousQuantity: previousQuantity, PreviousQuantity: float64(previousQuantity),
NewQuantity: newQuantity, NewQuantity: float64(newQuantity),
UnitCost: req.UnitCost, UnitCost: req.UnitCost,
TotalCost: float64(req.Quantity) * req.UnitCost, TotalCost: float64(req.Quantity) * req.UnitCost,
ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType), ReferenceType: (*entities.InventoryMovementReferenceType)(req.ReferenceType),
@ -89,7 +90,7 @@ func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req
return response, nil 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) movement, err := p.movementRepo.GetWithRelations(ctx, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("movement not found: %w", err) return nil, fmt.Errorf("movement not found: %w", err)
@ -99,44 +100,29 @@ func (p *InventoryMovementProcessorImpl) GetMovementByID(ctx context.Context, id
return response, nil return response, nil
} }
func (p *InventoryMovementProcessorImpl) ListMovements(ctx context.Context, req *models.ListInventoryMovementsRequest) (*models.ListInventoryMovementsResponse, error) { func (p *InventoryMovementProcessorImpl) ListInventoryMovements(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.InventoryMovementResponse], error) {
filters := make(map[string]interface{}) // Set default values
if req.OrganizationID != nil { if page < 1 {
filters["organization_id"] = *req.OrganizationID page = 1
} }
if req.OutletID != nil { if limit < 1 {
filters["outlet_id"] = *req.OutletID limit = 10
} }
if req.ProductID != nil { if limit > 100 {
filters["product_id"] = *req.ProductID limit = 100
}
if req.MovementType != nil {
filters["movement_type"] = string(*req.MovementType)
}
if req.ReferenceType != nil {
filters["reference_type"] = string(*req.ReferenceType)
}
if req.ReferenceID != nil {
filters["reference_id"] = *req.ReferenceID
}
if req.OrderID != nil {
filters["order_id"] = *req.OrderID
}
if req.PaymentID != nil {
filters["payment_id"] = *req.PaymentID
}
if req.UserID != nil {
filters["user_id"] = *req.UserID
}
if req.DateFrom != nil {
filters["date_from"] = *req.DateFrom
}
if req.DateTo != nil {
filters["date_to"] = *req.DateTo
} }
offset := (req.Page - 1) * req.Limit filters := make(map[string]interface{})
movements, total, err := p.movementRepo.List(ctx, filters, req.Limit, offset) 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 { if err != nil {
return nil, fmt.Errorf("failed to list movements: %w", err) 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 // Create paginated response
totalPages := int(total) / req.Limit paginatedResponse := &models.PaginatedResponse[models.InventoryMovementResponse]{
if int(total)%req.Limit > 0 { Data: movementResponses,
totalPages++ Pagination: models.Pagination{
Page: page,
Limit: limit,
Total: total,
TotalPages: int((total + int64(limit) - 1) / int64(limit)),
},
} }
return &models.ListInventoryMovementsResponse{ return paginatedResponse, nil
Movements: movementResponses,
TotalCount: int(total),
Page: req.Page,
Limit: req.Limit,
TotalPages: totalPages,
}, nil
} }
func (p *InventoryMovementProcessorImpl) GetMovementsByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) (*models.ListInventoryMovementsResponse, error) { func (p *InventoryMovementProcessorImpl) GetMovementsByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID, limit, offset int) (*models.ListInventoryMovementsResponse, error) {

View File

@ -31,7 +31,6 @@ func NewPaymentMethodProcessorImpl(paymentMethodRepo repository.PaymentMethodRep
} }
func (p *PaymentMethodProcessorImpl) CreatePaymentMethod(ctx context.Context, req *models.CreatePaymentMethodRequest) (*models.PaymentMethodResponse, error) { func (p *PaymentMethodProcessorImpl) CreatePaymentMethod(ctx context.Context, req *models.CreatePaymentMethodRequest) (*models.PaymentMethodResponse, error) {
// Check if payment method with same name already exists
exists, err := p.paymentMethodRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil) exists, err := p.paymentMethodRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to check payment method name uniqueness: %w", err) return nil, fmt.Errorf("failed to check payment method name uniqueness: %w", err)
@ -40,21 +39,16 @@ func (p *PaymentMethodProcessorImpl) CreatePaymentMethod(ctx context.Context, re
return nil, fmt.Errorf("payment method with name '%s' already exists for this organization", req.Name) return nil, fmt.Errorf("payment method with name '%s' already exists for this organization", req.Name)
} }
// Map request to entity
paymentMethodEntity := mappers.CreatePaymentMethodRequestToEntity(req) paymentMethodEntity := mappers.CreatePaymentMethodRequestToEntity(req)
// Create payment method
if err := p.paymentMethodRepo.Create(ctx, paymentMethodEntity); err != nil { if err := p.paymentMethodRepo.Create(ctx, paymentMethodEntity); err != nil {
return nil, fmt.Errorf("failed to create payment method: %w", err) return nil, fmt.Errorf("failed to create payment method: %w", err)
} }
// Get created payment method
createdPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, paymentMethodEntity.ID) createdPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, paymentMethodEntity.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve created payment method: %w", err) return nil, fmt.Errorf("failed to retrieve created payment method: %w", err)
} }
// Map entity to response
response := mappers.PaymentMethodEntityToResponse(createdPaymentMethod) response := mappers.PaymentMethodEntityToResponse(createdPaymentMethod)
return response, nil return response, nil
} }
@ -70,7 +64,6 @@ func (p *PaymentMethodProcessorImpl) GetPaymentMethodByID(ctx context.Context, i
} }
func (p *PaymentMethodProcessorImpl) ListPaymentMethods(ctx context.Context, req *models.ListPaymentMethodsRequest) (*models.ListPaymentMethodsResponse, error) { func (p *PaymentMethodProcessorImpl) ListPaymentMethods(ctx context.Context, req *models.ListPaymentMethodsRequest) (*models.ListPaymentMethodsResponse, error) {
// Build filters
filters := make(map[string]interface{}) filters := make(map[string]interface{})
if req.OrganizationID != nil { if req.OrganizationID != nil {
filters["organization_id"] = *req.OrganizationID filters["organization_id"] = *req.OrganizationID
@ -85,10 +78,8 @@ func (p *PaymentMethodProcessorImpl) ListPaymentMethods(ctx context.Context, req
filters["search"] = req.Search filters["search"] = req.Search
} }
// Calculate offset
offset := (req.Page - 1) * req.Limit offset := (req.Page - 1) * req.Limit
// Get payment methods
paymentMethods, total, err := p.paymentMethodRepo.List(ctx, filters, req.Limit, offset) paymentMethods, total, err := p.paymentMethodRepo.List(ctx, filters, req.Limit, offset)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list payment methods: %w", err) return nil, fmt.Errorf("failed to list payment methods: %w", err)

View 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
}

View 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)
}

View 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
}

View 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
}

View File

@ -0,0 +1,102 @@
package repository
import (
"apskel-pos-be/internal/entities"
"context"
"fmt"
"gorm.io/gorm"
"github.com/google/uuid"
)
type IngredientRepository struct {
db *gorm.DB
}
func NewIngredientRepository(db *gorm.DB) *IngredientRepository {
return &IngredientRepository{db: db}
}
func (r *IngredientRepository) Create(ctx context.Context, ingredient *entities.Ingredient) error {
return r.db.WithContext(ctx).Create(ingredient).Error
}
func (r *IngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error) {
var ingredient entities.Ingredient
err := r.db.WithContext(ctx).Preload("Unit").Where("id = ? AND organization_id = ?", id, organizationID).First(&ingredient).Error
if err != nil {
return nil, err
}
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) {
var ingredients []*entities.Ingredient
var total int64
query := r.db.WithContext(ctx).Model(&entities.Ingredient{}).Where("organization_id = ?", organizationID)
if outletID != nil {
query = query.Where("outlet_id = ?", *outletID)
}
if search != "" {
searchValue := "%" + search + "%"
query = query.Where("name ILIKE ?", searchValue)
}
if isSemiFinished != nil {
query = query.Where("is_semi_finished = ?", *isSemiFinished)
}
// Count total records
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results with Unit preloaded
offset := (page - 1) * limit
err := query.Preload("Unit").Order("created_at DESC").Limit(limit).Offset(offset).Find(&ingredients).Error
if err != nil {
return nil, 0, err
}
return ingredients, int(total), nil
}
func (r *IngredientRepository) Update(ctx context.Context, ingredient *entities.Ingredient) error {
result := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", ingredient.ID, ingredient.OrganizationID).Save(ingredient)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected")
}
return nil
}
func (r *IngredientRepository) UpdateStock(ctx context.Context, id uuid.UUID, quantity float64, organizationID uuid.UUID) error {
result := r.db.WithContext(ctx).Model(&entities.Ingredient{}).
Where("id = ? AND organization_id = ?", id, organizationID).
Update("stock", gorm.Expr("stock + ?", quantity))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected")
}
return nil
}
func (r *IngredientRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
result := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", id, organizationID).Delete(&entities.Ingredient{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected")
}
return nil
}

View File

@ -150,11 +150,12 @@ func (r *PaymentRepositoryImpl) CreatePaymentWithInventoryMovement(ctx context.C
movement := &entities.InventoryMovement{ movement := &entities.InventoryMovement{
OrganizationID: order.OrganizationID, OrganizationID: order.OrganizationID,
OutletID: order.OutletID, OutletID: order.OutletID,
ProductID: item.ProductID, ItemID: item.ProductID,
ItemType: "PRODUCT",
MovementType: entities.InventoryMovementTypeSale, MovementType: entities.InventoryMovementTypeSale,
Quantity: -item.Quantity, Quantity: float64(-item.Quantity),
PreviousQuantity: updatedInventory.Quantity + item.Quantity, // Add back the quantity that was subtracted PreviousQuantity: float64(updatedInventory.Quantity + item.Quantity), // Add back the quantity that was subtracted
NewQuantity: updatedInventory.Quantity, NewQuantity: float64(updatedInventory.Quantity),
UnitCost: item.UnitCost, UnitCost: item.UnitCost,
TotalCost: float64(item.Quantity) * item.UnitCost, TotalCost: float64(item.Quantity) * item.UnitCost,
ReferenceType: func() *entities.InventoryMovementReferenceType { ReferenceType: func() *entities.InventoryMovementReferenceType {
@ -222,11 +223,12 @@ func (r *PaymentRepositoryImpl) RefundPaymentWithInventoryMovement(ctx context.C
movement := &entities.InventoryMovement{ movement := &entities.InventoryMovement{
OrganizationID: order.OrganizationID, OrganizationID: order.OrganizationID,
OutletID: order.OutletID, OutletID: order.OutletID,
ProductID: item.ProductID, ItemID: item.ProductID,
ItemType: "PRODUCT",
MovementType: entities.InventoryMovementTypeRefund, MovementType: entities.InventoryMovementTypeRefund,
Quantity: refundedQuantity, Quantity: float64(refundedQuantity),
PreviousQuantity: updatedInventory.Quantity - refundedQuantity, // Subtract the quantity that was added PreviousQuantity: float64(updatedInventory.Quantity - refundedQuantity), // Subtract the quantity that was added
NewQuantity: updatedInventory.Quantity, NewQuantity: float64(updatedInventory.Quantity),
UnitCost: item.UnitCost, UnitCost: item.UnitCost,
TotalCost: float64(refundedQuantity) * item.UnitCost, TotalCost: float64(refundedQuantity) * item.UnitCost,
ReferenceType: func() *entities.InventoryMovementReferenceType { ReferenceType: func() *entities.InventoryMovementReferenceType {

View 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
}

View File

@ -0,0 +1,84 @@
package repository
import (
"apskel-pos-be/internal/entities"
"context"
"fmt"
"gorm.io/gorm"
"github.com/google/uuid"
)
type UnitRepository struct {
db *gorm.DB
}
func NewUnitRepository(db *gorm.DB) *UnitRepository {
return &UnitRepository{db: db}
}
func (r *UnitRepository) Create(ctx context.Context, unit *entities.Unit) error {
return r.db.WithContext(ctx).Create(unit).Error
}
func (r *UnitRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error) {
var unit entities.Unit
err := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", id, organizationID).First(&unit).Error
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) {
var units []*entities.Unit
var total int64
query := r.db.WithContext(ctx).Model(&entities.Unit{}).Where("organization_id = ?", organizationID)
if outletID != nil {
query = query.Where("outlet_id = ?", *outletID)
}
if search != "" {
searchValue := "%" + search + "%"
query = query.Where("name ILIKE ? OR abbreviation ILIKE ?", searchValue, searchValue)
}
// Count total records
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&units).Error
if err != nil {
return nil, 0, err
}
return units, int(total), nil
}
func (r *UnitRepository) Update(ctx context.Context, unit *entities.Unit) error {
result := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", unit.ID, unit.OrganizationID).Save(unit)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected")
}
return nil
}
func (r *UnitRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
result := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", id, organizationID).Delete(&entities.Unit{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected")
}
return nil
}

View File

@ -29,6 +29,8 @@ type Router struct {
paymentMethodHandler *handler.PaymentMethodHandler paymentMethodHandler *handler.PaymentMethodHandler
analyticsHandler *handler.AnalyticsHandler analyticsHandler *handler.AnalyticsHandler
tableHandler *handler.TableHandler tableHandler *handler.TableHandler
unitHandler *handler.UnitHandler
ingredientHandler *handler.IngredientHandler
authMiddleware *middleware.AuthMiddleware authMiddleware *middleware.AuthMiddleware
} }
@ -61,7 +63,9 @@ func NewRouter(cfg *config.Config,
paymentMethodValidator validator.PaymentMethodValidator, paymentMethodValidator validator.PaymentMethodValidator,
analyticsService *service.AnalyticsServiceImpl, analyticsService *service.AnalyticsServiceImpl,
tableService *service.TableServiceImpl, tableService *service.TableServiceImpl,
tableValidator *validator.TableValidator) *Router { tableValidator *validator.TableValidator,
unitService handler.UnitService,
ingredientService handler.IngredientService) *Router {
return &Router{ return &Router{
config: cfg, config: cfg,
@ -80,6 +84,8 @@ func NewRouter(cfg *config.Config,
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator), paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()), analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
tableHandler: handler.NewTableHandler(tableService, tableValidator), tableHandler: handler.NewTableHandler(tableService, tableValidator),
unitHandler: handler.NewUnitHandler(unitService),
ingredientHandler: handler.NewIngredientHandler(ingredientService),
authMiddleware: authMiddleware, authMiddleware: authMiddleware,
} }
} }
@ -133,6 +139,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
adminUsers.DELETE("/:id", r.userHandler.DeleteUser) adminUsers.DELETE("/:id", r.userHandler.DeleteUser)
adminUsers.PUT("/:id/activate", r.userHandler.ActivateUser) adminUsers.PUT("/:id/activate", r.userHandler.ActivateUser)
adminUsers.PUT("/:id/deactivate", r.userHandler.DeactivateUser) adminUsers.PUT("/:id/deactivate", r.userHandler.DeactivateUser)
adminUsers.POST("/select-outlet", r.userHandler.UpdateUserOutlet)
} }
users.PUT("/:id/password", r.userHandler.ChangePassword) users.PUT("/:id/password", r.userHandler.ChangePassword)
@ -160,6 +167,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
categories.DELETE("/:id", r.categoryHandler.DeleteCategory) 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 := protected.Group("/products")
products.Use(r.authMiddleware.RequireAdminOrManager()) products.Use(r.authMiddleware.RequireAdminOrManager())
{ {
@ -258,6 +275,16 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
tables.POST("/:id/release", r.tableHandler.ReleaseTable) 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 := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager()) outlets.Use(r.authMiddleware.RequireAdminOrManager())
{ {

View 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)
}

View 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)
}

View 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)
}

View 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
}

View File

@ -31,15 +31,11 @@ func NewPaymentMethodService(paymentMethodProcessor processor.PaymentMethodProce
} }
func (s *PaymentMethodServiceImpl) CreatePaymentMethod(ctx context.Context, contextInfo *appcontext.ContextInfo, req *contract.CreatePaymentMethodRequest) *contract.Response { func (s *PaymentMethodServiceImpl) CreatePaymentMethod(ctx context.Context, contextInfo *appcontext.ContextInfo, req *contract.CreatePaymentMethodRequest) *contract.Response {
// Convert contract to model
modelReq := mappers.CreatePaymentMethodContractToModel(req) modelReq := mappers.CreatePaymentMethodContractToModel(req)
// Set organization ID from context if not provided
if modelReq.OrganizationID == uuid.Nil && contextInfo != nil { if modelReq.OrganizationID == uuid.Nil && contextInfo != nil {
modelReq.OrganizationID = contextInfo.OrganizationID modelReq.OrganizationID = contextInfo.OrganizationID
} }
// Process request
response, err := s.paymentMethodProcessor.CreatePaymentMethod(ctx, modelReq) response, err := s.paymentMethodProcessor.CreatePaymentMethod(ctx, modelReq)
if err != nil { if err != nil {
return contract.BuildErrorResponse([]*contract.ResponseError{ return contract.BuildErrorResponse([]*contract.ResponseError{
@ -47,7 +43,6 @@ func (s *PaymentMethodServiceImpl) CreatePaymentMethod(ctx context.Context, cont
}) })
} }
// Convert model to contract
contractResponse := mappers.PaymentMethodResponseToContract(response) contractResponse := mappers.PaymentMethodResponseToContract(response)
return contract.BuildSuccessResponse(contractResponse) return contract.BuildSuccessResponse(contractResponse)
} }

View 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)
}

View 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)
}

View File

@ -28,7 +28,6 @@ func (v *PaymentMethodValidatorImpl) ValidateCreatePaymentMethodRequest(req *con
return err, constants.ValidationErrorCode return err, constants.ValidationErrorCode
} }
// Additional business logic validation
if req.Name == "" { if req.Name == "" {
return constants.ErrPaymentMethodNameRequired, constants.MissingFieldErrorCode return constants.ErrPaymentMethodNameRequired, constants.MissingFieldErrorCode
} }

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS units;

View 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);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS ingredients;

View 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);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS ingredient_compositions;

View File

@ -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);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS product_ingredients;

View 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);

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -0,0 +1,2 @@
-- Revert the permissions fix
ALTER TABLE users ALTER COLUMN permissions SET DEFAULT '{}'::jsonb;

View File

@ -0,0 +1,7 @@
-- Fix any existing users with invalid permissions data
UPDATE users
SET permissions = '{}'::jsonb
WHERE permissions IS NULL OR permissions = 'null'::jsonb OR permissions = '[]'::jsonb;
-- Ensure all users have valid permissions
ALTER TABLE users ALTER COLUMN permissions SET DEFAULT '{}'::jsonb;