Add Customer users
This commit is contained in:
parent
96743cf50b
commit
5741243425
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
16
internal/contract/ingredient_composition_contract.go
Normal file
16
internal/contract/ingredient_composition_contract.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientCompositionContract interface {
|
||||||
|
Create(request *models.CreateIngredientCompositionRequest, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
|
||||||
|
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
|
||||||
|
GetByParentIngredientID(parentIngredientID uuid.UUID, organizationID uuid.UUID) ([]*models.IngredientCompositionResponse, error)
|
||||||
|
GetByChildIngredientID(childIngredientID uuid.UUID, organizationID uuid.UUID) ([]*models.IngredientCompositionResponse, error)
|
||||||
|
Update(id uuid.UUID, request *models.UpdateIngredientCompositionRequest, organizationID uuid.UUID) (*models.IngredientCompositionResponse, error)
|
||||||
|
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
}
|
||||||
16
internal/contract/ingredient_contract.go
Normal file
16
internal/contract/ingredient_contract.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientContract interface {
|
||||||
|
Create(request *models.CreateIngredientRequest, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
GetAll(organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||||
|
Update(id uuid.UUID, request *models.UpdateIngredientRequest, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
UpdateStock(id uuid.UUID, quantity float64, organizationID uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
type CreateOrderRequest struct {
|
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"`
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
17
internal/contract/product_ingredient_contract.go
Normal file
17
internal/contract/product_ingredient_contract.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductIngredientContract interface {
|
||||||
|
Create(request *models.CreateProductIngredientRequest, organizationID uuid.UUID) (*models.ProductIngredientResponse, error)
|
||||||
|
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.ProductIngredientResponse, error)
|
||||||
|
GetByProductID(productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductIngredientResponse, error)
|
||||||
|
GetByIngredientID(ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductIngredientResponse, error)
|
||||||
|
Update(id uuid.UUID, request *models.UpdateProductIngredientRequest, organizationID uuid.UUID) (*models.ProductIngredientResponse, error)
|
||||||
|
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
BulkCreate(productID uuid.UUID, ingredients []models.CreateProductIngredientRequest, organizationID uuid.UUID) ([]*models.ProductIngredientResponse, error)
|
||||||
|
}
|
||||||
15
internal/contract/unit_contract.go
Normal file
15
internal/contract/unit_contract.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnitContract interface {
|
||||||
|
Create(request *models.CreateUnitRequest, organizationID uuid.UUID) (*models.UnitResponse, error)
|
||||||
|
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.UnitResponse, error)
|
||||||
|
GetAll(organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error)
|
||||||
|
Update(id uuid.UUID, request *models.UpdateUnitRequest, organizationID uuid.UUID) (*models.UnitResponse, error)
|
||||||
|
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
}
|
||||||
23
internal/entities/ingredient.go
Normal file
23
internal/entities/ingredient.go
Normal 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"`
|
||||||
|
}
|
||||||
20
internal/entities/ingredient_composition.go
Normal file
20
internal/entities/ingredient_composition.go
Normal 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"`
|
||||||
|
}
|
||||||
@ -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"`
|
||||||
|
|||||||
@ -19,6 +19,8 @@ type Product struct {
|
|||||||
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"`
|
||||||
|
UnitID *uuid.UUID `gorm:"type:uuid;index" json:"unit_id"`
|
||||||
|
HasIngredients bool `gorm:"default:false" json:"has_ingredients"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
@ -26,7 +28,9 @@ type Product struct {
|
|||||||
|
|
||||||
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"`
|
||||||
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||||
|
ProductIngredients []ProductIngredient `gorm:"foreignKey:ProductID" json:"product_ingredients,omitempty"`
|
||||||
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
|
||||||
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
22
internal/entities/product_ingredient.go
Normal file
22
internal/entities/product_ingredient.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductIngredient struct {
|
||||||
|
ID uuid.UUID `json:"id" db:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity" db:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Product *Product `json:"product,omitempty"`
|
||||||
|
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||||
|
}
|
||||||
18
internal/entities/unit.go
Normal file
18
internal/entities/unit.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Unit struct {
|
||||||
|
ID uuid.UUID `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"`
|
||||||
|
}
|
||||||
177
internal/handler/ingredient_handler.go
Normal file
177
internal/handler/ingredient_handler.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientHandler struct {
|
||||||
|
ingredientService IngredientService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIngredientHandler(ingredientService IngredientService) *IngredientHandler {
|
||||||
|
return &IngredientHandler{
|
||||||
|
ingredientService: ingredientService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IngredientHandler) Create(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var request models.CreateIngredientRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
logger.FromContext(c.Request.Context()).WithError(err).Error("IngredientHandler::Create -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request.OrganizationID = contextInfo.OrganizationID
|
||||||
|
|
||||||
|
ingredientResponse, err := h.ingredientService.CreateIngredient(ctx, &request)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> Failed to create ingredient from service")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Create")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IngredientHandler) GetByID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Invalid ingredient ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientResponse, err := h.ingredientService.GetIngredientByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Failed to get ingredient from service")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Ingredient not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetByID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IngredientHandler) GetAll(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
// Get query parameters
|
||||||
|
pageStr := c.DefaultQuery("page", "1")
|
||||||
|
limitStr := c.DefaultQuery("limit", "10")
|
||||||
|
search := c.Query("search")
|
||||||
|
outletIDStr := c.Query("outlet_id")
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(pageStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid page parameter")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid limit parameter")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var outletID *uuid.UUID
|
||||||
|
if outletIDStr != "" {
|
||||||
|
parsedOutletID, err := uuid.Parse(outletIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid outlet ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outletID = &parsedOutletID
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientResponse, err := h.ingredientService.ListIngredients(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Failed to get ingredients from service")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get ingredients")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetAll")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IngredientHandler) Update(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Invalid ingredient ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request models.UpdateIngredientRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientResponse, err := h.ingredientService.UpdateIngredient(ctx, id, &request)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Failed to update ingredient from service")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Update")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IngredientHandler) Delete(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Invalid ingredient ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.ingredientService.DeleteIngredient(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Failed to delete ingredient from service")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]interface{}{
|
||||||
|
"message": "Ingredient deleted successfully",
|
||||||
|
}), "IngredientHandler::Delete")
|
||||||
|
}
|
||||||
16
internal/handler/ingredient_service.go
Normal file
16
internal/handler/ingredient_service.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientService interface {
|
||||||
|
CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error)
|
||||||
|
UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error)
|
||||||
|
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
||||||
|
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||||
|
}
|
||||||
14
internal/handler/inventory_movement_service.go
Normal file
14
internal/handler/inventory_movement_service.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InventoryMovementService interface {
|
||||||
|
CreateInventoryMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error)
|
||||||
|
GetInventoryMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error)
|
||||||
|
ListInventoryMovements(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.InventoryMovementResponse], error)
|
||||||
|
}
|
||||||
@ -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]
|
||||||
|
|||||||
199
internal/handler/unit_handler.go
Normal file
199
internal/handler/unit_handler.go
Normal 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")
|
||||||
|
}
|
||||||
16
internal/handler/unit_service.go
Normal file
16
internal/handler/unit_service.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnitService interface {
|
||||||
|
CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error)
|
||||||
|
UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error)
|
||||||
|
DeleteUnit(ctx context.Context, id uuid.UUID) error
|
||||||
|
GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error)
|
||||||
|
ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error)
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package handler
|
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)
|
||||||
|
|||||||
70
internal/mappers/ingredient_composition_mapper.go
Normal file
70
internal/mappers/ingredient_composition_mapper.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapIngredientCompositionEntityToModel(entity *entities.IngredientComposition) *models.IngredientComposition {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.IngredientComposition{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
ParentIngredientID: entity.ParentIngredientID,
|
||||||
|
ChildIngredientID: entity.ChildIngredientID,
|
||||||
|
Quantity: entity.Quantity,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
ParentIngredient: MapIngredientEntityToModel(entity.ParentIngredient),
|
||||||
|
ChildIngredient: MapIngredientEntityToModel(entity.ChildIngredient),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapIngredientCompositionModelToEntity(model *models.IngredientComposition) *entities.IngredientComposition {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.IngredientComposition{
|
||||||
|
ID: model.ID,
|
||||||
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
|
ParentIngredientID: model.ParentIngredientID,
|
||||||
|
ChildIngredientID: model.ChildIngredientID,
|
||||||
|
Quantity: model.Quantity,
|
||||||
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
|
ParentIngredient: MapIngredientModelToEntity(model.ParentIngredient),
|
||||||
|
ChildIngredient: MapIngredientModelToEntity(model.ChildIngredient),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapIngredientCompositionEntitiesToModels(entities []*entities.IngredientComposition) []*models.IngredientComposition {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]*models.IngredientComposition, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
models[i] = MapIngredientCompositionEntityToModel(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapIngredientCompositionModelsToEntities(models []*models.IngredientComposition) []*entities.IngredientComposition {
|
||||||
|
if models == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entities := make([]*entities.IngredientComposition, len(models))
|
||||||
|
for i, model := range models {
|
||||||
|
entities[i] = MapIngredientCompositionModelToEntity(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
76
internal/mappers/ingredient_mapper.go
Normal file
76
internal/mappers/ingredient_mapper.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapIngredientEntityToModel(entity *entities.Ingredient) *models.Ingredient {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.Ingredient{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
Name: entity.Name,
|
||||||
|
UnitID: entity.UnitID,
|
||||||
|
Cost: entity.Cost,
|
||||||
|
Stock: entity.Stock,
|
||||||
|
IsSemiFinished: entity.IsSemiFinished,
|
||||||
|
IsActive: entity.IsActive,
|
||||||
|
Metadata: entity.Metadata,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
Unit: MapUnitEntityToModel(entity.Unit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapIngredientModelToEntity(model *models.Ingredient) *entities.Ingredient {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.Ingredient{
|
||||||
|
ID: model.ID,
|
||||||
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
|
Name: model.Name,
|
||||||
|
UnitID: model.UnitID,
|
||||||
|
Cost: model.Cost,
|
||||||
|
Stock: model.Stock,
|
||||||
|
IsSemiFinished: model.IsSemiFinished,
|
||||||
|
IsActive: model.IsActive,
|
||||||
|
Metadata: model.Metadata,
|
||||||
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
|
Unit: MapUnitModelToEntity(model.Unit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapIngredientEntitiesToModels(entities []*entities.Ingredient) []*models.Ingredient {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]*models.Ingredient, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
models[i] = MapIngredientEntityToModel(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapIngredientModelsToEntities(models []*models.Ingredient) []*entities.Ingredient {
|
||||||
|
if models == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entities := make([]*entities.Ingredient, len(models))
|
||||||
|
for i, model := range models {
|
||||||
|
entities[i] = MapIngredientModelToEntity(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
@ -14,11 +14,12 @@ func InventoryMovementEntityToModel(entity *entities.InventoryMovement) *models.
|
|||||||
ID: entity.ID,
|
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),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
70
internal/mappers/product_ingredient_mapper.go
Normal file
70
internal/mappers/product_ingredient_mapper.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *models.ProductIngredient {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ProductIngredient{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
ProductID: entity.ProductID,
|
||||||
|
IngredientID: entity.IngredientID,
|
||||||
|
Quantity: entity.Quantity,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
Product: ProductEntityToModel(entity.Product),
|
||||||
|
Ingredient: MapIngredientEntityToModel(entity.Ingredient),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entities.ProductIngredient {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.ProductIngredient{
|
||||||
|
ID: model.ID,
|
||||||
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
|
ProductID: model.ProductID,
|
||||||
|
IngredientID: model.IngredientID,
|
||||||
|
Quantity: model.Quantity,
|
||||||
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
|
Product: ProductModelToEntity(model.Product),
|
||||||
|
Ingredient: MapIngredientModelToEntity(model.Ingredient),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapProductIngredientEntitiesToModels(entities []*entities.ProductIngredient) []*models.ProductIngredient {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]*models.ProductIngredient, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
models[i] = MapProductIngredientEntityToModel(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapProductIngredientModelsToEntities(models []*models.ProductIngredient) []*entities.ProductIngredient {
|
||||||
|
if models == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entities := make([]*entities.ProductIngredient, len(models))
|
||||||
|
for i, model := range models {
|
||||||
|
entities[i] = MapProductIngredientModelToEntity(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
66
internal/mappers/unit_mapper.go
Normal file
66
internal/mappers/unit_mapper.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapUnitEntityToModel(entity *entities.Unit) *models.Unit {
|
||||||
|
if entity == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.Unit{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
Name: entity.Name,
|
||||||
|
Abbreviation: entity.Abbreviation,
|
||||||
|
IsActive: entity.IsActive,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapUnitModelToEntity(model *models.Unit) *entities.Unit {
|
||||||
|
if model == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entities.Unit{
|
||||||
|
ID: model.ID,
|
||||||
|
OrganizationID: model.OrganizationID,
|
||||||
|
OutletID: model.OutletID,
|
||||||
|
Name: model.Name,
|
||||||
|
Abbreviation: model.Abbreviation,
|
||||||
|
IsActive: model.IsActive,
|
||||||
|
CreatedAt: model.CreatedAt,
|
||||||
|
UpdatedAt: model.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapUnitEntitiesToModels(entities []*entities.Unit) []*models.Unit {
|
||||||
|
if entities == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]*models.Unit, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
models[i] = MapUnitEntityToModel(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapUnitModelsToEntities(models []*models.Unit) []*entities.Unit {
|
||||||
|
if models == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entities := make([]*entities.Unit, len(models))
|
||||||
|
for i, model := range models {
|
||||||
|
entities[i] = MapUnitModelToEntity(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
66
internal/models/ingredient.go
Normal file
66
internal/models/ingredient.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ingredient struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
Stock float64 `json:"stock"`
|
||||||
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Unit *Unit `json:"unit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateIngredientRequest struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||||
|
Cost float64 `json:"cost" validate:"min=0"`
|
||||||
|
Stock float64 `json:"stock" validate:"min=0"`
|
||||||
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateIngredientRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||||
|
Cost float64 `json:"cost" validate:"min=0"`
|
||||||
|
Stock float64 `json:"stock" validate:"min=0"`
|
||||||
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IngredientResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
Stock float64 `json:"stock"`
|
||||||
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Unit *Unit `json:"unit,omitempty"`
|
||||||
|
}
|
||||||
49
internal/models/ingredient_composition.go
Normal file
49
internal/models/ingredient_composition.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientComposition struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
|
||||||
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
||||||
|
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateIngredientCompositionRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ParentIngredientID uuid.UUID `json:"parent_ingredient_id" validate:"required"`
|
||||||
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateIngredientCompositionRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IngredientCompositionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
|
||||||
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
||||||
|
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
||||||
|
}
|
||||||
@ -37,7 +37,8 @@ type InventoryMovement struct {
|
|||||||
ID uuid.UUID
|
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
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
@ -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,7 +60,7 @@ 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"`
|
||||||
@ -64,6 +68,8 @@ type UpdateProductRequest struct {
|
|||||||
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"`
|
||||||
|
UnitID *uuid.UUID `validate:"omitempty"`
|
||||||
|
HasIngredients *bool `validate:"omitempty"`
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
IsActive *bool
|
IsActive *bool
|
||||||
// Stock management fields
|
// Stock management fields
|
||||||
@ -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
|
||||||
|
|||||||
49
internal/models/product_ingredient.go
Normal file
49
internal/models/product_ingredient.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductIngredient struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Product *Product `json:"product,omitempty"`
|
||||||
|
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProductIngredientRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductIngredientRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductIngredientResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Product *Product `json:"product,omitempty"`
|
||||||
|
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||||
|
}
|
||||||
44
internal/models/unit.go
Normal file
44
internal/models/unit.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Unit struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Abbreviation *string `json:"abbreviation"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUnitRequest struct {
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=50"`
|
||||||
|
Abbreviation *string `json:"abbreviation" validate:"omitempty,max=10"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUnitRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=50"`
|
||||||
|
Abbreviation *string `json:"abbreviation" validate:"omitempty,max=10"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnitResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Abbreviation *string `json:"abbreviation"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
238
internal/processor/ingredient_processor.go
Normal file
238
internal/processor/ingredient_processor.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientProcessorImpl struct {
|
||||||
|
ingredientRepo IngredientRepository
|
||||||
|
unitRepo UnitRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository) *IngredientProcessorImpl {
|
||||||
|
return &IngredientProcessorImpl{
|
||||||
|
ingredientRepo: ingredientRepo,
|
||||||
|
unitRepo: unitRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
||||||
|
// Validate unit exists
|
||||||
|
_, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ingredient entity
|
||||||
|
ingredient := &entities.Ingredient{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
Name: req.Name,
|
||||||
|
UnitID: req.UnitID,
|
||||||
|
Cost: req.Cost,
|
||||||
|
Stock: req.Stock,
|
||||||
|
IsSemiFinished: req.IsSemiFinished,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
Metadata: req.Metadata,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
err = p.ingredientRepo.Create(ctx, ingredient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get with relations
|
||||||
|
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, ingredient.ID, req.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to response
|
||||||
|
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
||||||
|
response := &models.IngredientResponse{
|
||||||
|
ID: ingredientModel.ID,
|
||||||
|
OrganizationID: ingredientModel.OrganizationID,
|
||||||
|
OutletID: ingredientModel.OutletID,
|
||||||
|
Name: ingredientModel.Name,
|
||||||
|
UnitID: ingredientModel.UnitID,
|
||||||
|
Cost: ingredientModel.Cost,
|
||||||
|
Stock: ingredientModel.Stock,
|
||||||
|
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||||
|
IsActive: ingredientModel.IsActive,
|
||||||
|
Metadata: ingredientModel.Metadata,
|
||||||
|
CreatedAt: ingredientModel.CreatedAt,
|
||||||
|
UpdatedAt: ingredientModel.UpdatedAt,
|
||||||
|
Unit: ingredientModel.Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||||
|
// For now, we'll need to get organizationID from context or request
|
||||||
|
// This is a limitation of the current interface design
|
||||||
|
organizationID := uuid.Nil // This should come from context
|
||||||
|
|
||||||
|
ingredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientModel := mappers.MapIngredientEntityToModel(ingredient)
|
||||||
|
response := &models.IngredientResponse{
|
||||||
|
ID: ingredientModel.ID,
|
||||||
|
OrganizationID: ingredientModel.OrganizationID,
|
||||||
|
OutletID: ingredientModel.OutletID,
|
||||||
|
Name: ingredientModel.Name,
|
||||||
|
UnitID: ingredientModel.UnitID,
|
||||||
|
Cost: ingredientModel.Cost,
|
||||||
|
Stock: ingredientModel.Stock,
|
||||||
|
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||||
|
IsActive: ingredientModel.IsActive,
|
||||||
|
Metadata: ingredientModel.Metadata,
|
||||||
|
CreatedAt: ingredientModel.CreatedAt,
|
||||||
|
UpdatedAt: ingredientModel.UpdatedAt,
|
||||||
|
Unit: ingredientModel.Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error) {
|
||||||
|
// Set default values
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredients, total, err := p.ingredientRepo.GetAll(ctx, organizationID, outletID, page, limit, search, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to response models
|
||||||
|
ingredientModels := mappers.MapIngredientEntitiesToModels(ingredients)
|
||||||
|
ingredientResponses := make([]models.IngredientResponse, len(ingredientModels))
|
||||||
|
|
||||||
|
for i, ingredientModel := range ingredientModels {
|
||||||
|
ingredientResponses[i] = models.IngredientResponse{
|
||||||
|
ID: ingredientModel.ID,
|
||||||
|
OrganizationID: ingredientModel.OrganizationID,
|
||||||
|
OutletID: ingredientModel.OutletID,
|
||||||
|
Name: ingredientModel.Name,
|
||||||
|
UnitID: ingredientModel.UnitID,
|
||||||
|
Cost: ingredientModel.Cost,
|
||||||
|
Stock: ingredientModel.Stock,
|
||||||
|
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||||
|
IsActive: ingredientModel.IsActive,
|
||||||
|
Metadata: ingredientModel.Metadata,
|
||||||
|
CreatedAt: ingredientModel.CreatedAt,
|
||||||
|
UpdatedAt: ingredientModel.UpdatedAt,
|
||||||
|
Unit: ingredientModel.Unit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create paginated response
|
||||||
|
paginatedResponse := &models.PaginatedResponse[models.IngredientResponse]{
|
||||||
|
Data: ingredientResponses,
|
||||||
|
Pagination: models.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Total: int64(total),
|
||||||
|
TotalPages: (total + limit - 1) / limit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return paginatedResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
||||||
|
// For now, we'll need to get organizationID from context or request
|
||||||
|
// This is a limitation of the current interface design
|
||||||
|
organizationID := uuid.Nil // This should come from context
|
||||||
|
|
||||||
|
// Get existing ingredient
|
||||||
|
existingIngredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate unit exists if changed
|
||||||
|
if req.UnitID != existingIngredient.UnitID {
|
||||||
|
_, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
existingIngredient.OutletID = req.OutletID
|
||||||
|
existingIngredient.Name = req.Name
|
||||||
|
existingIngredient.UnitID = req.UnitID
|
||||||
|
existingIngredient.Cost = req.Cost
|
||||||
|
existingIngredient.Stock = req.Stock
|
||||||
|
existingIngredient.IsSemiFinished = req.IsSemiFinished
|
||||||
|
existingIngredient.IsActive = req.IsActive
|
||||||
|
existingIngredient.Metadata = req.Metadata
|
||||||
|
existingIngredient.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
// Save to database
|
||||||
|
err = p.ingredientRepo.Update(ctx, existingIngredient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get with relations
|
||||||
|
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to response
|
||||||
|
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
||||||
|
response := &models.IngredientResponse{
|
||||||
|
ID: ingredientModel.ID,
|
||||||
|
OrganizationID: ingredientModel.OrganizationID,
|
||||||
|
OutletID: ingredientModel.OutletID,
|
||||||
|
Name: ingredientModel.Name,
|
||||||
|
UnitID: ingredientModel.UnitID,
|
||||||
|
Cost: ingredientModel.Cost,
|
||||||
|
Stock: ingredientModel.Stock,
|
||||||
|
IsSemiFinished: ingredientModel.IsSemiFinished,
|
||||||
|
IsActive: ingredientModel.IsActive,
|
||||||
|
Metadata: ingredientModel.Metadata,
|
||||||
|
CreatedAt: ingredientModel.CreatedAt,
|
||||||
|
UpdatedAt: ingredientModel.UpdatedAt,
|
||||||
|
Unit: ingredientModel.Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
||||||
|
// For now, we'll need to get organizationID from context or request
|
||||||
|
// This is a limitation of the current interface design
|
||||||
|
organizationID := uuid.Nil // This should come from context
|
||||||
|
|
||||||
|
err := p.ingredientRepo.Delete(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
17
internal/processor/ingredient_repository.go
Normal file
17
internal/processor/ingredient_repository.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientRepository interface {
|
||||||
|
Create(ctx context.Context, ingredient *entities.Ingredient) error
|
||||||
|
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error)
|
||||||
|
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error)
|
||||||
|
Update(ctx context.Context, ingredient *entities.Ingredient) error
|
||||||
|
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error
|
||||||
|
}
|
||||||
@ -47,8 +47,8 @@ func NewInventoryMovementProcessorImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *InventoryMovementProcessorImpl) CreateMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error) {
|
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) {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
166
internal/processor/unit_processor.go
Normal file
166
internal/processor/unit_processor.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnitProcessorImpl struct {
|
||||||
|
unitRepo UnitRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnitProcessor(unitRepo UnitRepository) *UnitProcessorImpl {
|
||||||
|
return &UnitProcessorImpl{
|
||||||
|
unitRepo: unitRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnitProcessorImpl) CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error) {
|
||||||
|
unit := &entities.Unit{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
Name: req.Name,
|
||||||
|
Abbreviation: req.Abbreviation,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.unitRepo.Create(ctx, unit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unitModel := mappers.MapUnitEntityToModel(unit)
|
||||||
|
response := &models.UnitResponse{
|
||||||
|
ID: unitModel.ID,
|
||||||
|
OrganizationID: unitModel.OrganizationID,
|
||||||
|
OutletID: unitModel.OutletID,
|
||||||
|
Name: unitModel.Name,
|
||||||
|
Abbreviation: unitModel.Abbreviation,
|
||||||
|
IsActive: unitModel.IsActive,
|
||||||
|
CreatedAt: unitModel.CreatedAt,
|
||||||
|
UpdatedAt: unitModel.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnitProcessorImpl) GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error) {
|
||||||
|
organizationID := uuid.Nil // This should come from context
|
||||||
|
|
||||||
|
unit, err := p.unitRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unitModel := mappers.MapUnitEntityToModel(unit)
|
||||||
|
response := &models.UnitResponse{
|
||||||
|
ID: unitModel.ID,
|
||||||
|
OrganizationID: unitModel.OrganizationID,
|
||||||
|
OutletID: unitModel.OutletID,
|
||||||
|
Name: unitModel.Name,
|
||||||
|
Abbreviation: unitModel.Abbreviation,
|
||||||
|
IsActive: unitModel.IsActive,
|
||||||
|
CreatedAt: unitModel.CreatedAt,
|
||||||
|
UpdatedAt: unitModel.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnitProcessorImpl) ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error) {
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
units, total, err := p.unitRepo.GetAll(ctx, organizationID, outletID, page, limit, search)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unitModels := mappers.MapUnitEntitiesToModels(units)
|
||||||
|
unitResponses := make([]models.UnitResponse, len(unitModels))
|
||||||
|
|
||||||
|
for i, unitModel := range unitModels {
|
||||||
|
unitResponses[i] = models.UnitResponse{
|
||||||
|
ID: unitModel.ID,
|
||||||
|
OrganizationID: unitModel.OrganizationID,
|
||||||
|
OutletID: unitModel.OutletID,
|
||||||
|
Name: unitModel.Name,
|
||||||
|
Abbreviation: unitModel.Abbreviation,
|
||||||
|
IsActive: unitModel.IsActive,
|
||||||
|
CreatedAt: unitModel.CreatedAt,
|
||||||
|
UpdatedAt: unitModel.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paginatedResponse := &models.PaginatedResponse[models.UnitResponse]{
|
||||||
|
Data: unitResponses,
|
||||||
|
Pagination: models.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Total: int64(total),
|
||||||
|
TotalPages: (total + limit - 1) / limit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return paginatedResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnitProcessorImpl) UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error) {
|
||||||
|
organizationID := uuid.Nil
|
||||||
|
|
||||||
|
existingUnit, err := p.unitRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUnit.OutletID = req.OutletID
|
||||||
|
existingUnit.Name = req.Name
|
||||||
|
existingUnit.Abbreviation = req.Abbreviation
|
||||||
|
existingUnit.IsActive = req.IsActive
|
||||||
|
existingUnit.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
err = p.unitRepo.Update(ctx, existingUnit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unitModel := mappers.MapUnitEntityToModel(existingUnit)
|
||||||
|
response := &models.UnitResponse{
|
||||||
|
ID: unitModel.ID,
|
||||||
|
OrganizationID: unitModel.OrganizationID,
|
||||||
|
OutletID: unitModel.OutletID,
|
||||||
|
Name: unitModel.Name,
|
||||||
|
Abbreviation: unitModel.Abbreviation,
|
||||||
|
IsActive: unitModel.IsActive,
|
||||||
|
CreatedAt: unitModel.CreatedAt,
|
||||||
|
UpdatedAt: unitModel.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UnitProcessorImpl) DeleteUnit(ctx context.Context, id uuid.UUID) error {
|
||||||
|
organizationID := uuid.Nil
|
||||||
|
|
||||||
|
err := p.unitRepo.Delete(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
153
internal/processor/unit_processor_test.go
Normal file
153
internal/processor/unit_processor_test.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockUnitRepository is a mock implementation of the unit repository
|
||||||
|
type MockUnitRepository struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUnitRepository) Create(ctx context.Context, unit *models.Unit) error {
|
||||||
|
args := m.Called(ctx, unit)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUnitRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.Unit, error) {
|
||||||
|
args := m.Called(ctx, id, organizationID)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*models.Unit), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUnitRepository) GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*models.Unit, int, error) {
|
||||||
|
args := m.Called(ctx, organizationID, outletID, page, limit, search)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Int(1), args.Error(2)
|
||||||
|
}
|
||||||
|
return args.Get(0).([]*models.Unit), args.Int(1), args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUnitRepository) Update(ctx context.Context, unit *models.Unit) error {
|
||||||
|
args := m.Called(ctx, unit)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockUnitRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
args := m.Called(ctx, id, organizationID)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnitProcessor_Create(t *testing.T) {
|
||||||
|
// Create mock repository
|
||||||
|
mockRepo := &MockUnitRepository{}
|
||||||
|
|
||||||
|
// Create processor
|
||||||
|
processor := NewUnitProcessor(mockRepo)
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
organizationID := uuid.New()
|
||||||
|
request := &models.CreateUnitRequest{
|
||||||
|
Name: "Gram",
|
||||||
|
Abbreviation: "g",
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock expectations
|
||||||
|
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*models.Unit")).Return(nil)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
result, err := processor.Create(request, organizationID)
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, request.Name, result.Name)
|
||||||
|
assert.Equal(t, request.Abbreviation, result.Abbreviation)
|
||||||
|
assert.Equal(t, request.IsActive, result.IsActive)
|
||||||
|
assert.Equal(t, organizationID, result.OrganizationID)
|
||||||
|
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnitProcessor_GetByID(t *testing.T) {
|
||||||
|
// Create mock repository
|
||||||
|
mockRepo := &MockUnitRepository{}
|
||||||
|
|
||||||
|
// Create processor
|
||||||
|
processor := NewUnitProcessor(mockRepo)
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
unitID := uuid.New()
|
||||||
|
organizationID := uuid.New()
|
||||||
|
expectedUnit := &models.Unit{
|
||||||
|
ID: unitID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
Name: "Gram",
|
||||||
|
Abbreviation: "g",
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock expectations
|
||||||
|
mockRepo.On("GetByID", mock.Anything, unitID, organizationID).Return(expectedUnit, nil)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
result, err := processor.GetByID(unitID, organizationID)
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, expectedUnit.ID, result.ID)
|
||||||
|
assert.Equal(t, expectedUnit.Name, result.Name)
|
||||||
|
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnitProcessor_GetAll(t *testing.T) {
|
||||||
|
// Create mock repository
|
||||||
|
mockRepo := &MockUnitRepository{}
|
||||||
|
|
||||||
|
// Create processor
|
||||||
|
processor := NewUnitProcessor(mockRepo)
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
organizationID := uuid.New()
|
||||||
|
expectedUnits := []*models.Unit{
|
||||||
|
{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
Name: "Gram",
|
||||||
|
Abbreviation: "g",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
Name: "Liter",
|
||||||
|
Abbreviation: "L",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock expectations
|
||||||
|
mockRepo.On("GetAll", mock.Anything, organizationID, (*uuid.UUID)(nil), 1, 10, "").Return(expectedUnits, 2, nil)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
result, err := processor.GetAll(organizationID, nil, 1, 10, "")
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Len(t, result.Data, 2)
|
||||||
|
assert.Equal(t, 2, result.Pagination.Total)
|
||||||
|
|
||||||
|
mockRepo.AssertExpectations(t)
|
||||||
|
}
|
||||||
16
internal/processor/unit_repository.go
Normal file
16
internal/processor/unit_repository.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnitRepository interface {
|
||||||
|
Create(ctx context.Context, unit *entities.Unit) error
|
||||||
|
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error)
|
||||||
|
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error)
|
||||||
|
Update(ctx context.Context, unit *entities.Unit) error
|
||||||
|
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
}
|
||||||
287
internal/repository/ingredient_composition_repository.go
Normal file
287
internal/repository/ingredient_composition_repository.go
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientCompositionRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIngredientCompositionRepository(db *sql.DB) *IngredientCompositionRepository {
|
||||||
|
return &IngredientCompositionRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) Create(ctx context.Context, composition *entities.IngredientComposition) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO ingredient_compositions (id, organization_id, outlet_id, parent_ingredient_id, child_ingredient_id, quantity, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
composition.ID,
|
||||||
|
composition.OrganizationID,
|
||||||
|
composition.OutletID,
|
||||||
|
composition.ParentIngredientID,
|
||||||
|
composition.ChildIngredientID,
|
||||||
|
composition.Quantity,
|
||||||
|
composition.CreatedAt,
|
||||||
|
composition.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientComposition, error) {
|
||||||
|
query := `
|
||||||
|
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
||||||
|
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
||||||
|
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
||||||
|
FROM ingredient_compositions ic
|
||||||
|
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
||||||
|
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
||||||
|
WHERE ic.id = $1 AND ic.organization_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
composition := &entities.IngredientComposition{}
|
||||||
|
parentIngredient := &entities.Ingredient{}
|
||||||
|
childIngredient := &entities.Ingredient{}
|
||||||
|
|
||||||
|
err := r.db.QueryRowContext(ctx, query, id, organizationID).Scan(
|
||||||
|
&composition.ID,
|
||||||
|
&composition.OrganizationID,
|
||||||
|
&composition.OutletID,
|
||||||
|
&composition.ParentIngredientID,
|
||||||
|
&composition.ChildIngredientID,
|
||||||
|
&composition.Quantity,
|
||||||
|
&composition.CreatedAt,
|
||||||
|
&composition.UpdatedAt,
|
||||||
|
&parentIngredient.ID,
|
||||||
|
&parentIngredient.OrganizationID,
|
||||||
|
&parentIngredient.OutletID,
|
||||||
|
&parentIngredient.Name,
|
||||||
|
&parentIngredient.UnitID,
|
||||||
|
&parentIngredient.Cost,
|
||||||
|
&parentIngredient.Stock,
|
||||||
|
&parentIngredient.IsSemiFinished,
|
||||||
|
&parentIngredient.IsActive,
|
||||||
|
&parentIngredient.Metadata,
|
||||||
|
&parentIngredient.CreatedAt,
|
||||||
|
&parentIngredient.UpdatedAt,
|
||||||
|
&childIngredient.ID,
|
||||||
|
&childIngredient.OrganizationID,
|
||||||
|
&childIngredient.OutletID,
|
||||||
|
&childIngredient.Name,
|
||||||
|
&childIngredient.UnitID,
|
||||||
|
&childIngredient.Cost,
|
||||||
|
&childIngredient.Stock,
|
||||||
|
&childIngredient.IsSemiFinished,
|
||||||
|
&childIngredient.IsActive,
|
||||||
|
&childIngredient.Metadata,
|
||||||
|
&childIngredient.CreatedAt,
|
||||||
|
&childIngredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
composition.ParentIngredient = parentIngredient
|
||||||
|
composition.ChildIngredient = childIngredient
|
||||||
|
return composition, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) GetByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error) {
|
||||||
|
query := `
|
||||||
|
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
||||||
|
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
||||||
|
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
||||||
|
FROM ingredient_compositions ic
|
||||||
|
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
||||||
|
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
||||||
|
WHERE ic.parent_ingredient_id = $1 AND ic.organization_id = $2
|
||||||
|
ORDER BY ic.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, parentIngredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var compositions []*entities.IngredientComposition
|
||||||
|
for rows.Next() {
|
||||||
|
composition := &entities.IngredientComposition{}
|
||||||
|
parentIngredient := &entities.Ingredient{}
|
||||||
|
childIngredient := &entities.Ingredient{}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&composition.ID,
|
||||||
|
&composition.OrganizationID,
|
||||||
|
&composition.OutletID,
|
||||||
|
&composition.ParentIngredientID,
|
||||||
|
&composition.ChildIngredientID,
|
||||||
|
&composition.Quantity,
|
||||||
|
&composition.CreatedAt,
|
||||||
|
&composition.UpdatedAt,
|
||||||
|
&parentIngredient.ID,
|
||||||
|
&parentIngredient.OrganizationID,
|
||||||
|
&parentIngredient.OutletID,
|
||||||
|
&parentIngredient.Name,
|
||||||
|
&parentIngredient.UnitID,
|
||||||
|
&parentIngredient.Cost,
|
||||||
|
&parentIngredient.Stock,
|
||||||
|
&parentIngredient.IsSemiFinished,
|
||||||
|
&parentIngredient.IsActive,
|
||||||
|
&parentIngredient.Metadata,
|
||||||
|
&parentIngredient.CreatedAt,
|
||||||
|
&parentIngredient.UpdatedAt,
|
||||||
|
&childIngredient.ID,
|
||||||
|
&childIngredient.OrganizationID,
|
||||||
|
&childIngredient.OutletID,
|
||||||
|
&childIngredient.Name,
|
||||||
|
&childIngredient.UnitID,
|
||||||
|
&childIngredient.Cost,
|
||||||
|
&childIngredient.Stock,
|
||||||
|
&childIngredient.IsSemiFinished,
|
||||||
|
&childIngredient.IsActive,
|
||||||
|
&childIngredient.Metadata,
|
||||||
|
&childIngredient.CreatedAt,
|
||||||
|
&childIngredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
composition.ParentIngredient = parentIngredient
|
||||||
|
composition.ChildIngredient = childIngredient
|
||||||
|
compositions = append(compositions, composition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compositions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) GetByChildIngredientID(ctx context.Context, childIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error) {
|
||||||
|
query := `
|
||||||
|
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
||||||
|
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
||||||
|
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
||||||
|
FROM ingredient_compositions ic
|
||||||
|
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
||||||
|
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
||||||
|
WHERE ic.child_ingredient_id = $1 AND ic.organization_id = $2
|
||||||
|
ORDER BY ic.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, childIngredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var compositions []*entities.IngredientComposition
|
||||||
|
for rows.Next() {
|
||||||
|
composition := &entities.IngredientComposition{}
|
||||||
|
parentIngredient := &entities.Ingredient{}
|
||||||
|
childIngredient := &entities.Ingredient{}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&composition.ID,
|
||||||
|
&composition.OrganizationID,
|
||||||
|
&composition.OutletID,
|
||||||
|
&composition.ParentIngredientID,
|
||||||
|
&composition.ChildIngredientID,
|
||||||
|
&composition.Quantity,
|
||||||
|
&composition.CreatedAt,
|
||||||
|
&composition.UpdatedAt,
|
||||||
|
&parentIngredient.ID,
|
||||||
|
&parentIngredient.OrganizationID,
|
||||||
|
&parentIngredient.OutletID,
|
||||||
|
&parentIngredient.Name,
|
||||||
|
&parentIngredient.UnitID,
|
||||||
|
&parentIngredient.Cost,
|
||||||
|
&parentIngredient.Stock,
|
||||||
|
&parentIngredient.IsSemiFinished,
|
||||||
|
&parentIngredient.IsActive,
|
||||||
|
&parentIngredient.Metadata,
|
||||||
|
&parentIngredient.CreatedAt,
|
||||||
|
&parentIngredient.UpdatedAt,
|
||||||
|
&childIngredient.ID,
|
||||||
|
&childIngredient.OrganizationID,
|
||||||
|
&childIngredient.OutletID,
|
||||||
|
&childIngredient.Name,
|
||||||
|
&childIngredient.UnitID,
|
||||||
|
&childIngredient.Cost,
|
||||||
|
&childIngredient.Stock,
|
||||||
|
&childIngredient.IsSemiFinished,
|
||||||
|
&childIngredient.IsActive,
|
||||||
|
&childIngredient.Metadata,
|
||||||
|
&childIngredient.CreatedAt,
|
||||||
|
&childIngredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
composition.ParentIngredient = parentIngredient
|
||||||
|
composition.ChildIngredient = childIngredient
|
||||||
|
compositions = append(compositions, composition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compositions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) Update(ctx context.Context, composition *entities.IngredientComposition) error {
|
||||||
|
query := `
|
||||||
|
UPDATE ingredient_compositions
|
||||||
|
SET outlet_id = $1, quantity = $2, updated_at = $3
|
||||||
|
WHERE id = $4 AND organization_id = $5
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := r.db.ExecContext(ctx, query,
|
||||||
|
composition.OutletID,
|
||||||
|
composition.Quantity,
|
||||||
|
composition.UpdatedAt,
|
||||||
|
composition.ID,
|
||||||
|
composition.OrganizationID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
query := `DELETE FROM ingredient_compositions WHERE id = $1 AND organization_id = $2`
|
||||||
|
|
||||||
|
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
102
internal/repository/ingredient_repository.go
Normal file
102
internal/repository/ingredient_repository.go
Normal 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
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
309
internal/repository/product_ingredient_repository.go
Normal file
309
internal/repository/product_ingredient_repository.go
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductIngredientRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductIngredientRepository(db *sql.DB) *ProductIngredientRepository {
|
||||||
|
return &ProductIngredientRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) Create(ctx context.Context, productIngredient *entities.ProductIngredient) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO product_ingredients (id, organization_id, outlet_id, product_id, ingredient_id, quantity, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
productIngredient.ID,
|
||||||
|
productIngredient.OrganizationID,
|
||||||
|
productIngredient.OutletID,
|
||||||
|
productIngredient.ProductID,
|
||||||
|
productIngredient.IngredientID,
|
||||||
|
productIngredient.Quantity,
|
||||||
|
productIngredient.CreatedAt,
|
||||||
|
productIngredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductIngredient, error) {
|
||||||
|
query := `
|
||||||
|
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||||
|
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||||
|
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||||
|
FROM product_ingredients pi
|
||||||
|
LEFT JOIN products p ON pi.product_id = p.id
|
||||||
|
LEFT JOIN ingredients i ON pi.ingredient_id = i.id
|
||||||
|
WHERE pi.id = $1 AND pi.organization_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
productIngredient := &entities.ProductIngredient{}
|
||||||
|
product := &entities.Product{}
|
||||||
|
ingredient := &entities.Ingredient{}
|
||||||
|
|
||||||
|
err := r.db.QueryRowContext(ctx, query, id, organizationID).Scan(
|
||||||
|
&productIngredient.ID,
|
||||||
|
&productIngredient.OrganizationID,
|
||||||
|
&productIngredient.OutletID,
|
||||||
|
&productIngredient.ProductID,
|
||||||
|
&productIngredient.IngredientID,
|
||||||
|
&productIngredient.Quantity,
|
||||||
|
&productIngredient.CreatedAt,
|
||||||
|
&productIngredient.UpdatedAt,
|
||||||
|
&product.ID,
|
||||||
|
&product.OrganizationID,
|
||||||
|
&product.CategoryID,
|
||||||
|
&product.SKU,
|
||||||
|
&product.Name,
|
||||||
|
&product.Description,
|
||||||
|
&product.Price,
|
||||||
|
&product.Cost,
|
||||||
|
&product.BusinessType,
|
||||||
|
&product.ImageURL,
|
||||||
|
&product.PrinterType,
|
||||||
|
&product.UnitID,
|
||||||
|
&product.HasIngredients,
|
||||||
|
&product.Metadata,
|
||||||
|
&product.IsActive,
|
||||||
|
&product.CreatedAt,
|
||||||
|
&product.UpdatedAt,
|
||||||
|
&ingredient.ID,
|
||||||
|
&ingredient.OrganizationID,
|
||||||
|
&ingredient.OutletID,
|
||||||
|
&ingredient.Name,
|
||||||
|
&ingredient.UnitID,
|
||||||
|
&ingredient.Cost,
|
||||||
|
&ingredient.Stock,
|
||||||
|
&ingredient.IsSemiFinished,
|
||||||
|
&ingredient.IsActive,
|
||||||
|
&ingredient.Metadata,
|
||||||
|
&ingredient.CreatedAt,
|
||||||
|
&ingredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
productIngredient.Product = product
|
||||||
|
productIngredient.Ingredient = ingredient
|
||||||
|
return productIngredient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) {
|
||||||
|
query := `
|
||||||
|
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||||
|
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||||
|
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||||
|
FROM product_ingredients pi
|
||||||
|
LEFT JOIN products p ON pi.product_id = p.id
|
||||||
|
LEFT JOIN ingredients i ON pi.ingredient_id = i.id
|
||||||
|
WHERE pi.product_id = $1 AND pi.organization_id = $2
|
||||||
|
ORDER BY pi.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, productID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var productIngredients []*entities.ProductIngredient
|
||||||
|
for rows.Next() {
|
||||||
|
productIngredient := &entities.ProductIngredient{}
|
||||||
|
product := &entities.Product{}
|
||||||
|
ingredient := &entities.Ingredient{}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&productIngredient.ID,
|
||||||
|
&productIngredient.OrganizationID,
|
||||||
|
&productIngredient.OutletID,
|
||||||
|
&productIngredient.ProductID,
|
||||||
|
&productIngredient.IngredientID,
|
||||||
|
&productIngredient.Quantity,
|
||||||
|
&productIngredient.CreatedAt,
|
||||||
|
&productIngredient.UpdatedAt,
|
||||||
|
&product.ID,
|
||||||
|
&product.OrganizationID,
|
||||||
|
&product.CategoryID,
|
||||||
|
&product.SKU,
|
||||||
|
&product.Name,
|
||||||
|
&product.Description,
|
||||||
|
&product.Price,
|
||||||
|
&product.Cost,
|
||||||
|
&product.BusinessType,
|
||||||
|
&product.ImageURL,
|
||||||
|
&product.PrinterType,
|
||||||
|
&product.UnitID,
|
||||||
|
&product.HasIngredients,
|
||||||
|
&product.Metadata,
|
||||||
|
&product.IsActive,
|
||||||
|
&product.CreatedAt,
|
||||||
|
&product.UpdatedAt,
|
||||||
|
&ingredient.ID,
|
||||||
|
&ingredient.OrganizationID,
|
||||||
|
&ingredient.OutletID,
|
||||||
|
&ingredient.Name,
|
||||||
|
&ingredient.UnitID,
|
||||||
|
&ingredient.Cost,
|
||||||
|
&ingredient.Stock,
|
||||||
|
&ingredient.IsSemiFinished,
|
||||||
|
&ingredient.IsActive,
|
||||||
|
&ingredient.Metadata,
|
||||||
|
&ingredient.CreatedAt,
|
||||||
|
&ingredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
productIngredient.Product = product
|
||||||
|
productIngredient.Ingredient = ingredient
|
||||||
|
productIngredients = append(productIngredients, productIngredient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return productIngredients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error) {
|
||||||
|
query := `
|
||||||
|
SELECT pi.id, pi.organization_id, pi.outlet_id, pi.product_id, pi.ingredient_id, pi.quantity, pi.created_at, pi.updated_at,
|
||||||
|
p.id, p.organization_id, p.category_id, p.sku, p.name, p.description, p.price, p.cost, p.business_type, p.image_url, p.printer_type, p.unit_id, p.has_ingredients, p.metadata, p.is_active, p.created_at, p.updated_at,
|
||||||
|
i.id, i.organization_id, i.outlet_id, i.name, i.unit_id, i.cost, i.stock, i.is_semi_finished, i.is_active, i.metadata, i.created_at, i.updated_at
|
||||||
|
FROM product_ingredients pi
|
||||||
|
LEFT JOIN products p ON pi.product_id = p.id
|
||||||
|
LEFT JOIN ingredients i ON pi.ingredient_id = i.id
|
||||||
|
WHERE pi.ingredient_id = $1 AND pi.organization_id = $2
|
||||||
|
ORDER BY pi.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, ingredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var productIngredients []*entities.ProductIngredient
|
||||||
|
for rows.Next() {
|
||||||
|
productIngredient := &entities.ProductIngredient{}
|
||||||
|
product := &entities.Product{}
|
||||||
|
ingredient := &entities.Ingredient{}
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&productIngredient.ID,
|
||||||
|
&productIngredient.OrganizationID,
|
||||||
|
&productIngredient.OutletID,
|
||||||
|
&productIngredient.ProductID,
|
||||||
|
&productIngredient.IngredientID,
|
||||||
|
&productIngredient.Quantity,
|
||||||
|
&productIngredient.CreatedAt,
|
||||||
|
&productIngredient.UpdatedAt,
|
||||||
|
&product.ID,
|
||||||
|
&product.OrganizationID,
|
||||||
|
&product.CategoryID,
|
||||||
|
&product.SKU,
|
||||||
|
&product.Name,
|
||||||
|
&product.Description,
|
||||||
|
&product.Price,
|
||||||
|
&product.Cost,
|
||||||
|
&product.BusinessType,
|
||||||
|
&product.ImageURL,
|
||||||
|
&product.PrinterType,
|
||||||
|
&product.UnitID,
|
||||||
|
&product.HasIngredients,
|
||||||
|
&product.Metadata,
|
||||||
|
&product.IsActive,
|
||||||
|
&product.CreatedAt,
|
||||||
|
&product.UpdatedAt,
|
||||||
|
&ingredient.ID,
|
||||||
|
&ingredient.OrganizationID,
|
||||||
|
&ingredient.OutletID,
|
||||||
|
&ingredient.Name,
|
||||||
|
&ingredient.UnitID,
|
||||||
|
&ingredient.Cost,
|
||||||
|
&ingredient.Stock,
|
||||||
|
&ingredient.IsSemiFinished,
|
||||||
|
&ingredient.IsActive,
|
||||||
|
&ingredient.Metadata,
|
||||||
|
&ingredient.CreatedAt,
|
||||||
|
&ingredient.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
productIngredient.Product = product
|
||||||
|
productIngredient.Ingredient = ingredient
|
||||||
|
productIngredients = append(productIngredients, productIngredient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return productIngredients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) Update(ctx context.Context, productIngredient *entities.ProductIngredient) error {
|
||||||
|
query := `
|
||||||
|
UPDATE product_ingredients
|
||||||
|
SET outlet_id = $1, quantity = $2, updated_at = $3
|
||||||
|
WHERE id = $4 AND organization_id = $5
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := r.db.ExecContext(ctx, query,
|
||||||
|
productIngredient.OutletID,
|
||||||
|
productIngredient.Quantity,
|
||||||
|
productIngredient.UpdatedAt,
|
||||||
|
productIngredient.ID,
|
||||||
|
productIngredient.OrganizationID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
query := `DELETE FROM product_ingredients WHERE id = $1 AND organization_id = $2`
|
||||||
|
|
||||||
|
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductIngredientRepository) DeleteByProductID(ctx context.Context, productID, organizationID uuid.UUID) error {
|
||||||
|
query := `DELETE FROM product_ingredients WHERE product_id = $1 AND organization_id = $2`
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx, query, productID, organizationID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
84
internal/repository/unit_repository.go
Normal file
84
internal/repository/unit_repository.go
Normal 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
|
||||||
|
}
|
||||||
@ -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())
|
||||||
{
|
{
|
||||||
|
|||||||
16
internal/service/ingredient_processor.go
Normal file
16
internal/service/ingredient_processor.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientProcessor interface {
|
||||||
|
CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error)
|
||||||
|
UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error)
|
||||||
|
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
||||||
|
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||||
|
}
|
||||||
38
internal/service/ingredient_service.go
Normal file
38
internal/service/ingredient_service.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngredientServiceImpl struct {
|
||||||
|
ingredientProcessor IngredientProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIngredientService(ingredientProcessor IngredientProcessor) *IngredientServiceImpl {
|
||||||
|
return &IngredientServiceImpl{
|
||||||
|
ingredientProcessor: ingredientProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
||||||
|
return s.ingredientProcessor.CreateIngredient(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
||||||
|
return s.ingredientProcessor.UpdateIngredient(ctx, id, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.ingredientProcessor.DeleteIngredient(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||||
|
return s.ingredientProcessor.GetIngredientByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error) {
|
||||||
|
return s.ingredientProcessor.ListIngredients(ctx, organizationID, outletID, page, limit, search)
|
||||||
|
}
|
||||||
14
internal/service/inventory_movement_processor.go
Normal file
14
internal/service/inventory_movement_processor.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InventoryMovementProcessor interface {
|
||||||
|
CreateInventoryMovement(ctx context.Context, req *models.CreateInventoryMovementRequest) (*models.InventoryMovementResponse, error)
|
||||||
|
GetInventoryMovementByID(ctx context.Context, id uuid.UUID) (*models.InventoryMovementResponse, error)
|
||||||
|
ListInventoryMovements(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.InventoryMovementResponse], error)
|
||||||
|
}
|
||||||
96
internal/service/inventory_movement_service.go
Normal file
96
internal/service/inventory_movement_service.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/repository"
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InventoryMovementService interface {
|
||||||
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InventoryMovementServiceImpl struct {
|
||||||
|
inventoryMovementRepo repository.InventoryMovementRepository
|
||||||
|
ingredientRepo *repository.IngredientRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInventoryMovementService(inventoryMovementRepo repository.InventoryMovementRepository, ingredientRepo *repository.IngredientRepository) InventoryMovementService {
|
||||||
|
return &InventoryMovementServiceImpl{
|
||||||
|
inventoryMovementRepo: inventoryMovementRepo,
|
||||||
|
ingredientRepo: ingredientRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InventoryMovementServiceImpl) CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
||||||
|
ingredient, err := s.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
previousQuantity := ingredient.Stock
|
||||||
|
newQuantity := previousQuantity + quantity
|
||||||
|
|
||||||
|
movement := &entities.InventoryMovement{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
OutletID: outletID,
|
||||||
|
ItemID: ingredientID,
|
||||||
|
ItemType: "INGREDIENT",
|
||||||
|
MovementType: movementType,
|
||||||
|
Quantity: quantity,
|
||||||
|
PreviousQuantity: previousQuantity,
|
||||||
|
NewQuantity: newQuantity,
|
||||||
|
UnitCost: unitCost,
|
||||||
|
TotalCost: unitCost * quantity,
|
||||||
|
ReferenceType: referenceType,
|
||||||
|
ReferenceID: referenceID,
|
||||||
|
UserID: userID,
|
||||||
|
Reason: &reason,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.inventoryMovementRepo.Create(ctx, movement)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.ingredientRepo.UpdateStock(ctx, ingredientID, quantity, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InventoryMovementServiceImpl) CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error {
|
||||||
|
movement := &entities.InventoryMovement{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
OutletID: outletID,
|
||||||
|
ItemID: productID,
|
||||||
|
ItemType: "PRODUCT",
|
||||||
|
MovementType: movementType,
|
||||||
|
Quantity: quantity,
|
||||||
|
PreviousQuantity: 0, // TODO This would be fetched from product inventory
|
||||||
|
NewQuantity: 0, // TODO This would be calculated
|
||||||
|
UnitCost: unitCost,
|
||||||
|
TotalCost: unitCost * quantity,
|
||||||
|
ReferenceType: referenceType,
|
||||||
|
ReferenceID: referenceID,
|
||||||
|
UserID: userID,
|
||||||
|
Reason: &reason,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.inventoryMovementRepo.Create(ctx, movement)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
16
internal/service/unit_processor.go
Normal file
16
internal/service/unit_processor.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnitProcessor interface {
|
||||||
|
CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error)
|
||||||
|
UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error)
|
||||||
|
DeleteUnit(ctx context.Context, id uuid.UUID) error
|
||||||
|
GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error)
|
||||||
|
ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error)
|
||||||
|
}
|
||||||
38
internal/service/unit_service.go
Normal file
38
internal/service/unit_service.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnitServiceImpl struct {
|
||||||
|
unitProcessor UnitProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnitService(unitProcessor UnitProcessor) *UnitServiceImpl {
|
||||||
|
return &UnitServiceImpl{
|
||||||
|
unitProcessor: unitProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnitServiceImpl) CreateUnit(ctx context.Context, req *models.CreateUnitRequest) (*models.UnitResponse, error) {
|
||||||
|
return s.unitProcessor.CreateUnit(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnitServiceImpl) UpdateUnit(ctx context.Context, id uuid.UUID, req *models.UpdateUnitRequest) (*models.UnitResponse, error) {
|
||||||
|
return s.unitProcessor.UpdateUnit(ctx, id, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnitServiceImpl) DeleteUnit(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.unitProcessor.DeleteUnit(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnitServiceImpl) GetUnitByID(ctx context.Context, id uuid.UUID) (*models.UnitResponse, error) {
|
||||||
|
return s.unitProcessor.GetUnitByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnitServiceImpl) ListUnits(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.UnitResponse], error) {
|
||||||
|
return s.unitProcessor.ListUnits(ctx, organizationID, outletID, page, limit, search)
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
1
migrations/000026_create_units_table.down.sql
Normal file
1
migrations/000026_create_units_table.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS units;
|
||||||
17
migrations/000026_create_units_table.up.sql
Normal file
17
migrations/000026_create_units_table.up.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-- Units table
|
||||||
|
CREATE TABLE units (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
abbreviation VARCHAR(10),
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_units_organization_id ON units(organization_id);
|
||||||
|
CREATE INDEX idx_units_outlet_id ON units(outlet_id);
|
||||||
|
CREATE INDEX idx_units_is_active ON units(is_active);
|
||||||
|
CREATE INDEX idx_units_created_at ON units(created_at);
|
||||||
1
migrations/000027_create_ingredients_table.down.sql
Normal file
1
migrations/000027_create_ingredients_table.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS ingredients;
|
||||||
23
migrations/000027_create_ingredients_table.up.sql
Normal file
23
migrations/000027_create_ingredients_table.up.sql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
-- Ingredients table
|
||||||
|
CREATE TABLE ingredients (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
unit_id UUID NOT NULL REFERENCES units(id) ON DELETE CASCADE,
|
||||||
|
cost DECIMAL(12,2) DEFAULT 0,
|
||||||
|
stock DECIMAL(12,3) DEFAULT 0,
|
||||||
|
is_semi_finished BOOLEAN DEFAULT false,
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_ingredients_organization_id ON ingredients(organization_id);
|
||||||
|
CREATE INDEX idx_ingredients_outlet_id ON ingredients(outlet_id);
|
||||||
|
CREATE INDEX idx_ingredients_unit_id ON ingredients(unit_id);
|
||||||
|
CREATE INDEX idx_ingredients_is_semi_finished ON ingredients(is_semi_finished);
|
||||||
|
CREATE INDEX idx_ingredients_is_active ON ingredients(is_active);
|
||||||
|
CREATE INDEX idx_ingredients_created_at ON ingredients(created_at);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS ingredient_compositions;
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
-- Ingredient compositions table
|
||||||
|
CREATE TABLE ingredient_compositions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||||
|
parent_ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||||
|
child_ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||||
|
quantity DECIMAL(12,3) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_ingredient_compositions_organization_id ON ingredient_compositions(organization_id);
|
||||||
|
CREATE INDEX idx_ingredient_compositions_outlet_id ON ingredient_compositions(outlet_id);
|
||||||
|
CREATE INDEX idx_ingredient_compositions_parent_ingredient_id ON ingredient_compositions(parent_ingredient_id);
|
||||||
|
CREATE INDEX idx_ingredient_compositions_child_ingredient_id ON ingredient_compositions(child_ingredient_id);
|
||||||
|
CREATE INDEX idx_ingredient_compositions_created_at ON ingredient_compositions(created_at);
|
||||||
|
|
||||||
|
-- Unique constraint to prevent duplicate compositions
|
||||||
|
CREATE UNIQUE INDEX idx_ingredient_compositions_unique ON ingredient_compositions(parent_ingredient_id, child_ingredient_id);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS product_ingredients;
|
||||||
21
migrations/000029_create_product_ingredients_table.up.sql
Normal file
21
migrations/000029_create_product_ingredients_table.up.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-- Product ingredients table
|
||||||
|
CREATE TABLE product_ingredients (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||||
|
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||||
|
quantity DECIMAL(12,3) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_product_ingredients_organization_id ON product_ingredients(organization_id);
|
||||||
|
CREATE INDEX idx_product_ingredients_outlet_id ON product_ingredients(outlet_id);
|
||||||
|
CREATE INDEX idx_product_ingredients_product_id ON product_ingredients(product_id);
|
||||||
|
CREATE INDEX idx_product_ingredients_ingredient_id ON product_ingredients(ingredient_id);
|
||||||
|
CREATE INDEX idx_product_ingredients_created_at ON product_ingredients(created_at);
|
||||||
|
|
||||||
|
-- Unique constraint to prevent duplicate product-ingredient combinations
|
||||||
|
CREATE UNIQUE INDEX idx_product_ingredients_unique ON product_ingredients(product_id, ingredient_id);
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
-- Remove unit_id and has_ingredients from products table
|
||||||
|
ALTER TABLE products
|
||||||
|
DROP COLUMN IF EXISTS unit_id,
|
||||||
|
DROP COLUMN IF EXISTS has_ingredients;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
-- Add unit_id and has_ingredients to products table
|
||||||
|
ALTER TABLE products
|
||||||
|
ADD COLUMN unit_id UUID REFERENCES units(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN has_ingredients BOOLEAN DEFAULT false;
|
||||||
|
|
||||||
|
-- Index for unit_id
|
||||||
|
CREATE INDEX idx_products_unit_id ON products(unit_id);
|
||||||
|
CREATE INDEX idx_products_has_ingredients ON products(has_ingredients);
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
-- Revert inventory_movements table changes
|
||||||
|
-- Add back product_id column
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ADD COLUMN product_id UUID;
|
||||||
|
|
||||||
|
-- Copy item_id data back to product_id where item_type is 'PRODUCT'
|
||||||
|
UPDATE inventory_movements
|
||||||
|
SET product_id = item_id
|
||||||
|
WHERE item_type = 'PRODUCT';
|
||||||
|
|
||||||
|
-- Drop the new columns
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
DROP COLUMN item_id,
|
||||||
|
DROP COLUMN item_type;
|
||||||
|
|
||||||
|
-- Revert quantity columns to integer
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ALTER COLUMN quantity TYPE INTEGER USING quantity::integer,
|
||||||
|
ALTER COLUMN previous_quantity TYPE INTEGER USING previous_quantity::integer,
|
||||||
|
ALTER COLUMN new_quantity TYPE INTEGER USING new_quantity::integer;
|
||||||
|
|
||||||
|
-- Revert cost columns to original precision
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ALTER COLUMN unit_cost TYPE DECIMAL(10,2),
|
||||||
|
ALTER COLUMN total_cost TYPE DECIMAL(10,2);
|
||||||
|
|
||||||
|
-- Drop the new indexes
|
||||||
|
DROP INDEX IF EXISTS idx_inventory_movements_item_id;
|
||||||
|
DROP INDEX IF EXISTS idx_inventory_movements_item_type;
|
||||||
|
DROP INDEX IF EXISTS idx_inventory_movements_item_id_type;
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
-- Update inventory_movements table to support ingredients
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ADD COLUMN item_id UUID,
|
||||||
|
ADD COLUMN item_type VARCHAR(20);
|
||||||
|
|
||||||
|
-- Copy existing product_id data to item_id
|
||||||
|
UPDATE inventory_movements
|
||||||
|
SET item_id = product_id,
|
||||||
|
item_type = 'PRODUCT'
|
||||||
|
WHERE product_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Make item_id and item_type NOT NULL after data migration
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ALTER COLUMN item_id SET NOT NULL,
|
||||||
|
ALTER COLUMN item_type SET NOT NULL;
|
||||||
|
|
||||||
|
-- Drop the old product_id column
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
DROP COLUMN product_id;
|
||||||
|
|
||||||
|
-- Update quantity columns to support decimal
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ALTER COLUMN quantity TYPE DECIMAL(12,3),
|
||||||
|
ALTER COLUMN previous_quantity TYPE DECIMAL(12,3),
|
||||||
|
ALTER COLUMN new_quantity TYPE DECIMAL(12,3);
|
||||||
|
|
||||||
|
-- Update cost columns to support higher precision
|
||||||
|
ALTER TABLE inventory_movements
|
||||||
|
ALTER COLUMN unit_cost TYPE DECIMAL(12,2),
|
||||||
|
ALTER COLUMN total_cost TYPE DECIMAL(12,2);
|
||||||
|
|
||||||
|
-- Add indexes for the new structure
|
||||||
|
CREATE INDEX idx_inventory_movements_item_id ON inventory_movements(item_id);
|
||||||
|
CREATE INDEX idx_inventory_movements_item_type ON inventory_movements(item_type);
|
||||||
|
CREATE INDEX idx_inventory_movements_item_id_type ON inventory_movements(item_id, item_type);
|
||||||
2
migrations/000032_fix_user_permissions.down.sql
Normal file
2
migrations/000032_fix_user_permissions.down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Revert the permissions fix
|
||||||
|
ALTER TABLE users ALTER COLUMN permissions SET DEFAULT '{}'::jsonb;
|
||||||
7
migrations/000032_fix_user_permissions.up.sql
Normal file
7
migrations/000032_fix_user_permissions.up.sql
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user