Compare commits
10
Commits
hpp
...
f5d9fe5223
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5d9fe5223 | ||
|
|
3721fb3cd7 | ||
|
|
2d6df8e4c6 | ||
|
|
2c76962959 | ||
|
|
a7022dd4c1 | ||
|
|
3542104050 | ||
|
|
eb95459578 | ||
|
|
1a5ddd2b34 | ||
|
|
0aa280462c | ||
|
|
421475006b |
+1
-1
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y ca-certificates tzdata curl && rm -rf /
|
|||||||
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||||
COPY --from=build /out/app /app
|
COPY --from=build /out/app /app
|
||||||
ENV TZ=Asia/Jakarta
|
ENV TZ=Asia/Jakarta
|
||||||
EXPOSE 3300
|
EXPOSE 4000
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
CMD curl -fsS http://localhost:3300/health || exit 1
|
CMD curl -fsS http://localhost:3300/health || exit 1
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ func (c *Config) GetCustomerJWTExpiresTTL() int {
|
|||||||
return c.Jwt.Customer.ExpiresTTL
|
return c.Jwt.Customer.ExpiresTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetSelfOrderJWTSecret() string {
|
||||||
|
return c.Jwt.SelfOrder.Secret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetSelfOrderJWTExpiresTTL() int {
|
||||||
|
return c.Jwt.SelfOrder.ExpiresTTL
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) LogLevel() string {
|
func (c *Config) LogLevel() string {
|
||||||
return c.Log.LogLevel
|
return c.Log.LogLevel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ type Jwt struct {
|
|||||||
Token Token `mapstructure:"token"`
|
Token Token `mapstructure:"token"`
|
||||||
RefreshToken RefreshToken `mapstructure:"refresh_token"`
|
RefreshToken RefreshToken `mapstructure:"refresh_token"`
|
||||||
Customer Customer `mapstructure:"customer"`
|
Customer Customer `mapstructure:"customer"`
|
||||||
|
SelfOrder SelfOrder `mapstructure:"self_order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrder struct {
|
||||||
|
ExpiresTTL int `mapstructure:"expires-ttl"`
|
||||||
|
Secret string `mapstructure:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
APP_NAME="apskel-pos"
|
APP_NAME="apskel-pos"
|
||||||
PORT="3300"
|
PORT="4000"
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
echo "🔄 Pulling latest code..."
|
||||||
git pull
|
git pull
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ jwt:
|
|||||||
customer:
|
customer:
|
||||||
expires-ttl: 7776000
|
expires-ttl: 7776000
|
||||||
secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd"
|
secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd"
|
||||||
|
self_order:
|
||||||
|
expires-ttl: 120
|
||||||
|
secret: "S3lf0rd3r_S3ss10n_S3cr3t_K3y_2024"
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
host: 62.72.45.250
|
host: 62.72.45.250
|
||||||
|
|||||||
+21
-5
@@ -44,6 +44,16 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators := a.initValidators()
|
validators := a.initValidators()
|
||||||
middleware := a.initMiddleware(services, cfg)
|
middleware := a.initMiddleware(services, cfg)
|
||||||
healthHandler := handler.NewHealthHandler()
|
healthHandler := handler.NewHealthHandler()
|
||||||
|
selfOrderHandler := handler.NewSelfOrderHandler(
|
||||||
|
services.orderService,
|
||||||
|
services.categoryService,
|
||||||
|
services.productService,
|
||||||
|
repos.tableRepo,
|
||||||
|
repos.outletRepo,
|
||||||
|
repos.userRepo,
|
||||||
|
cfg.GetSelfOrderJWTSecret(),
|
||||||
|
cfg.GetSelfOrderJWTExpiresTTL(),
|
||||||
|
)
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -105,6 +115,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
services.customerPointsService,
|
services.customerPointsService,
|
||||||
services.spinGameService,
|
services.spinGameService,
|
||||||
middleware.customerAuthMiddleware,
|
middleware.customerAuthMiddleware,
|
||||||
|
selfOrderHandler,
|
||||||
|
middleware.selfOrderAuthMiddleware,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -170,6 +182,7 @@ type repositories struct {
|
|||||||
tableRepo *repository.TableRepository
|
tableRepo *repository.TableRepository
|
||||||
unitRepo *repository.UnitRepository
|
unitRepo *repository.UnitRepository
|
||||||
ingredientRepo *repository.IngredientRepository
|
ingredientRepo *repository.IngredientRepository
|
||||||
|
ingredientCompositionRepo *repository.IngredientCompositionRepository
|
||||||
productRecipeRepo *repository.ProductRecipeRepository
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
vendorRepo *repository.VendorRepositoryImpl
|
vendorRepo *repository.VendorRepositoryImpl
|
||||||
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
purchaseOrderRepo *repository.PurchaseOrderRepositoryImpl
|
||||||
@@ -215,6 +228,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
tableRepo: repository.NewTableRepository(a.db),
|
tableRepo: repository.NewTableRepository(a.db),
|
||||||
unitRepo: repository.NewUnitRepository(a.db),
|
unitRepo: repository.NewUnitRepository(a.db),
|
||||||
ingredientRepo: repository.NewIngredientRepository(a.db),
|
ingredientRepo: repository.NewIngredientRepository(a.db),
|
||||||
|
ingredientCompositionRepo: repository.NewIngredientCompositionRepository(a.db),
|
||||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||||
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
vendorRepo: repository.NewVendorRepositoryImpl(a.db),
|
||||||
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
purchaseOrderRepo: repository.NewPurchaseOrderRepositoryImpl(a.db),
|
||||||
@@ -302,7 +316,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
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),
|
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
|
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo, repos.ingredientCompositionRepo),
|
||||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
vendorProcessor: processor.NewVendorProcessorImpl(repos.vendorRepo),
|
||||||
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
purchaseOrderProcessor: processor.NewPurchaseOrderProcessorImpl(repos.purchaseOrderRepo, repos.vendorRepo, repos.ingredientRepo, repos.unitRepo, repos.fileRepo, inventoryMovementService, repos.unitConverterRepo),
|
||||||
@@ -437,14 +451,16 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
}
|
}
|
||||||
|
|
||||||
type middlewares struct {
|
type middlewares struct {
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
|
selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares {
|
func (a *App) initMiddleware(services *services, cfg *config.Config) *middlewares {
|
||||||
return &middlewares{
|
return &middlewares{
|
||||||
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
authMiddleware: middleware.NewAuthMiddleware(services.authService),
|
||||||
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
|
customerAuthMiddleware: middleware.NewCustomerAuthMiddleware(cfg.GetCustomerJWTSecret()),
|
||||||
|
selfOrderAuthMiddleware: middleware.NewSelfOrderAuthMiddleware(cfg.GetSelfOrderJWTSecret()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const (
|
|||||||
VendorServiceEntity = "vendor_service"
|
VendorServiceEntity = "vendor_service"
|
||||||
PurchaseOrderServiceEntity = "purchase_order_service"
|
PurchaseOrderServiceEntity = "purchase_order_service"
|
||||||
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
IngredientUnitConverterServiceEntity = "ingredient_unit_converter_service"
|
||||||
|
IngredientCompositionServiceEntity = "ingredient_composition_service"
|
||||||
TableEntity = "table"
|
TableEntity = "table"
|
||||||
// Gamification entities
|
// Gamification entities
|
||||||
CustomerPointsEntity = "customer_points"
|
CustomerPointsEntity = "customer_points"
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelfOrderSessionRequest struct {
|
||||||
|
TableID uuid.UUID `json:"table_id" validate:"required"`
|
||||||
|
CustomerName string `json:"customer_name" validate:"required"`
|
||||||
|
Phone *string `json:"phone,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderSessionResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt int64 `json:"expires_at"`
|
||||||
|
TableID uuid.UUID `json:"table_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderMenuResponse struct {
|
||||||
|
OutletName string `json:"outlet_name"`
|
||||||
|
TableName string `json:"table_name"`
|
||||||
|
Categories []SelfOrderMenuCategory `json:"categories"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderMenuCategory struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Products []SelfOrderMenuItem `json:"products"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderMenuItem struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
ImageURL *string `json:"image_url,omitempty"`
|
||||||
|
Variants []SelfOrderMenuVariant `json:"variants,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderMenuVariant struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PriceModifier float64 `json:"price_modifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderCreateOrderRequest struct {
|
||||||
|
Phone *string `json:"phone,omitempty"`
|
||||||
|
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderCreateOrderItem struct {
|
||||||
|
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||||
|
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderCategoryItem struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfOrderListCategoriesResponse struct {
|
||||||
|
Categories []SelfOrderCategoryItem `json:"categories"`
|
||||||
|
}
|
||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Ingredient struct {
|
type Ingredient 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"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
||||||
Name string `gorm:"not null;size:255" json:"name"`
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
UnitID uuid.UUID `gorm:"type:uuid;not null;index" json:"unit_id"`
|
UnitID uuid.UUID `gorm:"type:uuid;not null;index" json:"unit_id"`
|
||||||
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
|
Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost"`
|
||||||
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
|
Stock float64 `gorm:"type:decimal(10,2);default:0.00" json:"stock"`
|
||||||
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
|
IsSemiFinished bool `gorm:"default:false" json:"is_semi_finished"`
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID;references:ID" json:"unit,omitempty"`
|
Unit *Unit `gorm:"foreignKey:UnitID;references:ID" json:"unit,omitempty"`
|
||||||
|
Compositions []IngredientComposition `gorm:"foreignKey:ParentIngredientID;references:ID" json:"compositions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type IngredientComposition struct {
|
type IngredientComposition struct {
|
||||||
ID uuid.UUID `json:"id" db:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id" db:"organization_id"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id" db:"outlet_id"`
|
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
||||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id" db:"parent_ingredient_id"`
|
ParentIngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"parent_ingredient_id"`
|
||||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id" db:"child_ingredient_id"`
|
ChildIngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"child_ingredient_id"`
|
||||||
Quantity float64 `json:"quantity" db:"quantity"`
|
Quantity float64 `gorm:"type:decimal(10,4);not null" json:"quantity"`
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
ParentIngredient *Ingredient `gorm:"foreignKey:ParentIngredientID;references:ID" json:"parent_ingredient,omitempty"`
|
||||||
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
ChildIngredient *Ingredient `gorm:"foreignKey:ChildIngredientID;references:ID" json:"child_ingredient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ type IngredientHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewIngredientHandler(ingredientService IngredientService) *IngredientHandler {
|
func NewIngredientHandler(ingredientService IngredientService) *IngredientHandler {
|
||||||
return &IngredientHandler{
|
return &IngredientHandler{ingredientService: ingredientService}
|
||||||
ingredientService: ingredientService,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IngredientHandler) Create(c *gin.Context) {
|
func (h *IngredientHandler) Create(c *gin.Context) {
|
||||||
@@ -29,53 +27,53 @@ func (h *IngredientHandler) Create(c *gin.Context) {
|
|||||||
|
|
||||||
var request models.CreateIngredientRequest
|
var request models.CreateIngredientRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.FromContext(c.Request.Context()).WithError(err).Error("IngredientHandler::Create -> request binding failed")
|
logger.FromContext(ctx).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{
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::Create")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
request.OrganizationID = contextInfo.OrganizationID
|
request.OrganizationID = contextInfo.OrganizationID
|
||||||
|
|
||||||
ingredientResponse, err := h.ingredientService.CreateIngredient(ctx, &request)
|
resp, err := h.ingredientService.CreateIngredient(ctx, &request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> Failed to create ingredient from service")
|
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Create -> failed")
|
||||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Create")
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::Create")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Create")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::Create")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IngredientHandler) GetByID(c *gin.Context) {
|
func (h *IngredientHandler) GetByID(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
idStr := c.Param("id")
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
id, err := uuid.Parse(idStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Invalid ingredient ID")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
|
}), "IngredientHandler::GetByID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ingredientResponse, err := h.ingredientService.GetIngredientByID(ctx, id)
|
resp, err := h.ingredientService.GetIngredientByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetByID -> Failed to get ingredient from service")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Ingredient not found")
|
contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Ingredient not found"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetByID")
|
}), "IngredientHandler::GetByID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetByID")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::GetByID")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IngredientHandler) GetAll(c *gin.Context) {
|
func (h *IngredientHandler) GetAll(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
// Get query parameters
|
|
||||||
pageStr := c.DefaultQuery("page", "1")
|
pageStr := c.DefaultQuery("page", "1")
|
||||||
limitStr := c.DefaultQuery("limit", "10")
|
limitStr := c.DefaultQuery("limit", "10")
|
||||||
search := c.Query("search")
|
search := c.Query("search")
|
||||||
@@ -83,95 +81,177 @@ func (h *IngredientHandler) GetAll(c *gin.Context) {
|
|||||||
|
|
||||||
page, err := strconv.Atoi(pageStr)
|
page, err := strconv.Atoi(pageStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid page parameter")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter")
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid page parameter"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
}), "IngredientHandler::GetAll")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
limit, err := strconv.Atoi(limitStr)
|
limit, err := strconv.Atoi(limitStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid limit parameter")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter")
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid limit parameter"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
}), "IngredientHandler::GetAll")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var outletID *uuid.UUID
|
var outletID *uuid.UUID
|
||||||
if outletIDStr != "" {
|
if outletIDStr != "" {
|
||||||
parsedOutletID, err := uuid.Parse(outletIDStr)
|
parsed, err := uuid.Parse(outletIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Invalid outlet ID")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
}), "IngredientHandler::GetAll")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
outletID = &parsedOutletID
|
outletID = &parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
ingredientResponse, err := h.ingredientService.ListIngredients(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
|
resp, err := h.ingredientService.ListIngredients(ctx, contextInfo.OrganizationID, outletID, page, limit, search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::GetAll -> Failed to get ingredients from service")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get ingredients")
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, "Failed to get ingredients"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::GetAll")
|
}), "IngredientHandler::GetAll")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::GetAll")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::GetAll")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IngredientHandler) Update(c *gin.Context) {
|
func (h *IngredientHandler) Update(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
idStr := c.Param("id")
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
id, err := uuid.Parse(idStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Invalid ingredient ID")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
}), "IngredientHandler::Update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var request models.UpdateIngredientRequest
|
var request models.UpdateIngredientRequest
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> request binding failed")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
}), "IngredientHandler::Update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ingredientResponse, err := h.ingredientService.UpdateIngredient(ctx, id, &request)
|
resp, err := h.ingredientService.UpdateIngredient(ctx, id, &request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Update -> Failed to update ingredient from service")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Update")
|
}), "IngredientHandler::Update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(ingredientResponse), "IngredientHandler::Update")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::Update")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IngredientHandler) Delete(c *gin.Context) {
|
func (h *IngredientHandler) Delete(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
idStr := c.Param("id")
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
id, err := uuid.Parse(idStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Invalid ingredient ID")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID"),
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
|
}), "IngredientHandler::Delete")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.ingredientService.DeleteIngredient(ctx, id)
|
if err := h.ingredientService.DeleteIngredient(ctx, id); err != nil {
|
||||||
if err != nil {
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
logger.FromContext(ctx).WithError(err).Error("IngredientHandler::Delete -> Failed to delete ingredient from service")
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
}), "IngredientHandler::Delete")
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientHandler::Delete")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]interface{}{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]string{
|
||||||
"message": "Ingredient deleted successfully",
|
"message": "Ingredient deleted successfully",
|
||||||
}), "IngredientHandler::Delete")
|
}), "IngredientHandler::Delete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddCompositions adds multiple composition items to a semi-finished ingredient.
|
||||||
|
func (h *IngredientHandler) AddCompositions(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid ingredient id"),
|
||||||
|
}), "IngredientHandler::AddCompositions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req models.AddIngredientCompositionsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::AddCompositions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.ingredientService.AddCompositions(ctx, id, &req)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::AddCompositions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::AddCompositions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateComposition updates quantity/outlet of a single composition entry.
|
||||||
|
func (h *IngredientHandler) UpdateComposition(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
id, err := uuid.Parse(c.Param("composition_id"))
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid composition id"),
|
||||||
|
}), "IngredientHandler::UpdateComposition")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req models.UpdateIngredientCompositionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::UpdateComposition")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.ingredientService.UpdateComposition(ctx, id, &req)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::UpdateComposition")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::UpdateComposition")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteComposition removes a single composition entry.
|
||||||
|
func (h *IngredientHandler) DeleteComposition(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
id, err := uuid.Parse(c.Param("composition_id"))
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "invalid composition id"),
|
||||||
|
}), "IngredientHandler::DeleteComposition")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.ingredientService.DeleteComposition(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "IngredientHandler::DeleteComposition")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "IngredientHandler::DeleteComposition")
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ type IngredientService interface {
|
|||||||
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
||||||
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, 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)
|
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||||
|
UpdateComposition(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientCompositionRequest) (*models.IngredientCompositionResponse, error)
|
||||||
|
DeleteComposition(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
AddCompositions(ctx context.Context, parentID uuid.UUID, req *models.AddIngredientCompositionsRequest) (*models.AddIngredientCompositionsResponse, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
"apskel-pos-be/internal/repository"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/transformer"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelfOrderHandler struct {
|
||||||
|
orderService service.OrderService
|
||||||
|
categoryService service.CategoryService
|
||||||
|
productService service.ProductService
|
||||||
|
tableRepo repository.TableRepositoryInterface
|
||||||
|
outletRepo processor.OutletRepository
|
||||||
|
userRepo processor.UserRepository
|
||||||
|
selfOrderJWTSecret string
|
||||||
|
selfOrderJWTTTL int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSelfOrderHandler(
|
||||||
|
orderService service.OrderService,
|
||||||
|
categoryService service.CategoryService,
|
||||||
|
productService service.ProductService,
|
||||||
|
tableRepo repository.TableRepositoryInterface,
|
||||||
|
outletRepo processor.OutletRepository,
|
||||||
|
userRepo processor.UserRepository,
|
||||||
|
selfOrderJWTSecret string,
|
||||||
|
selfOrderJWTTTL int,
|
||||||
|
) *SelfOrderHandler {
|
||||||
|
return &SelfOrderHandler{
|
||||||
|
orderService: orderService,
|
||||||
|
categoryService: categoryService,
|
||||||
|
productService: productService,
|
||||||
|
tableRepo: tableRepo,
|
||||||
|
outletRepo: outletRepo,
|
||||||
|
userRepo: userRepo,
|
||||||
|
selfOrderJWTSecret: selfOrderJWTSecret,
|
||||||
|
selfOrderJWTTTL: selfOrderJWTTTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) getSelfOrderContext(c *gin.Context) (uuid.UUID, string, string, error) {
|
||||||
|
tableIDStr, _ := c.Get("self_order_table_id")
|
||||||
|
customerName, _ := c.Get("self_order_customer_name")
|
||||||
|
phoneStr, _ := c.Get("self_order_phone")
|
||||||
|
|
||||||
|
tableIDStrTyped, ok := tableIDStr.(string)
|
||||||
|
if !ok || tableIDStrTyped == "" {
|
||||||
|
return uuid.Nil, "", "", fmt.Errorf("table_id not found in context")
|
||||||
|
}
|
||||||
|
tableID, err := uuid.Parse(tableIDStrTyped)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, "", "", fmt.Errorf("invalid table_id in token")
|
||||||
|
}
|
||||||
|
|
||||||
|
nameTyped, ok := customerName.(string)
|
||||||
|
if !ok || nameTyped == "" {
|
||||||
|
return uuid.Nil, "", "", fmt.Errorf("customer_name not found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
phone, _ := phoneStr.(string)
|
||||||
|
|
||||||
|
return tableID, nameTyped, phone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) CreateSession(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req contract.SelfOrderSessionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> request binding failed")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.TableID == uuid.Nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "table_id is required"),
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.CustomerName == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "customer_name is required"),
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := h.tableRepo.GetByID(ctx, req.TableID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> table not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"),
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !table.IsActive {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not active"),
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
phone := ""
|
||||||
|
if req.Phone != nil {
|
||||||
|
phone = *req.Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
token, expiresAt, err := util.GenerateSelfOrderSessionToken(req.TableID, req.CustomerName, phone, h.selfOrderJWTSecret, h.selfOrderJWTTTL)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateSession -> failed to generate token")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create session"),
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(&contract.SelfOrderSessionResponse{
|
||||||
|
Token: token,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
TableID: req.TableID,
|
||||||
|
}), "SelfOrderHandler::CreateSession")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) GetMenu(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
tableID, customerName, _, err := h.getSelfOrderContext(c)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := h.tableRepo.GetByID(ctx, tableID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> table not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"),
|
||||||
|
}), "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !table.IsActive {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not active"),
|
||||||
|
}), "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outlet, err := h.outletRepo.GetByID(ctx, table.OutletID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::GetMenu -> outlet not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.NotFoundErrorCode, constants.OrderServiceEntity, "outlet not found"),
|
||||||
|
}), "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = customerName
|
||||||
|
|
||||||
|
isActive := true
|
||||||
|
catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{
|
||||||
|
OrganizationID: &table.OrganizationID,
|
||||||
|
Page: 1,
|
||||||
|
Limit: 100,
|
||||||
|
})
|
||||||
|
if catResp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(catResp.GetErrors()[0]).Error("SelfOrderHandler::GetMenu -> failed to list categories")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, catResp, "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prodResp := h.productService.ListProducts(ctx, &contract.ListProductsRequest{
|
||||||
|
OrganizationID: &table.OrganizationID,
|
||||||
|
IsActive: &isActive,
|
||||||
|
Page: 1,
|
||||||
|
Limit: 1000,
|
||||||
|
})
|
||||||
|
if prodResp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(prodResp.GetErrors()[0]).Error("SelfOrderHandler::GetMenu -> failed to list products")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, prodResp, "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
catList, ok := catResp.Data.(*contract.ListCategoriesResponse)
|
||||||
|
if !ok {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, "unexpected categories response type"),
|
||||||
|
}), "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prodList, ok := prodResp.Data.(*contract.ListProductsResponse)
|
||||||
|
if !ok {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, "unexpected products response type"),
|
||||||
|
}), "SelfOrderHandler::GetMenu")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
menu := h.buildMenuResponse(outlet, table, catList.Categories, prodList.Products)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(menu), "SelfOrderHandler::GetMenu")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) buildMenuResponse(
|
||||||
|
outlet *entities.Outlet,
|
||||||
|
table *entities.Table,
|
||||||
|
categories []contract.CategoryResponse,
|
||||||
|
products []contract.ProductResponse,
|
||||||
|
) *contract.SelfOrderMenuResponse {
|
||||||
|
productMap := make(map[uuid.UUID][]contract.ProductResponse)
|
||||||
|
for _, p := range products {
|
||||||
|
productMap[p.CategoryID] = append(productMap[p.CategoryID], p)
|
||||||
|
}
|
||||||
|
|
||||||
|
menuCategories := make([]contract.SelfOrderMenuCategory, 0, len(categories))
|
||||||
|
for _, cat := range categories {
|
||||||
|
menuItems := make([]contract.SelfOrderMenuItem, 0)
|
||||||
|
if prods, ok := productMap[cat.ID]; ok {
|
||||||
|
for _, p := range prods {
|
||||||
|
item := contract.SelfOrderMenuItem{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Description: p.Description,
|
||||||
|
Price: p.Price,
|
||||||
|
ImageURL: p.ImageURL,
|
||||||
|
}
|
||||||
|
for _, v := range p.Variants {
|
||||||
|
item.Variants = append(item.Variants, contract.SelfOrderMenuVariant{
|
||||||
|
ID: v.ID,
|
||||||
|
Name: v.Name,
|
||||||
|
PriceModifier: v.PriceModifier,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
menuItems = append(menuItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menuCategories = append(menuCategories, contract.SelfOrderMenuCategory{
|
||||||
|
ID: cat.ID,
|
||||||
|
Name: cat.Name,
|
||||||
|
Description: cat.Description,
|
||||||
|
Order: cat.Order,
|
||||||
|
Products: menuItems,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.SelfOrderMenuResponse{
|
||||||
|
OutletName: outlet.Name,
|
||||||
|
TableName: table.TableName,
|
||||||
|
Categories: menuCategories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
tableID, customerName, phone, err := h.getSelfOrderContext(c)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.SelfOrderCreateOrderRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> request binding failed")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.validateCreateOrderRequest(&req); err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := h.tableRepo.GetByID(ctx, tableID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> table not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !table.IsActive || !table.IsAvailable() {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.TableEntity, "table is not available for ordering"),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := h.resolveOrgUser(ctx, table.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to resolve org user")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, "failed to create self-order"),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderItems := make([]models.CreateOrderItemRequest, 0, len(req.OrderItems))
|
||||||
|
for _, item := range req.OrderItems {
|
||||||
|
orderItems = append(orderItems, models.CreateOrderItemRequest{
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
ProductVariantID: item.ProductVariantID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
Notes: item.Notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
customerPhone := phone
|
||||||
|
if req.Phone != nil {
|
||||||
|
customerPhone = *req.Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := make(map[string]interface{})
|
||||||
|
metadata["self_order"] = true
|
||||||
|
metadata["customer_name"] = customerName
|
||||||
|
if customerPhone != "" {
|
||||||
|
metadata["customer_phone"] = customerPhone
|
||||||
|
}
|
||||||
|
|
||||||
|
tableIDPtr := tableID
|
||||||
|
modelReq := &models.CreateOrderRequest{
|
||||||
|
OutletID: table.OutletID,
|
||||||
|
UserID: userID,
|
||||||
|
TableID: &tableIDPtr,
|
||||||
|
TableNumber: &table.TableName,
|
||||||
|
OrderType: constants.OrderTypeDineIn,
|
||||||
|
OrderItems: orderItems,
|
||||||
|
CustomerName: &customerName,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.orderService.CreateOrder(ctx, modelReq, table.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::CreateOrder -> failed to create order")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.OrderServiceEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::CreateOrder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contractResp := transformer.OrderModelToContract(response)
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "SelfOrderHandler::CreateOrder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCreateOrderRequest) error {
|
||||||
|
if len(req.OrderItems) == 0 {
|
||||||
|
return fmt.Errorf("at least one order item is required")
|
||||||
|
}
|
||||||
|
for i, item := range req.OrderItems {
|
||||||
|
if item.ProductID == uuid.Nil {
|
||||||
|
return fmt.Errorf("product_id is required for item %d", i+1)
|
||||||
|
}
|
||||||
|
if item.Quantity <= 0 {
|
||||||
|
return fmt.Errorf("quantity must be greater than zero for item %d", i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
tableID, _, _, err := h.getSelfOrderContext(c)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error()),
|
||||||
|
}), "SelfOrderHandler::ListCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := h.tableRepo.GetByID(ctx, tableID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("SelfOrderHandler::ListCategories -> table not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "table not found"),
|
||||||
|
}), "SelfOrderHandler::ListCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
catResp := h.categoryService.ListCategories(ctx, &contract.ListCategoriesRequest{
|
||||||
|
OrganizationID: &table.OrganizationID,
|
||||||
|
Page: 1,
|
||||||
|
Limit: 100,
|
||||||
|
})
|
||||||
|
if catResp.HasErrors() {
|
||||||
|
logger.FromContext(ctx).WithError(catResp.GetErrors()[0]).Error("SelfOrderHandler::ListCategories -> failed to list categories")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, catResp, "SelfOrderHandler::ListCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
catList, ok := catResp.Data.(*contract.ListCategoriesResponse)
|
||||||
|
if !ok {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, "unexpected categories response type"),
|
||||||
|
}), "SelfOrderHandler::ListCategories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]contract.SelfOrderCategoryItem, 0, len(catList.Categories))
|
||||||
|
for _, cat := range catList.Categories {
|
||||||
|
items = append(items, contract.SelfOrderCategoryItem{
|
||||||
|
ID: cat.ID,
|
||||||
|
Name: cat.Name,
|
||||||
|
Description: cat.Description,
|
||||||
|
Order: cat.Order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(&contract.SelfOrderListCategoriesResponse{
|
||||||
|
Categories: items,
|
||||||
|
}), "SelfOrderHandler::ListCategories")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SelfOrderHandler) resolveOrgUser(ctx context.Context, organizationID uuid.UUID) (uuid.UUID, error) {
|
||||||
|
users, err := h.userRepo.GetByOrganizationID(ctx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, fmt.Errorf("failed to get users for organization: %w", err)
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
return uuid.Nil, fmt.Errorf("no users found for organization")
|
||||||
|
}
|
||||||
|
return users[0].ID, nil
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,11 @@ import (
|
|||||||
|
|
||||||
func CORS() gin.HandlerFunc {
|
func CORS() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header("Access-Control-Allow-Origin", "*")
|
origin := c.Request.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
origin = "*"
|
||||||
|
}
|
||||||
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
c.Header("Access-Control-Allow-Credentials", "true")
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelfOrderAuthMiddleware struct {
|
||||||
|
selfOrderJWTSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSelfOrderAuthMiddleware(selfOrderJWTSecret string) *SelfOrderAuthMiddleware {
|
||||||
|
return &SelfOrderAuthMiddleware{
|
||||||
|
selfOrderJWTSecret: selfOrderJWTSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SelfOrderAuthMiddleware) ValidateSelfOrderToken() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Authorization header is required"),
|
||||||
|
}), "SelfOrderAuthMiddleware::ValidateSelfOrderToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid authorization header format"),
|
||||||
|
}), "SelfOrderAuthMiddleware::ValidateSelfOrderToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
if tokenString == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Token is required"),
|
||||||
|
}), "SelfOrderAuthMiddleware::ValidateSelfOrderToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := util.ValidateSelfOrderToken(tokenString, m.selfOrderJWTSecret)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Invalid token: "+err.Error()),
|
||||||
|
}), "SelfOrderAuthMiddleware::ValidateSelfOrderToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tableID, ok := claims["table_id"].(string)
|
||||||
|
if !ok || tableID == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "table_id not found in token"),
|
||||||
|
}), "SelfOrderAuthMiddleware::ValidateSelfOrderToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerName, ok := claims["customer_name"].(string)
|
||||||
|
if !ok || customerName == "" {
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
|
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "customer_name not found in token"),
|
||||||
|
}), "SelfOrderAuthMiddleware::ValidateSelfOrderToken")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
phone, _ := claims["phone"].(string)
|
||||||
|
|
||||||
|
c.Set("self_order_table_id", tableID)
|
||||||
|
c.Set("self_order_customer_name", customerName)
|
||||||
|
c.Set("self_order_phone", phone)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,15 +26,23 @@ type Ingredient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateIngredientRequest struct {
|
type CreateIngredientRequest struct {
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
UnitID uuid.UUID `json:"unit_id" validate:"required"`
|
||||||
Cost float64 `json:"cost" validate:"min=0"`
|
Cost float64 `json:"cost" validate:"min=0"`
|
||||||
Stock float64 `json:"stock" validate:"min=0"`
|
Stock float64 `json:"stock" validate:"min=0"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Metadata entities.Metadata `json:"metadata"`
|
Metadata entities.Metadata `json:"metadata"`
|
||||||
|
Compositions []CompositionItemRequest `json:"compositions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositionItemRequest is used inside create ingredient request.
|
||||||
|
type CompositionItemRequest struct {
|
||||||
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateIngredientRequest struct {
|
type UpdateIngredientRequest struct {
|
||||||
@@ -49,19 +57,18 @@ type UpdateIngredientRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IngredientResponse struct {
|
type IngredientResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UnitID uuid.UUID `json:"unit_id"`
|
UnitID uuid.UUID `json:"unit_id"`
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
Stock float64 `json:"stock"`
|
Stock float64 `json:"stock"`
|
||||||
IsSemiFinished bool `json:"is_semi_finished"`
|
IsSemiFinished bool `json:"is_semi_finished"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
Metadata entities.Metadata `json:"metadata"`
|
Metadata entities.Metadata `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Unit *Unit `json:"unit,omitempty"`
|
||||||
// Relations
|
Compositions []*IngredientCompositionResponse `json:"compositions,omitempty"`
|
||||||
Unit *Unit `json:"unit,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,44 +6,33 @@ import (
|
|||||||
"github.com/google/uuid"
|
"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 {
|
type UpdateIngredientCompositionRequest struct {
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IngredientCompositionResponse struct {
|
type IngredientCompositionResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
||||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
|
Quantity float64 `json:"quantity"`
|
||||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Quantity float64 `json:"quantity"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
ChildIngredient *IngredientResponse `json:"child_ingredient,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
ParentIngredient *IngredientResponse `json:"parent_ingredient,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Relations
|
type CompositionItem struct {
|
||||||
ParentIngredient *Ingredient `json:"parent_ingredient,omitempty"`
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id" validate:"required"`
|
||||||
ChildIngredient *Ingredient `json:"child_ingredient,omitempty"`
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddIngredientCompositionsRequest struct {
|
||||||
|
Compositions []CompositionItem `json:"compositions" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddIngredientCompositionsResponse struct {
|
||||||
|
ParentIngredient *IngredientResponse `json:"parent_ingredient"`
|
||||||
|
Compositions []*IngredientCompositionResponse `json:"compositions"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,31 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/appcontext"
|
"apskel-pos-be/internal/appcontext"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/mappers"
|
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/transformer"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IngredientProcessorImpl struct {
|
type IngredientProcessorImpl struct {
|
||||||
ingredientRepo IngredientRepository
|
ingredientRepo IngredientRepository
|
||||||
unitRepo UnitRepository
|
unitRepo UnitRepository
|
||||||
|
compositionRepo IngredientCompositionRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository) *IngredientProcessorImpl {
|
func NewIngredientProcessor(ingredientRepo IngredientRepository, unitRepo UnitRepository, compositionRepo IngredientCompositionRepository) *IngredientProcessorImpl {
|
||||||
return &IngredientProcessorImpl{
|
return &IngredientProcessorImpl{
|
||||||
ingredientRepo: ingredientRepo,
|
ingredientRepo: ingredientRepo,
|
||||||
unitRepo: unitRepo,
|
unitRepo: unitRepo,
|
||||||
|
compositionRepo: compositionRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *models.CreateIngredientRequest) (*models.IngredientResponse, error) {
|
||||||
_, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID)
|
if _, err := p.unitRepo.GetByID(ctx, req.UnitID, req.OrganizationID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *mod
|
|||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
UnitID: req.UnitID,
|
UnitID: req.UnitID,
|
||||||
Cost: req.Cost,
|
Cost: req.Cost, // akan di-override oleh recalculateCost kalau semi-finished
|
||||||
Stock: req.Stock,
|
Stock: req.Stock,
|
||||||
IsSemiFinished: req.IsSemiFinished,
|
IsSemiFinished: req.IsSemiFinished,
|
||||||
IsActive: req.IsActive,
|
IsActive: req.IsActive,
|
||||||
@@ -44,70 +46,30 @@ func (p *IngredientProcessorImpl) CreateIngredient(ctx context.Context, req *mod
|
|||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = p.ingredientRepo.Create(ctx, ingredient)
|
if req.IsSemiFinished {
|
||||||
if err != nil {
|
ingredient.Cost = 0 // dihitung dari compositions
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.ingredientRepo.Create(ctx, ingredient); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ingredientWithUnit, err := p.ingredientRepo.GetByID(ctx, ingredient.ID, req.OrganizationID)
|
// Save compositions if provided (only valid for semi-finished ingredients)
|
||||||
if err != nil {
|
if req.IsSemiFinished && len(req.Compositions) > 0 {
|
||||||
return nil, err
|
if err := p.saveCompositions(ctx, ingredient.ID, req.OrganizationID, req.Compositions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ingredientModel := mappers.MapIngredientEntityToModel(ingredientWithUnit)
|
return p.buildIngredientResponse(ctx, ingredient.ID, req.OrganizationID)
|
||||||
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) {
|
func (p *IngredientProcessorImpl) GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
|
return p.buildIngredientResponse(ctx, id, ctxInfo.OrganizationID)
|
||||||
// For now, we'll need to get organizationID from context or request
|
|
||||||
// This is a limitation of the current interface design
|
|
||||||
organizationID := contextInfo.OrganizationID // 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) {
|
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 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
@@ -123,30 +85,9 @@ func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizat
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map to response models
|
ingredientResponses := transformer.IngredientsToResponses(ingredients)
|
||||||
ingredientModels := mappers.MapIngredientEntitiesToModels(ingredients)
|
|
||||||
ingredientResponses := make([]models.IngredientResponse, len(ingredientModels))
|
|
||||||
|
|
||||||
for i, ingredientModel := range ingredientModels {
|
return &models.PaginatedResponse[models.IngredientResponse]{
|
||||||
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,
|
Data: ingredientResponses,
|
||||||
Pagination: models.Pagination{
|
Pagination: models.Pagination{
|
||||||
Page: page,
|
Page: page,
|
||||||
@@ -154,86 +95,250 @@ func (p *IngredientProcessorImpl) ListIngredients(ctx context.Context, organizat
|
|||||||
Total: int64(total),
|
Total: int64(total),
|
||||||
TotalPages: (total + limit - 1) / limit,
|
TotalPages: (total + limit - 1) / limit,
|
||||||
},
|
},
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
return paginatedResponse, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
func (p *IngredientProcessorImpl) UpdateIngredient(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientRequest) (*models.IngredientResponse, error) {
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
// For now, we'll need to get organizationID from context or request
|
organizationID := ctxInfo.OrganizationID
|
||||||
// This is a limitation of the current interface design
|
|
||||||
organizationID := contextInfo.OrganizationID // This should come from context
|
|
||||||
|
|
||||||
// Get existing ingredient
|
existing, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||||
existingIngredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate unit exists if changed
|
if req.UnitID != existing.UnitID {
|
||||||
if req.UnitID != existingIngredient.UnitID {
|
if _, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID); err != nil {
|
||||||
_, err := p.unitRepo.GetByID(ctx, req.UnitID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update fields
|
existing.OutletID = req.OutletID
|
||||||
if req.OutletID != nil {
|
existing.Name = req.Name
|
||||||
existingIngredient.OutletID = req.OutletID
|
existing.UnitID = req.UnitID
|
||||||
|
existing.Stock = req.Stock
|
||||||
|
existing.IsSemiFinished = req.IsSemiFinished
|
||||||
|
existing.IsActive = req.IsActive
|
||||||
|
existing.Metadata = req.Metadata
|
||||||
|
existing.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
// Cost hanya dipakai kalau bukan semi-finished
|
||||||
|
// Kalau semi-finished, cost dihitung dari compositions
|
||||||
|
if !req.IsSemiFinished {
|
||||||
|
existing.Cost = req.Cost
|
||||||
}
|
}
|
||||||
|
|
||||||
existingIngredient.Name = req.Name
|
if err := p.ingredientRepo.Update(ctx, existing); err != nil {
|
||||||
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get with relations
|
return p.buildIngredientResponse(ctx, id, organizationID)
|
||||||
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 {
|
func (p *IngredientProcessorImpl) DeleteIngredient(ctx context.Context, id uuid.UUID) error {
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
organizationID := contextInfo.OrganizationID
|
return p.ingredientRepo.Delete(ctx, id, ctxInfo.OrganizationID)
|
||||||
|
}
|
||||||
|
|
||||||
err := p.ingredientRepo.Delete(ctx, id, organizationID)
|
func (p *IngredientProcessorImpl) AddCompositions(ctx context.Context, parentID uuid.UUID, req *models.AddIngredientCompositionsRequest) (*models.AddIngredientCompositionsResponse, error) {
|
||||||
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
|
organizationID := ctxInfo.OrganizationID
|
||||||
|
|
||||||
|
// Parent must exist and be semi-finished
|
||||||
|
parent, err := p.ingredientRepo.GetByID(ctx, parentID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parent ingredient not found: %w", err)
|
||||||
|
}
|
||||||
|
if !parent.IsSemiFinished {
|
||||||
|
return nil, fmt.Errorf("parent ingredient must be marked as semi-finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
createdIDs := make([]uuid.UUID, 0, len(req.Compositions))
|
||||||
|
|
||||||
|
// Validate and create all compositions
|
||||||
|
for _, item := range req.Compositions {
|
||||||
|
// Child must exist
|
||||||
|
if _, err := p.ingredientRepo.GetByID(ctx, item.ChildIngredientID, organizationID); err != nil {
|
||||||
|
return nil, fmt.Errorf("child ingredient %s not found: %w", item.ChildIngredientID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.ChildIngredientID == parentID {
|
||||||
|
return nil, fmt.Errorf("child ingredient cannot be the same as parent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve outlet
|
||||||
|
outletID := item.OutletID
|
||||||
|
if outletID == nil && ctxInfo.OutletID != uuid.Nil {
|
||||||
|
outletID = &ctxInfo.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
|
compositionID := uuid.New()
|
||||||
|
composition := &entities.IngredientComposition{
|
||||||
|
ID: compositionID,
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
OutletID: outletID,
|
||||||
|
ParentIngredientID: parentID,
|
||||||
|
ChildIngredientID: item.ChildIngredientID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.compositionRepo.Create(ctx, composition); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add composition: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdIDs = append(createdIDs, compositionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate parent cost
|
||||||
|
if err := p.recalculateCost(ctx, parentID, organizationID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated parent ingredient with all compositions
|
||||||
|
updatedParent, err := p.buildIngredientResponse(ctx, parentID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get created compositions with full details
|
||||||
|
createdCompositions := make([]*models.IngredientCompositionResponse, 0, len(createdIDs))
|
||||||
|
for _, id := range createdIDs {
|
||||||
|
comp, err := p.compositionRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
createdCompositions = append(createdCompositions, transformer.CompositionEntityToResponse(comp))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.AddIngredientCompositionsResponse{
|
||||||
|
ParentIngredient: updatedParent,
|
||||||
|
Compositions: createdCompositions,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) UpdateComposition(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientCompositionRequest) (*models.IngredientCompositionResponse, error) {
|
||||||
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
existing, err := p.compositionRepo.GetByID(ctx, id, ctxInfo.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("composition not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.Quantity = req.Quantity
|
||||||
|
existing.OutletID = req.OutletID
|
||||||
|
existing.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := p.compositionRepo.Update(ctx, existing); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update composition: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate parent cost since quantity changed
|
||||||
|
if err := p.recalculateCost(ctx, existing.ParentIngredientID, ctxInfo.OrganizationID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to recalculate cost: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := p.compositionRepo.GetByID(ctx, id, ctxInfo.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.CompositionEntityToResponse(updated), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) DeleteComposition(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||||
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
existing, err := p.compositionRepo.GetByID(ctx, id, ctxInfo.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("composition not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID := existing.ParentIngredientID
|
||||||
|
|
||||||
|
if err := p.compositionRepo.Delete(ctx, id, ctxInfo.OrganizationID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate parent cost after removal
|
||||||
|
if err := p.recalculateCost(ctx, parentID, ctxInfo.OrganizationID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated parent ingredient
|
||||||
|
return p.buildIngredientResponse(ctx, parentID, ctxInfo.OrganizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- private helpers ---
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) saveCompositions(ctx context.Context, parentID, organizationID uuid.UUID, items []models.CompositionItemRequest) error {
|
||||||
|
ctxInfo := appcontext.FromGinContext(ctx)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if item.ChildIngredientID == parentID {
|
||||||
|
return fmt.Errorf("child ingredient cannot be the same as parent")
|
||||||
|
}
|
||||||
|
if _, err := p.ingredientRepo.GetByID(ctx, item.ChildIngredientID, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("child ingredient %s not found: %w", item.ChildIngredientID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve outlet: use item's outlet if provided, otherwise fall back to context outlet
|
||||||
|
outletID := item.OutletID
|
||||||
|
if outletID == nil && ctxInfo.OutletID != uuid.Nil {
|
||||||
|
outletID = &ctxInfo.OutletID
|
||||||
|
}
|
||||||
|
|
||||||
|
composition := &entities.IngredientComposition{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
OutletID: outletID,
|
||||||
|
ParentIngredientID: parentID,
|
||||||
|
ChildIngredientID: item.ChildIngredientID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
if err := p.compositionRepo.Create(ctx, composition); err != nil {
|
||||||
|
return fmt.Errorf("failed to save composition: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate cost of the semi-finished parent from child costs
|
||||||
|
return p.recalculateCost(ctx, parentID, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recalculateCost sums up (child.cost * composition.quantity) and updates the parent ingredient cost.
|
||||||
|
func (p *IngredientProcessorImpl) recalculateCost(ctx context.Context, parentID, organizationID uuid.UUID) error {
|
||||||
|
compositions, err := p.compositionRepo.GetByParentIngredientID(ctx, parentID, organizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
var totalCost float64
|
||||||
|
for _, c := range compositions {
|
||||||
|
if c.ChildIngredient != nil {
|
||||||
|
totalCost += c.ChildIngredient.Cost * c.Quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := p.ingredientRepo.GetByID(ctx, parentID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.Cost = totalCost
|
||||||
|
parent.UpdatedAt = time.Now()
|
||||||
|
return p.ingredientRepo.Update(ctx, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IngredientProcessorImpl) buildIngredientResponse(ctx context.Context, id, organizationID uuid.UUID) (*models.IngredientResponse, error) {
|
||||||
|
ingredient, err := p.ingredientRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.IngredientEntityToResponse(ingredient), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"apskel-pos-be/internal/models"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mock repositories for testing
|
|
||||||
type MockProductRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockCategoryRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockProductVariantRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockInventoryRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockOutletRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test helper functions
|
|
||||||
func TestCreateProductWithInventory(t *testing.T) {
|
|
||||||
// This is a basic test structure - in a real implementation,
|
|
||||||
// you would use a proper testing framework with database mocks
|
|
||||||
|
|
||||||
t.Run("should create product with inventory when create_inventory is true", func(t *testing.T) {
|
|
||||||
// Arrange
|
|
||||||
productRepo := &MockProductRepository{}
|
|
||||||
categoryRepo := &MockCategoryRepository{}
|
|
||||||
productVariantRepo := &MockProductVariantRepository{}
|
|
||||||
inventoryRepo := &MockInventoryRepository{}
|
|
||||||
outletRepo := &MockOutletRepository{}
|
|
||||||
|
|
||||||
processor := NewProductProcessorImpl(
|
|
||||||
productRepo,
|
|
||||||
categoryRepo,
|
|
||||||
productVariantRepo,
|
|
||||||
inventoryRepo,
|
|
||||||
outletRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
req := &models.CreateProductRequest{
|
|
||||||
OrganizationID: uuid.New(),
|
|
||||||
CategoryID: uuid.New(),
|
|
||||||
Name: "Test Product",
|
|
||||||
Price: 10.0,
|
|
||||||
Cost: 5.0,
|
|
||||||
InitialStock: &[]int{100}[0],
|
|
||||||
ReorderLevel: &[]int{20}[0],
|
|
||||||
CreateInventory: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock expectations
|
|
||||||
categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil)
|
|
||||||
productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil)
|
|
||||||
productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil)
|
|
||||||
productRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
|
|
||||||
productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil)
|
|
||||||
|
|
||||||
// Mock outlets
|
|
||||||
outlets := []*models.Outlet{
|
|
||||||
{ID: uuid.New()},
|
|
||||||
{ID: uuid.New()},
|
|
||||||
}
|
|
||||||
outletRepo.On("GetByOrganizationID", mock.Anything, req.OrganizationID).Return(outlets, nil)
|
|
||||||
|
|
||||||
// Mock inventory creation
|
|
||||||
inventoryRepo.On("BulkCreate", mock.Anything, mock.Anything).Return(nil)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
result, err := processor.CreateProduct(context.Background(), req)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, result)
|
|
||||||
|
|
||||||
// Verify that inventory was created
|
|
||||||
inventoryRepo.AssertCalled(t, "BulkCreate", mock.Anything, mock.Anything)
|
|
||||||
outletRepo.AssertCalled(t, "GetByOrganizationID", mock.Anything, req.OrganizationID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should not create inventory when create_inventory is false", func(t *testing.T) {
|
|
||||||
// Arrange
|
|
||||||
productRepo := &MockProductRepository{}
|
|
||||||
categoryRepo := &MockCategoryRepository{}
|
|
||||||
productVariantRepo := &MockProductVariantRepository{}
|
|
||||||
inventoryRepo := &MockInventoryRepository{}
|
|
||||||
outletRepo := &MockOutletRepository{}
|
|
||||||
|
|
||||||
processor := NewProductProcessorImpl(
|
|
||||||
productRepo,
|
|
||||||
categoryRepo,
|
|
||||||
productVariantRepo,
|
|
||||||
inventoryRepo,
|
|
||||||
outletRepo,
|
|
||||||
)
|
|
||||||
|
|
||||||
req := &models.CreateProductRequest{
|
|
||||||
OrganizationID: uuid.New(),
|
|
||||||
CategoryID: uuid.New(),
|
|
||||||
Name: "Test Product",
|
|
||||||
Price: 10.0,
|
|
||||||
Cost: 5.0,
|
|
||||||
CreateInventory: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock expectations
|
|
||||||
categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil)
|
|
||||||
productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil)
|
|
||||||
productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil)
|
|
||||||
productRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
|
|
||||||
productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil)
|
|
||||||
|
|
||||||
// Act
|
|
||||||
result, err := processor.CreateProduct(context.Background(), req)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, result)
|
|
||||||
|
|
||||||
// Verify that inventory was NOT created
|
|
||||||
inventoryRepo.AssertNotCalled(t, "BulkCreate", mock.Anything, mock.Anything)
|
|
||||||
outletRepo.AssertNotCalled(t, "GetByOrganizationID", mock.Anything, mock.Anything)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock implementations (simplified for testing)
|
|
||||||
func (m *MockProductRepository) Create(ctx context.Context, product *models.Product) error {
|
|
||||||
args := m.Called(ctx, product)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockProductRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error) {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Get(0).(*models.Product), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockProductRepository) GetWithCategory(ctx context.Context, id uuid.UUID) (*models.Product, error) {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Get(0).(*models.Product), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockProductRepository) ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) {
|
|
||||||
args := m.Called(ctx, organizationID, sku, excludeID)
|
|
||||||
return args.Bool(0), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockProductRepository) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) {
|
|
||||||
args := m.Called(ctx, organizationID, name, excludeID)
|
|
||||||
return args.Bool(0), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockCategoryRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Category, error) {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Get(0).(*models.Category), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockInventoryRepository) BulkCreate(ctx context.Context, inventoryItems []*models.Inventory) error {
|
|
||||||
args := m.Called(ctx, inventoryItems)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockOutletRepository) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.Outlet, error) {
|
|
||||||
args := m.Called(ctx, organizationID)
|
|
||||||
return args.Get(0).([]*models.Outlet), args.Error(1)
|
|
||||||
}
|
|
||||||
@@ -82,3 +82,12 @@ type UnitRepository interface {
|
|||||||
Update(ctx context.Context, unit *entities.Unit) error
|
Update(ctx context.Context, unit *entities.Unit) error
|
||||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IngredientCompositionRepository interface {
|
||||||
|
Create(ctx context.Context, composition *entities.IngredientComposition) error
|
||||||
|
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientComposition, error)
|
||||||
|
GetByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error)
|
||||||
|
Update(ctx context.Context, composition *entities.IngredientComposition) error
|
||||||
|
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||||
|
DeleteByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) error
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,33 +77,43 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz
|
|||||||
dateFormat = "DATE(o.created_at)"
|
dateFormat = "DATE(o.created_at)"
|
||||||
}
|
}
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
outletFilter := ""
|
||||||
Table("orders o").
|
args := []interface{}{organizationID, false, false, string(entities.PaymentStatusCompleted), dateFrom, dateTo}
|
||||||
Select(`
|
if outletID != nil {
|
||||||
`+dateFormat+` as date,
|
outletFilter = "AND o.outlet_id = ?"
|
||||||
|
args = append(args, *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawQuery := `
|
||||||
|
SELECT
|
||||||
|
` + dateFormat + ` as date,
|
||||||
COALESCE(SUM(o.total_amount), 0) as sales,
|
COALESCE(SUM(o.total_amount), 0) as sales,
|
||||||
COUNT(o.id) as orders,
|
COUNT(o.id) as orders,
|
||||||
COALESCE(SUM(CASE WHEN oi.status != 'cancelled' AND oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as items,
|
COALESCE(SUM(oi_agg.total_items), 0) as items,
|
||||||
COALESCE(SUM(o.tax_amount), 0) as tax,
|
COALESCE(SUM(o.tax_amount), 0) as tax,
|
||||||
COALESCE(SUM(o.discount_amount), 0) as discount,
|
COALESCE(SUM(o.discount_amount), 0) as discount,
|
||||||
COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales
|
COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales
|
||||||
`).
|
FROM orders o
|
||||||
Joins("LEFT JOIN order_items oi ON o.id = oi.order_id").
|
LEFT JOIN (
|
||||||
Where("o.organization_id = ?", organizationID).
|
SELECT
|
||||||
Where("o.is_void = ?", false).
|
oi.order_id,
|
||||||
Where("o.is_refund = ?", false).
|
SUM(oi.quantity - COALESCE(oi.refund_quantity, 0)) as total_items
|
||||||
Where("o.payment_status = ?", entities.PaymentStatusCompleted).
|
FROM order_items oi
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
WHERE oi.status != 'cancelled'
|
||||||
|
AND oi.is_fully_refunded = false
|
||||||
if outletID != nil {
|
GROUP BY oi.order_id
|
||||||
query = query.Where("o.outlet_id = ?", *outletID)
|
) oi_agg ON oi_agg.order_id = o.id
|
||||||
}
|
WHERE o.organization_id = ?
|
||||||
|
AND o.is_void = ?
|
||||||
err := query.
|
AND o.is_refund = ?
|
||||||
Group("date").
|
AND o.payment_status = ?
|
||||||
Order("date ASC").
|
AND o.created_at >= ? AND o.created_at <= ?
|
||||||
Scan(&results).Error
|
` + outletFilter + `
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Raw(rawQuery, args...).Scan(&results).Error
|
||||||
return results, err
|
return results, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,285 +3,81 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IngredientCompositionRepository struct {
|
type IngredientCompositionRepository struct {
|
||||||
db *sql.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIngredientCompositionRepository(db *sql.DB) *IngredientCompositionRepository {
|
func NewIngredientCompositionRepository(db *gorm.DB) *IngredientCompositionRepository {
|
||||||
return &IngredientCompositionRepository{db: db}
|
return &IngredientCompositionRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IngredientCompositionRepository) Create(ctx context.Context, composition *entities.IngredientComposition) error {
|
func (r *IngredientCompositionRepository) Create(ctx context.Context, composition *entities.IngredientComposition) error {
|
||||||
query := `
|
return r.db.WithContext(ctx).Create(composition).Error
|
||||||
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) {
|
func (r *IngredientCompositionRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientComposition, error) {
|
||||||
query := `
|
var composition entities.IngredientComposition
|
||||||
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,
|
err := r.db.WithContext(ctx).
|
||||||
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,
|
Preload("ChildIngredient.Unit").
|
||||||
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
|
Preload("ParentIngredient.Unit").
|
||||||
FROM ingredient_compositions ic
|
Preload("ParentIngredient.Compositions.ChildIngredient.Unit").
|
||||||
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
First(&composition).Error
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &composition, nil
|
||||||
composition.ParentIngredient = parentIngredient
|
|
||||||
composition.ChildIngredient = childIngredient
|
|
||||||
return composition, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IngredientCompositionRepository) GetByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error) {
|
func (r *IngredientCompositionRepository) GetByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) ([]*entities.IngredientComposition, error) {
|
||||||
query := `
|
var compositions []*entities.IngredientComposition
|
||||||
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,
|
err := r.db.WithContext(ctx).
|
||||||
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,
|
Preload("ChildIngredient.Unit").
|
||||||
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
|
Where("parent_ingredient_id = ? AND organization_id = ?", parentIngredientID, organizationID).
|
||||||
FROM ingredient_compositions ic
|
Order("created_at ASC").
|
||||||
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
Find(&compositions).Error
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return compositions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IngredientCompositionRepository) Update(ctx context.Context, composition *entities.IngredientComposition) error {
|
func (r *IngredientCompositionRepository) Update(ctx context.Context, composition *entities.IngredientComposition) error {
|
||||||
query := `
|
result := r.db.WithContext(ctx).
|
||||||
UPDATE ingredient_compositions
|
Model(&entities.IngredientComposition{}).
|
||||||
SET outlet_id = $1, quantity = $2, updated_at = $3
|
Where("id = ? AND organization_id = ?", composition.ID, composition.OrganizationID).
|
||||||
WHERE id = $4 AND organization_id = $5
|
Select("outlet_id", "quantity", "updated_at").
|
||||||
`
|
Updates(composition)
|
||||||
|
if result.Error != nil {
|
||||||
result, err := r.db.ExecContext(ctx, query,
|
return result.Error
|
||||||
composition.OutletID,
|
|
||||||
composition.Quantity,
|
|
||||||
composition.UpdatedAt,
|
|
||||||
composition.ID,
|
|
||||||
composition.OrganizationID,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
rowsAffected, err := result.RowsAffected()
|
return fmt.Errorf("no rows affected")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return sql.ErrNoRows
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *IngredientCompositionRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
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 := r.db.WithContext(ctx).
|
||||||
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
result, err := r.db.ExecContext(ctx, query, id, organizationID)
|
Delete(&entities.IngredientComposition{})
|
||||||
if err != nil {
|
if result.Error != nil {
|
||||||
return err
|
return result.Error
|
||||||
}
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
rowsAffected, err := result.RowsAffected()
|
return fmt.Errorf("no rows affected")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return sql.ErrNoRows
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *IngredientCompositionRepository) DeleteByParentIngredientID(ctx context.Context, parentIngredientID, organizationID uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("parent_ingredient_id = ? AND organization_id = ?", parentIngredientID, organizationID).
|
||||||
|
Delete(&entities.IngredientComposition{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ func (r *IngredientRepository) Create(ctx context.Context, ingredient *entities.
|
|||||||
|
|
||||||
func (r *IngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error) {
|
func (r *IngredientRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error) {
|
||||||
var ingredient entities.Ingredient
|
var ingredient entities.Ingredient
|
||||||
err := r.db.WithContext(ctx).Preload("Unit").Where("id = ? AND organization_id = ?", id, organizationID).First(&ingredient).Error
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Unit").
|
||||||
|
Preload("Compositions.ChildIngredient.Unit").
|
||||||
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
|
First(&ingredient).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -57,7 +61,13 @@ func (r *IngredientRepository) GetAll(ctx context.Context, organizationID uuid.U
|
|||||||
|
|
||||||
// Get paginated results with Unit preloaded
|
// Get paginated results with Unit preloaded
|
||||||
offset := (page - 1) * limit
|
offset := (page - 1) * limit
|
||||||
err := query.Preload("Unit").Order("created_at DESC").Limit(limit).Offset(offset).Find(&ingredients).Error
|
err := query.
|
||||||
|
Preload("Unit").
|
||||||
|
Preload("Compositions.ChildIngredient.Unit").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&ingredients).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,13 @@ type Router struct {
|
|||||||
customerAuthHandler *handler.CustomerAuthHandler
|
customerAuthHandler *handler.CustomerAuthHandler
|
||||||
customerPointsHandler *handler.CustomerPointsHandler
|
customerPointsHandler *handler.CustomerPointsHandler
|
||||||
spinGameHandler *handler.SpinGameHandler
|
spinGameHandler *handler.SpinGameHandler
|
||||||
|
selfOrderHandler *handler.SelfOrderHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
|
selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware) *Router {
|
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, selfOrderHandler *handler.SelfOrderHandler, selfOrderAuthMiddleware *middleware.SelfOrderAuthMiddleware) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -89,6 +91,8 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
customerAuthMiddleware: customerAuthMiddleware,
|
customerAuthMiddleware: customerAuthMiddleware,
|
||||||
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
||||||
|
selfOrderHandler: selfOrderHandler,
|
||||||
|
selfOrderAuthMiddleware: selfOrderAuthMiddleware,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +149,19 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
customer.POST("/spin", r.spinGameHandler.PlaySpinGame)
|
customer.POST("/spin", r.spinGameHandler.PlaySpinGame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selfOrder := v1.Group("/self-order")
|
||||||
|
{
|
||||||
|
selfOrder.POST("/session", r.selfOrderHandler.CreateSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
selfOrderProtected := v1.Group("/self-order")
|
||||||
|
selfOrderProtected.Use(r.selfOrderAuthMiddleware.ValidateSelfOrderToken())
|
||||||
|
{
|
||||||
|
selfOrderProtected.GET("/menu", r.selfOrderHandler.GetMenu)
|
||||||
|
selfOrderProtected.GET("/categories", r.selfOrderHandler.ListCategories)
|
||||||
|
selfOrderProtected.POST("/order", r.selfOrderHandler.CreateOrder)
|
||||||
|
}
|
||||||
|
|
||||||
organizations := v1.Group("/organizations")
|
organizations := v1.Group("/organizations")
|
||||||
{
|
{
|
||||||
organizations.POST("", r.organizationHandler.CreateOrganization)
|
organizations.POST("", r.organizationHandler.CreateOrganization)
|
||||||
@@ -322,6 +339,9 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
ingredients.GET("/:id", r.ingredientHandler.GetByID)
|
ingredients.GET("/:id", r.ingredientHandler.GetByID)
|
||||||
ingredients.PUT("/:id", r.ingredientHandler.Update)
|
ingredients.PUT("/:id", r.ingredientHandler.Update)
|
||||||
ingredients.DELETE("/:id", r.ingredientHandler.Delete)
|
ingredients.DELETE("/:id", r.ingredientHandler.Delete)
|
||||||
|
ingredients.POST("/:id/compositions", r.ingredientHandler.AddCompositions)
|
||||||
|
ingredients.PUT("/compositions/:composition_id", r.ingredientHandler.UpdateComposition)
|
||||||
|
ingredients.DELETE("/compositions/:composition_id", r.ingredientHandler.DeleteComposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
vendors := protected.Group("/vendors")
|
vendors := protected.Group("/vendors")
|
||||||
|
|||||||
@@ -13,4 +13,7 @@ type IngredientProcessor interface {
|
|||||||
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
DeleteIngredient(ctx context.Context, id uuid.UUID) error
|
||||||
GetIngredientByID(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, 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)
|
ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error)
|
||||||
|
UpdateComposition(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientCompositionRequest) (*models.IngredientCompositionResponse, error)
|
||||||
|
DeleteComposition(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error)
|
||||||
|
AddCompositions(ctx context.Context, parentID uuid.UUID, req *models.AddIngredientCompositionsRequest) (*models.AddIngredientCompositionsResponse, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,3 +36,15 @@ func (s *IngredientServiceImpl) GetIngredientByID(ctx context.Context, id uuid.U
|
|||||||
func (s *IngredientServiceImpl) ListIngredients(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) (*models.PaginatedResponse[models.IngredientResponse], error) {
|
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)
|
return s.ingredientProcessor.ListIngredients(ctx, organizationID, outletID, page, limit, search)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) UpdateComposition(ctx context.Context, id uuid.UUID, req *models.UpdateIngredientCompositionRequest) (*models.IngredientCompositionResponse, error) {
|
||||||
|
return s.ingredientProcessor.UpdateComposition(ctx, id, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) DeleteComposition(ctx context.Context, id uuid.UUID) (*models.IngredientResponse, error) {
|
||||||
|
return s.ingredientProcessor.DeleteComposition(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IngredientServiceImpl) AddCompositions(ctx context.Context, parentID uuid.UUID, req *models.AddIngredientCompositionsRequest) (*models.AddIngredientCompositionsResponse, error) {
|
||||||
|
return s.ingredientProcessor.AddCompositions(ctx, parentID, req)
|
||||||
|
}
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ func (s *OrderServiceImpl) validateCreatePaymentRequest(req *models.CreatePaymen
|
|||||||
return fmt.Errorf("order item ID is required for payment item %d", i+1)
|
return fmt.Errorf("order item ID is required for payment item %d", i+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Amount <= 0 {
|
if item.Amount < 0 {
|
||||||
return fmt.Errorf("payment item amount must be greater than zero for item %d", i+1)
|
return fmt.Errorf("payment item amount must be greater than zero for item %d", i+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package transformer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IngredientEntityToResponse converts ingredient entity to response model
|
||||||
|
func IngredientEntityToResponse(ingredient *entities.Ingredient) *models.IngredientResponse {
|
||||||
|
if ingredient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := mappers.MapIngredientEntityToModel(ingredient)
|
||||||
|
resp := &models.IngredientResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
OrganizationID: m.OrganizationID,
|
||||||
|
OutletID: m.OutletID,
|
||||||
|
Name: m.Name,
|
||||||
|
UnitID: m.UnitID,
|
||||||
|
Cost: m.Cost,
|
||||||
|
Stock: m.Stock,
|
||||||
|
IsSemiFinished: m.IsSemiFinished,
|
||||||
|
IsActive: m.IsActive,
|
||||||
|
Metadata: m.Metadata,
|
||||||
|
CreatedAt: m.CreatedAt,
|
||||||
|
UpdatedAt: m.UpdatedAt,
|
||||||
|
Unit: m.Unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use preloaded compositions for semi-finished ingredients
|
||||||
|
if ingredient.IsSemiFinished && len(ingredient.Compositions) > 0 {
|
||||||
|
resp.Compositions = make([]*models.IngredientCompositionResponse, 0, len(ingredient.Compositions))
|
||||||
|
for _, c := range ingredient.Compositions {
|
||||||
|
resp.Compositions = append(resp.Compositions, CompositionEntityToResponse(&c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositionEntityToResponse converts composition entity to response model
|
||||||
|
func CompositionEntityToResponse(c *entities.IngredientComposition) *models.IngredientCompositionResponse {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &models.IngredientCompositionResponse{
|
||||||
|
ID: c.ID,
|
||||||
|
OutletID: c.OutletID,
|
||||||
|
ChildIngredientID: c.ChildIngredientID,
|
||||||
|
Quantity: c.Quantity,
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
UpdatedAt: c.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ChildIngredient != nil {
|
||||||
|
resp.ChildIngredient = mappers.MapIngredientEntityToResponse(c.ChildIngredient)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ParentIngredient != nil {
|
||||||
|
resp.ParentIngredient = IngredientEntityToResponse(c.ParentIngredient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// IngredientsToResponses converts slice of ingredient entities to response models
|
||||||
|
func IngredientsToResponses(ingredients []*entities.Ingredient) []models.IngredientResponse {
|
||||||
|
if ingredients == nil {
|
||||||
|
return []models.IngredientResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]models.IngredientResponse, len(ingredients))
|
||||||
|
for i, ing := range ingredients {
|
||||||
|
response := IngredientEntityToResponse(ing)
|
||||||
|
if response != nil {
|
||||||
|
responses[i] = *response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateCustomerTokens generates access and refresh tokens for customer
|
// GenerateCustomerTokens generates access and refresh tokens for customer
|
||||||
@@ -85,3 +86,55 @@ func ExtractCustomerIDFromToken(token *jwt.Token) (string, error) {
|
|||||||
|
|
||||||
return customerID, nil
|
return customerID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateSelfOrderSessionToken(tableID uuid.UUID, customerName string, phone string, secret string, ttlMinutes int) (string, int64, error) {
|
||||||
|
now := time.Now()
|
||||||
|
expiresAt := now.Add(time.Duration(ttlMinutes) * time.Minute)
|
||||||
|
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"table_id": tableID.String(),
|
||||||
|
"customer_name": customerName,
|
||||||
|
"phone": phone,
|
||||||
|
"session_id": uuid.New().String(),
|
||||||
|
"type": "self_order_access",
|
||||||
|
"iat": now.Unix(),
|
||||||
|
"exp": expiresAt.Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
tokenString, err := token.SignedString([]byte(secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("failed to generate self-order session token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, expiresAt.Unix(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateSelfOrderToken(tokenString string, secret string) (jwt.MapClaims, error) {
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse self-order token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, fmt.Errorf("invalid self-order token")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid self-order token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenType, ok := claims["type"].(string)
|
||||||
|
if !ok || tokenType != "self_order_access" {
|
||||||
|
return nil, fmt.Errorf("invalid self-order token type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Test Ingredient Composition Endpoints
|
||||||
|
|
||||||
|
## 1. Add Compositions (Bulk)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST http://localhost:4000/api/v1/ingredients/{ingredient_id}/compositions
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZW1haWwiOiJhZG1pbkBnb2t1bmEuaWQiLCJyb2xlIjoiYWRtaW4iLCJvcmdhbml6YXRpb25faWQiOiIwMzIwNWQ4Zi0yYzA2LTQ1MTMtYTk1Mi0yMWQxMTA5Zjc4ZmYiLCJpc3MiOiJhcHNrZWwtcG9zIiwic3ViIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZXhwIjoxNzg1OTE0ODQ4LCJuYmYiOjE3NzcyNzQ4NDgsImlhdCI6MTc3NzI3NDg0OH0.prO4mU1zjVUZLgi5c8hj10A6ODETCMjnEP8wUQikZ30
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"compositions": [
|
||||||
|
{
|
||||||
|
"child_ingredient_id": "CHILD_INGREDIENT_UUID_1",
|
||||||
|
"quantity": 2.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child_ingredient_id": "CHILD_INGREDIENT_UUID_2",
|
||||||
|
"quantity": 1.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"parent_ingredient": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "Semi-Finished Product",
|
||||||
|
"cost": 15.50,
|
||||||
|
"compositions": [...]
|
||||||
|
},
|
||||||
|
"compositions": [
|
||||||
|
{
|
||||||
|
"id": "COMPOSITION_ID_1",
|
||||||
|
"child_ingredient_id": "...",
|
||||||
|
"quantity": 2.5,
|
||||||
|
"child_ingredient": {...},
|
||||||
|
"parent_ingredient": {...}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Update Composition
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PUT http://localhost:4000/api/v1/ingredients/compositions/{composition_id}
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZW1haWwiOiJhZG1pbkBnb2t1bmEuaWQiLCJyb2xlIjoiYWRtaW4iLCJvcmdhbml6YXRpb25faWQiOiIwMzIwNWQ4Zi0yYzA2LTQ1MTMtYTk1Mi0yMWQxMTA5Zjc4ZmYiLCJpc3MiOiJhcHNrZWwtcG9zIiwic3ViIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZXhwIjoxNzg1OTE0ODQ4LCJuYmYiOjE3NzcyNzQ4NDgsImlhdCI6MTc3NzI3NDg0OH0.prO4mU1zjVUZLgi5c8hj10A6ODETCMjnEP8wUQikZ30
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"quantity": 3.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "composition_id",
|
||||||
|
"child_ingredient_id": "...",
|
||||||
|
"quantity": 3.5,
|
||||||
|
"child_ingredient": {...},
|
||||||
|
"parent_ingredient": {
|
||||||
|
"id": "...",
|
||||||
|
"cost": 18.75,
|
||||||
|
"compositions": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Delete Composition
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DELETE http://localhost:4000/api/v1/ingredients/compositions/{composition_id}
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZW1haWwiOiJhZG1pbkBnb2t1bmEuaWQiLCJyb2xlIjoiYWRtaW4iLCJvcmdhbml6YXRpb25faWQiOiIwMzIwNWQ4Zi0yYzA2LTQ1MTMtYTk1Mi0yMWQxMTA5Zjc4ZmYiLCJpc3MiOiJhcHNrZWwtcG9zIiwic3ViIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZXhwIjoxNzg1OTE0ODQ4LCJuYmYiOjE3NzcyNzQ4NDgsImlhdCI6MTc3NzI3NDg0OH0.prO4mU1zjVUZLgi5c8hj10A6ODETCMjnEP8wUQikZ30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": "parent_ingredient_id",
|
||||||
|
"name": "Semi-Finished Product",
|
||||||
|
"cost": 12.25,
|
||||||
|
"compositions": [
|
||||||
|
// remaining compositions after deletion
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steps to Test:
|
||||||
|
|
||||||
|
1. **Get a semi-finished ingredient ID** (or create one first)
|
||||||
|
2. **Get child ingredient IDs** (raw ingredients to add as compositions)
|
||||||
|
3. **Add compositions** using the bulk endpoint
|
||||||
|
4. **Copy composition ID** from the response
|
||||||
|
5. **Update or Delete** using that composition ID
|
||||||
|
|
||||||
|
## cURL Commands:
|
||||||
|
|
||||||
|
### Delete Composition:
|
||||||
|
```bash
|
||||||
|
curl --request DELETE \
|
||||||
|
--url 'http://localhost:4000/api/v1/ingredients/compositions/YOUR_COMPOSITION_ID_HERE' \
|
||||||
|
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZW1haWwiOiJhZG1pbkBnb2t1bmEuaWQiLCJyb2xlIjoiYWRtaW4iLCJvcmdhbml6YXRpb25faWQiOiIwMzIwNWQ4Zi0yYzA2LTQ1MTMtYTk1Mi0yMWQxMTA5Zjc4ZmYiLCJpc3MiOiJhcHNrZWwtcG9zIiwic3ViIjoiNmM2ZDI0MjAtNzA0MS00ZWE5LWE3MjYtYTM0MDAxNGJiMjRkIiwiZXhwIjoxNzg1OTE0ODQ4LCJuYmYiOjE3NzcyNzQ4NDgsImlhdCI6MTc3NzI3NDg0OH0.prO4mU1zjVUZLgi5c8hj10A6ODETCMjnEP8wUQikZ30'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes:
|
||||||
|
|
||||||
|
- Replace `{ingredient_id}` with actual parent ingredient UUID
|
||||||
|
- Replace `{composition_id}` with actual composition UUID
|
||||||
|
- Replace `CHILD_INGREDIENT_UUID_1` and `CHILD_INGREDIENT_UUID_2` with actual child ingredient UUIDs
|
||||||
|
- Parent ingredient must have `is_semi_finished: true`
|
||||||
|
- All responses now include the updated parent ingredient with recalculated cost
|
||||||
Reference in New Issue
Block a user