Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7022dd4c1 | ||
|
|
3542104050 | ||
|
|
eb95459578 | ||
|
|
1a5ddd2b34 | ||
|
|
0aa280462c | ||
|
|
421475006b | ||
|
|
75fdb8e847 | ||
|
|
8efa644680 | ||
|
|
ce99aef289 | ||
|
|
ba970229a9 | ||
|
|
9b606b4c8b | ||
|
|
d3dddea1c7 | ||
|
|
80a78137a0 | ||
|
|
3826a6b7a9 | ||
|
|
d695bedc97 | ||
|
|
3db4afbce6 | ||
|
|
535e4c84f6 | ||
|
|
f25ec1c06f |
+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
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+3
-1
@@ -170,6 +170,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 +216,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 +304,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),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package appcontext
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -77,5 +78,18 @@ func FromContext(ctx context.Context) *ContextInfo {
|
|||||||
if info, ok := ctx.Value(ctxKey).(*ContextInfo); ok {
|
if info, ok := ctx.Value(ctxKey).(*ContextInfo); ok {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
return nil
|
// Fallback: construct ContextInfo from individual context values
|
||||||
|
return &ContextInfo{
|
||||||
|
CorrelationID: value(ctx, CorrelationIDKey),
|
||||||
|
UserID: uuidValue(ctx, UserIDKey),
|
||||||
|
OutletID: uuidValue(ctx, OutletIDKey),
|
||||||
|
OrganizationID: uuidValue(ctx, OrganizationIDKey),
|
||||||
|
AppVersion: value(ctx, AppVersionKey),
|
||||||
|
AppID: value(ctx, AppIDKey),
|
||||||
|
AppType: value(ctx, AppTypeKey),
|
||||||
|
Platform: value(ctx, PlatformKey),
|
||||||
|
DeviceOS: value(ctx, DeviceOSKey),
|
||||||
|
UserLocale: value(ctx, UserLocaleKey),
|
||||||
|
UserRole: value(ctx, UserRoleKey),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ type ProductAnalyticsResponse struct {
|
|||||||
Data []ProductAnalyticsData `json:"data"`
|
Data []ProductAnalyticsData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsData represents individual product analytics data
|
|
||||||
type ProductAnalyticsData struct {
|
type ProductAnalyticsData struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
@@ -113,6 +112,12 @@ type ProductAnalyticsData struct {
|
|||||||
Revenue float64 `json:"revenue"`
|
Revenue float64 `json:"revenue"`
|
||||||
AveragePrice float64 `json:"average_price"`
|
AveragePrice float64 `json:"average_price"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
|
StandardHppPerUnit float64 `json:"standard_hpp_per_unit"`
|
||||||
|
StandardHppTotal float64 `json:"standard_hpp_total"`
|
||||||
|
FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"`
|
||||||
|
FifoHppTotal float64 `json:"fifo_hpp_total"`
|
||||||
|
MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"`
|
||||||
|
MovingAverageHppTotal float64 `json:"moving_average_hpp_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||||
@@ -132,7 +137,6 @@ type ProductAnalyticsPerCategoryResponse struct {
|
|||||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategoryData represents individual category analytics data
|
|
||||||
type ProductAnalyticsPerCategoryData struct {
|
type ProductAnalyticsPerCategoryData struct {
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
@@ -140,6 +144,9 @@ type ProductAnalyticsPerCategoryData struct {
|
|||||||
TotalQuantity int64 `json:"total_quantity"`
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
ProductCount int64 `json:"product_count"`
|
ProductCount int64 `json:"product_count"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -185,7 +185,7 @@ type CreatePaymentRequest struct {
|
|||||||
|
|
||||||
type CreatePaymentOrderItemRequest struct {
|
type CreatePaymentOrderItemRequest struct {
|
||||||
OrderItemID uuid.UUID `json:"order_item_id" validate:"required"`
|
OrderItemID uuid.UUID `json:"order_item_id" validate:"required"`
|
||||||
Amount float64 `json:"amount" validate:"required,min=0"`
|
Amount float64 `json:"amount" validate:"min=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentResponse struct {
|
type PaymentResponse struct {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type ProductResponse struct {
|
|||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
SKU *string `json:"sku"`
|
SKU *string `json:"sku"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ type SalesAnalytics struct {
|
|||||||
NetSales float64 `json:"net_sales"`
|
NetSales float64 `json:"net_sales"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalytics represents product analytics data
|
|
||||||
type ProductAnalytics struct {
|
type ProductAnalytics struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
@@ -39,9 +38,14 @@ type ProductAnalytics struct {
|
|||||||
Revenue float64 `json:"revenue"`
|
Revenue float64 `json:"revenue"`
|
||||||
AveragePrice float64 `json:"average_price"`
|
AveragePrice float64 `json:"average_price"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
|
StandardHppPerUnit float64 `json:"standard_hpp_per_unit"`
|
||||||
|
StandardHppTotal float64 `json:"standard_hpp_total"`
|
||||||
|
FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"`
|
||||||
|
FifoHppTotal float64 `json:"fifo_hpp_total"`
|
||||||
|
MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"`
|
||||||
|
MovingAverageHppTotal float64 `json:"moving_average_hpp_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategory represents product analytics data grouped by category
|
|
||||||
type ProductAnalyticsPerCategory struct {
|
type ProductAnalyticsPerCategory struct {
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
@@ -49,6 +53,9 @@ type ProductAnalyticsPerCategory struct {
|
|||||||
TotalQuantity int64 `json:"total_quantity"`
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
ProductCount int64 `json:"product_count"`
|
ProductCount int64 `json:"product_count"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardOverview represents dashboard overview data
|
// DashboardOverview represents dashboard overview data
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ type Ingredient struct {
|
|||||||
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" 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,37 @@ func (h *OutletHandler) GetOutlet(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::GetOutlet")
|
util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::GetOutlet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *OutletHandler) CreateOutlet(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var req contract.CreateOutletRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("OutletHandler::CreateOutlet -> Failed to bind JSON")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid request body")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::CreateOutlet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.outletValidator.ValidateCreateOutletRequest(&req)
|
||||||
|
if validationError != nil {
|
||||||
|
logger.FromContext(ctx).WithError(validationError).Error("OutletHandler::CreateOutlet -> request validation failed")
|
||||||
|
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::CreateOutlet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outletResponse := h.outletService.CreateOutlet(ctx, &req)
|
||||||
|
if outletResponse.HasErrors() {
|
||||||
|
errorResp := outletResponse.GetErrors()[0]
|
||||||
|
logger.FromContext(ctx).WithError(errorResp).Error("OutletHandler::CreateOutlet -> Failed to create outlet from service")
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::CreateOutlet")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *OutletHandler) UpdateOutlet(c *gin.Context) {
|
func (h *OutletHandler) UpdateOutlet(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProductEntityToModel(entity *entities.Product) *models.Product {
|
func ProductEntityToModel(entity *entities.Product) *models.Product {
|
||||||
@@ -118,10 +120,17 @@ func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get category name from the Category relation
|
||||||
|
categoryName := ""
|
||||||
|
if entity.Category.ID != uuid.Nil {
|
||||||
|
categoryName = entity.Category.Name
|
||||||
|
}
|
||||||
|
|
||||||
return &models.ProductResponse{
|
return &models.ProductResponse{
|
||||||
ID: entity.ID,
|
ID: entity.ID,
|
||||||
OrganizationID: entity.OrganizationID,
|
OrganizationID: entity.OrganizationID,
|
||||||
CategoryID: entity.CategoryID,
|
CategoryID: entity.CategoryID,
|
||||||
|
CategoryName: categoryName,
|
||||||
SKU: entity.SKU,
|
SKU: entity.SKU,
|
||||||
Name: entity.Name,
|
Name: entity.Name,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ type ProductAnalyticsResponse struct {
|
|||||||
Data []ProductAnalyticsData `json:"data"`
|
Data []ProductAnalyticsData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsData represents individual product analytics data
|
|
||||||
type ProductAnalyticsData struct {
|
type ProductAnalyticsData struct {
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
@@ -117,6 +116,12 @@ type ProductAnalyticsData struct {
|
|||||||
Revenue float64 `json:"revenue"`
|
Revenue float64 `json:"revenue"`
|
||||||
AveragePrice float64 `json:"average_price"`
|
AveragePrice float64 `json:"average_price"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
|
StandardHppPerUnit float64 `json:"standard_hpp_per_unit"`
|
||||||
|
StandardHppTotal float64 `json:"standard_hpp_total"`
|
||||||
|
FifoHppPerUnit float64 `json:"fifo_hpp_per_unit"`
|
||||||
|
FifoHppTotal float64 `json:"fifo_hpp_total"`
|
||||||
|
MovingAverageHppPerUnit float64 `json:"moving_average_hpp_per_unit"`
|
||||||
|
MovingAverageHppTotal float64 `json:"moving_average_hpp_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||||
@@ -136,7 +141,6 @@ type ProductAnalyticsPerCategoryResponse struct {
|
|||||||
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
Data []ProductAnalyticsPerCategoryData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategoryData represents individual category analytics data
|
|
||||||
type ProductAnalyticsPerCategoryData struct {
|
type ProductAnalyticsPerCategoryData struct {
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
@@ -144,6 +148,9 @@ type ProductAnalyticsPerCategoryData struct {
|
|||||||
TotalQuantity int64 `json:"total_quantity"`
|
TotalQuantity int64 `json:"total_quantity"`
|
||||||
ProductCount int64 `json:"product_count"`
|
ProductCount int64 `json:"product_count"`
|
||||||
OrderCount int64 `json:"order_count"`
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||||
|
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||||
|
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ type CreateIngredientRequest struct {
|
|||||||
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 {
|
||||||
@@ -61,7 +69,6 @@ type IngredientResponse struct {
|
|||||||
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"`
|
||||||
|
|
||||||
// Relations
|
|
||||||
Unit *Unit `json:"unit,omitempty"`
|
Unit *Unit `json:"unit,omitempty"`
|
||||||
|
Compositions []*IngredientCompositionResponse `json:"compositions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,28 +6,6 @@ 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"`
|
||||||
@@ -35,15 +13,26 @@ type UpdateIngredientCompositionRequest struct {
|
|||||||
|
|
||||||
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"`
|
||||||
ParentIngredientID uuid.UUID `json:"parent_ingredient_id"`
|
|
||||||
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
ChildIngredientID uuid.UUID `json:"child_ingredient_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ChildIngredient *IngredientResponse `json:"child_ingredient,omitempty"`
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ type ProductResponse struct {
|
|||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
CategoryID uuid.UUID
|
CategoryID uuid.UUID
|
||||||
|
CategoryName string
|
||||||
SKU *string
|
SKU *string
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
|
|||||||
@@ -195,6 +195,12 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m
|
|||||||
Revenue: data.Revenue,
|
Revenue: data.Revenue,
|
||||||
AveragePrice: data.AveragePrice,
|
AveragePrice: data.AveragePrice,
|
||||||
OrderCount: data.OrderCount,
|
OrderCount: data.OrderCount,
|
||||||
|
StandardHppPerUnit: data.StandardHppPerUnit,
|
||||||
|
StandardHppTotal: data.StandardHppTotal,
|
||||||
|
FifoHppPerUnit: data.FifoHppPerUnit,
|
||||||
|
FifoHppTotal: data.FifoHppTotal,
|
||||||
|
MovingAverageHppPerUnit: data.MovingAverageHppPerUnit,
|
||||||
|
MovingAverageHppTotal: data.MovingAverageHppTotal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +235,9 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont
|
|||||||
TotalQuantity: data.TotalQuantity,
|
TotalQuantity: data.TotalQuantity,
|
||||||
ProductCount: data.ProductCount,
|
ProductCount: data.ProductCount,
|
||||||
OrderCount: data.OrderCount,
|
OrderCount: data.OrderCount,
|
||||||
|
TotalStandardHpp: data.TotalStandardHpp,
|
||||||
|
TotalFifoHpp: data.TotalFifoHpp,
|
||||||
|
TotalMovingAverageHpp: data.TotalMovingAverageHpp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ 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"
|
||||||
@@ -14,18 +15,19 @@ import (
|
|||||||
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 {
|
||||||
|
if err := p.saveCompositions(ctx, ingredient.ID, req.OrganizationID, req.Compositions); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return p.buildIngredientResponse(ctx, ingredient.ID, req.OrganizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
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,83 +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
|
||||||
existingIngredient.OutletID = req.OutletID
|
existing.Name = req.Name
|
||||||
existingIngredient.Name = req.Name
|
existing.UnitID = req.UnitID
|
||||||
existingIngredient.UnitID = req.UnitID
|
existing.Stock = req.Stock
|
||||||
existingIngredient.Cost = req.Cost
|
existing.IsSemiFinished = req.IsSemiFinished
|
||||||
existingIngredient.Stock = req.Stock
|
existing.IsActive = req.IsActive
|
||||||
existingIngredient.IsSemiFinished = req.IsSemiFinished
|
existing.Metadata = req.Metadata
|
||||||
existingIngredient.IsActive = req.IsActive
|
existing.UpdatedAt = time.Now()
|
||||||
existingIngredient.Metadata = req.Metadata
|
|
||||||
existingIngredient.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
// Save to database
|
// Cost hanya dipakai kalau bukan semi-finished
|
||||||
err = p.ingredientRepo.Update(ctx, existingIngredient)
|
// Kalau semi-finished, cost dihitung dari compositions
|
||||||
if err != nil {
|
if !req.IsSemiFinished {
|
||||||
|
existing.Cost = req.Cost
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.ingredientRepo.Update(ctx, existing); 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (p *OutletProcessorImpl) GetOutletByID(ctx context.Context, organizationID,
|
|||||||
func (p *OutletProcessorImpl) CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error) {
|
func (p *OutletProcessorImpl) CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error) {
|
||||||
// Get organization ID from context
|
// Get organization ID from context
|
||||||
contextInfo := appcontext.FromContext(ctx)
|
contextInfo := appcontext.FromContext(ctx)
|
||||||
if contextInfo.OrganizationID == uuid.Nil {
|
if contextInfo == nil || contextInfo.OrganizationID == uuid.Nil {
|
||||||
return nil, fmt.Errorf("organization ID not found in context")
|
return nil, fmt.Errorf("organization ID not found in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ func (p *OutletProcessorImpl) UpdateOutlet(ctx context.Context, outletID uuid.UU
|
|||||||
|
|
||||||
func (p *OutletProcessorImpl) DeleteOutlet(ctx context.Context, outletID uuid.UUID) error {
|
func (p *OutletProcessorImpl) DeleteOutlet(ctx context.Context, outletID uuid.UUID) error {
|
||||||
contextInfo := appcontext.FromContext(ctx)
|
contextInfo := appcontext.FromContext(ctx)
|
||||||
if contextInfo.OrganizationID == uuid.Nil {
|
if contextInfo == nil || contextInfo.OrganizationID == uuid.Nil {
|
||||||
return fmt.Errorf("organization ID not found in context")
|
return fmt.Errorf("organization ID not found in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/mappers"
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -368,6 +367,8 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
|
|||||||
return nil, fmt.Errorf("purchase order not found: %w", err)
|
return nil, fmt.Errorf("purchase order not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("status:", po.Status)
|
||||||
|
|
||||||
// Check if status is changing to "received" and current status is not "received"
|
// Check if status is changing to "received" and current status is not "received"
|
||||||
if status == "received" && po.Status != "received" {
|
if status == "received" && po.Status != "received" {
|
||||||
// Get purchase order with items for inventory update
|
// Get purchase order with items for inventory update
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,11 +134,45 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
|||||||
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity)
|
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity)
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END as average_price,
|
END as average_price,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
|
COALESCE((
|
||||||
|
SELECT SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost)
|
||||||
|
FROM product_recipes pr
|
||||||
|
JOIN ingredients i ON pr.ingredient_id = i.id
|
||||||
|
WHERE pr.product_id = p.id
|
||||||
|
), p.cost, 0) as standard_hpp_per_unit,
|
||||||
|
COALESCE((
|
||||||
|
SELECT SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost)
|
||||||
|
FROM product_recipes pr
|
||||||
|
JOIN ingredients i ON pr.ingredient_id = i.id
|
||||||
|
WHERE pr.product_id = p.id
|
||||||
|
), p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as standard_hpp_total,
|
||||||
|
CASE
|
||||||
|
WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_cost), 0) / SUM(oi.quantity)
|
||||||
|
ELSE 0
|
||||||
|
END as fifo_hpp_per_unit,
|
||||||
|
COALESCE(SUM(oi.total_cost), 0) as fifo_hpp_total,
|
||||||
|
COALESCE(mahpp.hpp_per_unit, p.cost, 0) as moving_average_hpp_per_unit,
|
||||||
|
COALESCE(mahpp.hpp_per_unit, p.cost, 0) * COALESCE(SUM(oi.quantity), 0) as moving_average_hpp_total
|
||||||
`).
|
`).
|
||||||
Joins("JOIN products p ON oi.product_id = p.id").
|
Joins("JOIN products p ON oi.product_id = p.id").
|
||||||
Joins("JOIN categories c ON p.category_id = c.id").
|
Joins("JOIN categories c ON p.category_id = c.id").
|
||||||
Joins("JOIN orders o ON oi.order_id = o.id").
|
Joins("JOIN orders o ON oi.order_id = o.id").
|
||||||
|
Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id",
|
||||||
|
r.db.Table("product_recipes pr2").
|
||||||
|
Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit").
|
||||||
|
Joins("JOIN ingredients ing ON pr2.ingredient_id = ing.id").
|
||||||
|
Joins("LEFT JOIN (?) ma ON ma.ingredient_id = pr2.ingredient_id",
|
||||||
|
r.db.Table("inventory_movements im").
|
||||||
|
Select("im.item_id as ingredient_id, CASE WHEN SUM(im.quantity) > 0 THEN SUM(im.total_cost) / SUM(im.quantity) ELSE 0 END as moving_avg_cost").
|
||||||
|
Where("im.movement_type = ?", "purchase").
|
||||||
|
Where("im.item_type = ?", "INGREDIENT").
|
||||||
|
Where("im.organization_id = ?", organizationID).
|
||||||
|
Where("im.created_at <= ?", dateTo).
|
||||||
|
Group("im.item_id"),
|
||||||
|
).
|
||||||
|
Group("pr2.product_id"),
|
||||||
|
).
|
||||||
Where("o.organization_id = ?", organizationID).
|
Where("o.organization_id = ?", organizationID).
|
||||||
Where("o.is_void = ?", false).
|
Where("o.is_void = ?", false).
|
||||||
Where("o.is_refund = ?", false).
|
Where("o.is_refund = ?", false).
|
||||||
@@ -141,7 +185,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("p.id, p.name, c.id, c.name, c.order").
|
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
|
||||||
Order("revenue DESC").
|
Order("revenue DESC").
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Scan(&results).Error
|
Scan(&results).Error
|
||||||
@@ -160,11 +204,30 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
|
|||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as total_revenue,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_price - COALESCE(oi.refund_amount, 0) ELSE 0 END), 0) as total_revenue,
|
||||||
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.quantity - COALESCE(oi.refund_quantity, 0) ELSE 0 END), 0) as total_quantity,
|
||||||
COUNT(DISTINCT p.id) as product_count,
|
COUNT(DISTINCT p.id) as product_count,
|
||||||
COUNT(DISTINCT oi.order_id) as order_count
|
COUNT(DISTINCT oi.order_id) as order_count,
|
||||||
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(shpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_standard_hpp,
|
||||||
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN oi.total_cost * ((oi.quantity - COALESCE(oi.refund_quantity, 0))::float / NULLIF(oi.quantity, 0)) ELSE 0 END), 0) as total_fifo_hpp,
|
||||||
|
COALESCE(SUM(CASE WHEN oi.is_fully_refunded = false THEN COALESCE(mahpp.hpp_per_unit, p.cost, 0) * (oi.quantity - COALESCE(oi.refund_quantity, 0)) ELSE 0 END), 0) as total_moving_average_hpp
|
||||||
`).
|
`).
|
||||||
Joins("JOIN products p ON oi.product_id = p.id").
|
Joins("JOIN products p ON oi.product_id = p.id").
|
||||||
Joins("JOIN categories c ON p.category_id = c.id").
|
Joins("JOIN categories c ON p.category_id = c.id").
|
||||||
Joins("JOIN orders o ON oi.order_id = o.id").
|
Joins("JOIN orders o ON oi.order_id = o.id").
|
||||||
|
Joins("LEFT JOIN (SELECT pr.product_id, SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost) as hpp_per_unit FROM product_recipes pr JOIN ingredients i ON pr.ingredient_id = i.id GROUP BY pr.product_id) shpp ON shpp.product_id = p.id").
|
||||||
|
Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id",
|
||||||
|
r.db.Table("product_recipes pr2").
|
||||||
|
Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit").
|
||||||
|
Joins("JOIN ingredients ing ON pr2.ingredient_id = ing.id").
|
||||||
|
Joins("LEFT JOIN (?) ma ON ma.ingredient_id = pr2.ingredient_id",
|
||||||
|
r.db.Table("inventory_movements im").
|
||||||
|
Select("im.item_id as ingredient_id, CASE WHEN SUM(im.quantity) > 0 THEN SUM(im.total_cost) / SUM(im.quantity) ELSE 0 END as moving_avg_cost").
|
||||||
|
Where("im.movement_type = ?", "purchase").
|
||||||
|
Where("im.item_type = ?", "INGREDIENT").
|
||||||
|
Where("im.organization_id = ?", organizationID).
|
||||||
|
Where("im.created_at <= ?", dateTo).
|
||||||
|
Group("im.item_id"),
|
||||||
|
).
|
||||||
|
Group("pr2.product_id"),
|
||||||
|
).
|
||||||
Where("o.organization_id = ?", organizationID).
|
Where("o.organization_id = ?", organizationID).
|
||||||
Where("o.is_void = ?", false).
|
Where("o.is_void = ?", false).
|
||||||
Where("o.is_refund = ?", false).
|
Where("o.is_refund = ?", false).
|
||||||
|
|||||||
@@ -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 := `
|
|
||||||
SELECT ic.id, ic.organization_id, ic.outlet_id, ic.parent_ingredient_id, ic.child_ingredient_id, ic.quantity, ic.created_at, ic.updated_at,
|
|
||||||
pi.id, pi.organization_id, pi.outlet_id, pi.name, pi.unit_id, pi.cost, pi.stock, pi.is_semi_finished, pi.is_active, pi.metadata, pi.created_at, pi.updated_at,
|
|
||||||
ci.id, ci.organization_id, ci.outlet_id, ci.name, ci.unit_id, ci.cost, ci.stock, ci.is_semi_finished, ci.is_active, ci.metadata, ci.created_at, ci.updated_at
|
|
||||||
FROM ingredient_compositions ic
|
|
||||||
LEFT JOIN ingredients pi ON ic.parent_ingredient_id = pi.id
|
|
||||||
LEFT JOIN ingredients ci ON ic.child_ingredient_id = ci.id
|
|
||||||
WHERE ic.parent_ingredient_id = $1 AND ic.organization_id = $2
|
|
||||||
ORDER BY ic.created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := r.db.QueryContext(ctx, query, parentIngredientID, organizationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var compositions []*entities.IngredientComposition
|
var compositions []*entities.IngredientComposition
|
||||||
for rows.Next() {
|
err := r.db.WithContext(ctx).
|
||||||
composition := &entities.IngredientComposition{}
|
Preload("ChildIngredient.Unit").
|
||||||
parentIngredient := &entities.Ingredient{}
|
Where("parent_ingredient_id = ? AND organization_id = ?", parentIngredientID, organizationID).
|
||||||
childIngredient := &entities.Ingredient{}
|
Order("created_at ASC").
|
||||||
|
Find(&compositions).Error
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
}
|
}
|
||||||
@@ -66,7 +76,11 @@ func (r *IngredientRepository) GetAll(ctx context.Context, organizationID uuid.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *IngredientRepository) Update(ctx context.Context, ingredient *entities.Ingredient) error {
|
func (r *IngredientRepository) Update(ctx context.Context, ingredient *entities.Ingredient) error {
|
||||||
result := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", ingredient.ID, ingredient.OrganizationID).Save(ingredient)
|
result := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.Ingredient{}).
|
||||||
|
Where("id = ? AND organization_id = ?", ingredient.ID, ingredient.OrganizationID).
|
||||||
|
Select("outlet_id", "name", "unit_id", "cost", "stock", "is_semi_finished", "is_active", "metadata", "updated_at").
|
||||||
|
Updates(ingredient)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (r *ProductRecipeRepository) GetByID(ctx context.Context, id, organizationI
|
|||||||
Preload("Product").
|
Preload("Product").
|
||||||
Preload("ProductVariant").
|
Preload("ProductVariant").
|
||||||
Preload("Ingredient").
|
Preload("Ingredient").
|
||||||
|
Preload("Ingredient.Unit").
|
||||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
First(&productRecipe).Error
|
First(&productRecipe).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,6 +42,7 @@ func (r *ProductRecipeRepository) GetByProductID(ctx context.Context, productID,
|
|||||||
Preload("Product").
|
Preload("Product").
|
||||||
Preload("ProductVariant").
|
Preload("ProductVariant").
|
||||||
Preload("Ingredient").
|
Preload("Ingredient").
|
||||||
|
Preload("Ingredient.Unit").
|
||||||
Where("product_id = ? AND organization_id = ?", productID, organizationID).
|
Where("product_id = ? AND organization_id = ?", productID, organizationID).
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Find(&productRecipes).Error
|
Find(&productRecipes).Error
|
||||||
@@ -56,6 +58,7 @@ func (r *ProductRecipeRepository) GetByProductAndVariantID(ctx context.Context,
|
|||||||
Preload("Product").
|
Preload("Product").
|
||||||
Preload("ProductVariant").
|
Preload("ProductVariant").
|
||||||
Preload("Ingredient").
|
Preload("Ingredient").
|
||||||
|
Preload("Ingredient.Unit").
|
||||||
Where("product_id = ? AND organization_id = ?", productID, organizationID)
|
Where("product_id = ? AND organization_id = ?", productID, organizationID)
|
||||||
|
|
||||||
if variantID != nil {
|
if variantID != nil {
|
||||||
@@ -77,6 +80,7 @@ func (r *ProductRecipeRepository) GetByIngredientID(ctx context.Context, ingredi
|
|||||||
Preload("Product").
|
Preload("Product").
|
||||||
Preload("ProductVariant").
|
Preload("ProductVariant").
|
||||||
Preload("Ingredient").
|
Preload("Ingredient").
|
||||||
|
Preload("Ingredient.Unit").
|
||||||
Where("ingredient_id = ? AND organization_id = ?", ingredientID, organizationID).
|
Where("ingredient_id = ? AND organization_id = ?", ingredientID, organizationID).
|
||||||
Order("created_at DESC").
|
Order("created_at DESC").
|
||||||
Find(&productRecipes).Error
|
Find(&productRecipes).Error
|
||||||
|
|||||||
+14
-59
@@ -50,65 +50,7 @@ type Router struct {
|
|||||||
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
customerAuthMiddleware *middleware.CustomerAuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config,
|
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 {
|
||||||
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 {
|
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -146,6 +88,7 @@ func NewRouter(cfg *config.Config,
|
|||||||
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
|
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
customerAuthMiddleware: customerAuthMiddleware,
|
customerAuthMiddleware: customerAuthMiddleware,
|
||||||
|
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +213,14 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
products.DELETE("/:id", r.productHandler.DeleteProduct)
|
products.DELETE("/:id", r.productHandler.DeleteProduct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
productVariants := protected.Group("/product-variants")
|
||||||
|
{
|
||||||
|
productVariants.POST("", r.productVariantHandler.CreateProductVariant)
|
||||||
|
productVariants.PUT("/:id", r.productVariantHandler.UpdateProductVariant)
|
||||||
|
productVariants.DELETE("/:id", r.productVariantHandler.DeleteProductVariant)
|
||||||
|
productVariants.GET("/:id", r.productVariantHandler.GetProductVariant)
|
||||||
|
}
|
||||||
|
|
||||||
inventory := protected.Group("/inventory")
|
inventory := protected.Group("/inventory")
|
||||||
inventory.Use(r.authMiddleware.RequireAdminOrManager())
|
inventory.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
@@ -371,6 +322,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")
|
||||||
@@ -594,6 +548,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
outlets := protected.Group("/outlets")
|
outlets := protected.Group("/outlets")
|
||||||
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
|
outlets.POST("", r.outletHandler.CreateOutlet)
|
||||||
outlets.GET("/list", r.outletHandler.ListOutlets)
|
outlets.GET("/list", r.outletHandler.ListOutlets)
|
||||||
outlets.GET("/detail/:id", r.outletHandler.GetOutlet)
|
outlets.GET("/detail/:id", r.outletHandler.GetOutlet)
|
||||||
outlets.PUT("/detail/:id", r.outletHandler.UpdateOutlet)
|
outlets.PUT("/detail/:id", r.outletHandler.UpdateOutlet)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con
|
|||||||
Revenue: item.Revenue,
|
Revenue: item.Revenue,
|
||||||
AveragePrice: item.AveragePrice,
|
AveragePrice: item.AveragePrice,
|
||||||
OrderCount: item.OrderCount,
|
OrderCount: item.OrderCount,
|
||||||
|
StandardHppPerUnit: item.StandardHppPerUnit,
|
||||||
|
StandardHppTotal: item.StandardHppTotal,
|
||||||
|
FifoHppPerUnit: item.FifoHppPerUnit,
|
||||||
|
FifoHppTotal: item.FifoHppTotal,
|
||||||
|
MovingAverageHppPerUnit: item.MovingAverageHppPerUnit,
|
||||||
|
MovingAverageHppTotal: item.MovingAverageHppTotal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +220,9 @@ func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPer
|
|||||||
TotalQuantity: item.TotalQuantity,
|
TotalQuantity: item.TotalQuantity,
|
||||||
ProductCount: item.ProductCount,
|
ProductCount: item.ProductCount,
|
||||||
OrderCount: item.OrderCount,
|
OrderCount: item.OrderCount,
|
||||||
|
TotalStandardHpp: item.TotalStandardHpp,
|
||||||
|
TotalFifoHpp: item.TotalFifoHpp,
|
||||||
|
TotalMovingAverageHpp: item.TotalMovingAverageHpp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +274,12 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse)
|
|||||||
Revenue: item.Revenue,
|
Revenue: item.Revenue,
|
||||||
AveragePrice: item.AveragePrice,
|
AveragePrice: item.AveragePrice,
|
||||||
OrderCount: item.OrderCount,
|
OrderCount: item.OrderCount,
|
||||||
|
StandardHppPerUnit: item.StandardHppPerUnit,
|
||||||
|
StandardHppTotal: item.StandardHppTotal,
|
||||||
|
FifoHppPerUnit: item.FifoHppPerUnit,
|
||||||
|
FifoHppTotal: item.FifoHppTotal,
|
||||||
|
MovingAverageHppPerUnit: item.MovingAverageHppPerUnit,
|
||||||
|
MovingAverageHppTotal: item.MovingAverageHppTotal,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -101,6 +101,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
|
|||||||
ID: prod.ID,
|
ID: prod.ID,
|
||||||
OrganizationID: prod.OrganizationID,
|
OrganizationID: prod.OrganizationID,
|
||||||
CategoryID: prod.CategoryID,
|
CategoryID: prod.CategoryID,
|
||||||
|
CategoryName: prod.CategoryName,
|
||||||
SKU: prod.SKU,
|
SKU: prod.SKU,
|
||||||
Name: prod.Name,
|
Name: prod.Name,
|
||||||
Description: prod.Description,
|
Description: prod.Description,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- =========================
|
||||||
|
-- DROP ORDER DISCOUNTS
|
||||||
|
-- =========================
|
||||||
|
DROP INDEX IF EXISTS idx_order_discounts_discount;
|
||||||
|
DROP INDEX IF EXISTS idx_order_discounts_outlet;
|
||||||
|
DROP INDEX IF EXISTS idx_order_discounts_order;
|
||||||
|
DROP TABLE IF EXISTS order_discounts;
|
||||||
|
|
||||||
|
-- =========================
|
||||||
|
-- DROP DISCOUNT CATEGORIES
|
||||||
|
-- =========================
|
||||||
|
DROP INDEX IF EXISTS idx_discount_categories_category;
|
||||||
|
DROP INDEX IF EXISTS idx_discount_categories_discount;
|
||||||
|
DROP TABLE IF EXISTS discount_categories;
|
||||||
|
|
||||||
|
-- =========================
|
||||||
|
-- DROP DISCOUNT PRODUCTS
|
||||||
|
-- =========================
|
||||||
|
DROP INDEX IF EXISTS idx_discount_products_product;
|
||||||
|
DROP INDEX IF EXISTS idx_discount_products_discount;
|
||||||
|
DROP TABLE IF EXISTS discount_products;
|
||||||
|
|
||||||
|
-- =========================
|
||||||
|
-- DROP DISCOUNT OUTLETS
|
||||||
|
-- =========================
|
||||||
|
DROP INDEX IF EXISTS idx_discount_outlets_outlet;
|
||||||
|
DROP INDEX IF EXISTS idx_discount_outlets_discount;
|
||||||
|
DROP TABLE IF EXISTS discount_outlets;
|
||||||
|
|
||||||
|
-- =========================
|
||||||
|
-- DROP DISCOUNTS
|
||||||
|
-- =========================
|
||||||
|
DROP INDEX IF EXISTS idx_discounts_customer_type;
|
||||||
|
DROP INDEX IF EXISTS idx_discounts_dates;
|
||||||
|
DROP INDEX IF EXISTS idx_discounts_active;
|
||||||
|
DROP INDEX IF EXISTS idx_discounts_code;
|
||||||
|
DROP INDEX IF EXISTS idx_discounts_organization;
|
||||||
|
DROP INDEX IF EXISTS idx_discounts_campaign;
|
||||||
|
DROP TABLE IF EXISTS discounts;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
CREATE TABLE discounts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
campaign_id UUID NULL,
|
||||||
|
organization_id UUID NOT NULL,
|
||||||
|
code VARCHAR(50) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('percentage', 'fixed_amount', 'free_product')),
|
||||||
|
value DECIMAL(15,2) NOT NULL,
|
||||||
|
min_purchase_qty INT DEFAULT 0,
|
||||||
|
min_purchase_amount DECIMAL(15,2) DEFAULT 0,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE NOT NULL,
|
||||||
|
usage_limit_per_customer INT DEFAULT NULL,
|
||||||
|
usage_limit_total INT DEFAULT NULL,
|
||||||
|
usage_count INT DEFAULT 0,
|
||||||
|
customer_type VARCHAR(20) DEFAULT 'all' CHECK (customer_type IN ('all', 'member', 'vip')),
|
||||||
|
is_stackable BOOLEAN DEFAULT FALSE,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
priority INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (campaign_id) REFERENCES campaigns(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT chk_discount_ownership CHECK (campaign_id IS NOT NULL OR organization_id IS NOT NULL),
|
||||||
|
CONSTRAINT unique_code_per_org UNIQUE (organization_id, code)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_discounts_campaign ON discounts(campaign_id);
|
||||||
|
CREATE INDEX idx_discounts_organization ON discounts(organization_id);
|
||||||
|
CREATE INDEX idx_discounts_code ON discounts(code);
|
||||||
|
CREATE INDEX idx_discounts_active ON discounts(is_active);
|
||||||
|
CREATE INDEX idx_discounts_dates ON discounts(start_date, end_date);
|
||||||
|
CREATE INDEX idx_discounts_customer_type ON discounts(customer_type);
|
||||||
|
|
||||||
|
CREATE TABLE discount_outlets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
discount_id UUID NOT NULL,
|
||||||
|
outlet_id UUID NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT unique_discount_outlet UNIQUE (discount_id, outlet_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_discount_outlets_discount ON discount_outlets(discount_id);
|
||||||
|
CREATE INDEX idx_discount_outlets_outlet ON discount_outlets(outlet_id);
|
||||||
|
|
||||||
|
CREATE TABLE discount_products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
discount_id UUID NOT NULL,
|
||||||
|
product_id UUID NOT NULL,
|
||||||
|
rule_type VARCHAR(20) NOT NULL CHECK (rule_type IN ('required', 'free', 'excluded')),
|
||||||
|
quantity INT DEFAULT 1,
|
||||||
|
free_quantity INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_discount_products_discount ON discount_products(discount_id);
|
||||||
|
CREATE INDEX idx_discount_products_product ON discount_products(product_id);
|
||||||
|
|
||||||
|
CREATE TABLE discount_categories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
discount_id UUID NOT NULL,
|
||||||
|
category_id UUID NOT NULL,
|
||||||
|
rule_type VARCHAR(20) NOT NULL CHECK (rule_type IN ('included', 'excluded')),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_discount_categories_discount ON discount_categories(discount_id);
|
||||||
|
CREATE INDEX idx_discount_categories_category ON discount_categories(category_id);
|
||||||
|
|
||||||
|
CREATE TABLE order_discounts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
order_id UUID NOT NULL,
|
||||||
|
outlet_id UUID NOT NULL,
|
||||||
|
discount_id UUID NOT NULL,
|
||||||
|
discount_amount DECIMAL(15,2) NOT NULL,
|
||||||
|
applied_rules JSON,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE RESTRICT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_order_discounts_order ON order_discounts(order_id);
|
||||||
|
CREATE INDEX idx_order_discounts_outlet ON order_discounts(outlet_id);
|
||||||
|
CREATE INDEX idx_order_discounts_discount ON order_discounts(discount_id);
|
||||||
@@ -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