Update product receipe
This commit is contained in:
parent
265248ba49
commit
b72ab4ef3d
@ -79,6 +79,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
validators.tableValidator,
|
validators.tableValidator,
|
||||||
services.unitService,
|
services.unitService,
|
||||||
services.ingredientService,
|
services.ingredientService,
|
||||||
|
services.productRecipeService,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -144,6 +145,7 @@ type repositories struct {
|
|||||||
tableRepo repository.TableRepositoryInterface
|
tableRepo repository.TableRepositoryInterface
|
||||||
unitRepo *repository.UnitRepository
|
unitRepo *repository.UnitRepository
|
||||||
ingredientRepo *repository.IngredientRepository
|
ingredientRepo *repository.IngredientRepository
|
||||||
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@ -168,6 +170,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),
|
||||||
|
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +191,7 @@ type processors struct {
|
|||||||
tableProcessor *processor.TableProcessor
|
tableProcessor *processor.TableProcessor
|
||||||
unitProcessor *processor.UnitProcessorImpl
|
unitProcessor *processor.UnitProcessorImpl
|
||||||
ingredientProcessor *processor.IngredientProcessorImpl
|
ingredientProcessor *processor.IngredientProcessorImpl
|
||||||
|
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,6 +215,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
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),
|
||||||
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,6 +239,7 @@ type services struct {
|
|||||||
tableService *service.TableServiceImpl
|
tableService *service.TableServiceImpl
|
||||||
unitService *service.UnitServiceImpl
|
unitService *service.UnitServiceImpl
|
||||||
ingredientService *service.IngredientServiceImpl
|
ingredientService *service.IngredientServiceImpl
|
||||||
|
productRecipeService *service.ProductRecipeServiceImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -256,6 +262,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
|
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
|
||||||
unitService := service.NewUnitService(processors.unitProcessor)
|
unitService := service.NewUnitService(processors.unitProcessor)
|
||||||
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
|
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
|
||||||
|
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
userService: service.NewUserService(processors.userProcessor),
|
userService: service.NewUserService(processors.userProcessor),
|
||||||
@ -276,6 +283,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
tableService: tableService,
|
tableService: tableService,
|
||||||
unitService: unitService,
|
unitService: unitService,
|
||||||
ingredientService: ingredientService,
|
ingredientService: ingredientService,
|
||||||
|
productRecipeService: productRecipeService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
internal/contract/product_recipe_contract.go
Normal file
18
internal/contract/product_recipe_contract.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipeContract interface {
|
||||||
|
Create(request *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
GetByID(id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
GetByProductID(productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
GetByProductAndVariantID(productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
GetByIngredientID(ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
Update(id uuid.UUID, request *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
Delete(id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
BulkCreate(recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
}
|
||||||
36
internal/entities/product_recipe.go
Normal file
36
internal/entities/product_recipe.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipe struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
|
||||||
|
VariantID *uuid.UUID `gorm:"type:uuid;index" json:"variant_id"`
|
||||||
|
IngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"ingredient_id"`
|
||||||
|
Quantity float64 `gorm:"type:decimal(12,3);not null" json:"quantity"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Product *Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||||
|
ProductVariant *ProductVariant `gorm:"foreignKey:VariantID" json:"product_variant,omitempty"`
|
||||||
|
Ingredient *Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr *ProductRecipe) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if pr.ID == uuid.Nil {
|
||||||
|
pr.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ProductRecipe) TableName() string {
|
||||||
|
return "product_recipes"
|
||||||
|
}
|
||||||
227
internal/handler/product_recipe_handler.go
Normal file
227
internal/handler/product_recipe_handler.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/appcontext"
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"apskel-pos-be/internal/contract"
|
||||||
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/service"
|
||||||
|
"apskel-pos-be/internal/util"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipeHandler struct {
|
||||||
|
productRecipeService service.ProductRecipeService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductRecipeHandler(productRecipeService service.ProductRecipeService) *ProductRecipeHandler {
|
||||||
|
return &ProductRecipeHandler{
|
||||||
|
productRecipeService: productRecipeService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) Create(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var request models.CreateProductRecipeRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Create -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Create")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeResponse, err := h.productRecipeService.Create(ctx, contextInfo.OrganizationID, &request)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Create -> Failed to create product recipe")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Create")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(recipeResponse), "ProductRecipeHandler::Create")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) GetByID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByID -> Invalid recipe ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid recipe ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeResponse, err := h.productRecipeService.GetByID(ctx, id, contextInfo.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByID -> Failed to get product recipe")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.NotFoundErrorCode, constants.RequestEntity, "Product recipe not found")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(recipeResponse), "ProductRecipeHandler::GetByID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) GetByProductID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
productIDStr := c.Param("product_id")
|
||||||
|
productID, err := uuid.Parse(productIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByProductID -> Invalid product ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByProductID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if variant_id is provided
|
||||||
|
variantIDStr := c.Query("variant_id")
|
||||||
|
var variantID *uuid.UUID
|
||||||
|
if variantIDStr != "" {
|
||||||
|
parsed, err := uuid.Parse(variantIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByProductID -> Invalid variant ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid variant ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByProductID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
variantID = &parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
var recipes []*models.ProductRecipeResponse
|
||||||
|
if variantIDStr != "" {
|
||||||
|
// Get by product and variant ID
|
||||||
|
recipes, err = h.productRecipeService.GetByProductAndVariantID(ctx, productID, variantID, contextInfo.OrganizationID)
|
||||||
|
} else {
|
||||||
|
// Get by product ID only
|
||||||
|
recipes, err = h.productRecipeService.GetByProductID(ctx, productID, contextInfo.OrganizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByProductID -> Failed to get product recipes")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByProductID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(recipes), "ProductRecipeHandler::GetByProductID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) GetByIngredientID(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
ingredientIDStr := c.Param("ingredient_id")
|
||||||
|
ingredientID, err := uuid.Parse(ingredientIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByIngredientID -> Invalid ingredient ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByIngredientID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipes, err := h.productRecipeService.GetByIngredientID(ctx, ingredientID, contextInfo.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::GetByIngredientID -> Failed to get product recipes")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::GetByIngredientID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(recipes), "ProductRecipeHandler::GetByIngredientID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) Update(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Update -> Invalid recipe ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid recipe ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request models.UpdateProductRecipeRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Update -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeResponse, err := h.productRecipeService.Update(ctx, id, contextInfo.OrganizationID, &request)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Update -> Failed to update product recipe")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(recipeResponse), "ProductRecipeHandler::Update")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) Delete(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := uuid.Parse(idStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Delete -> Invalid recipe ID")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid recipe ID")
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.productRecipeService.Delete(ctx, id, contextInfo.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::Delete -> Failed to delete product recipe")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::Delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"message": "Product recipe deleted successfully",
|
||||||
|
}
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "ProductRecipeHandler::Delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductRecipeHandler) BulkCreate(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
|
var request struct {
|
||||||
|
Recipes []models.CreateProductRecipeRequest `json:"recipes" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::BulkCreate -> request binding failed")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::BulkCreate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipes, err := h.productRecipeService.BulkCreate(ctx, contextInfo.OrganizationID, request.Recipes)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(ctx).WithError(err).Error("ProductRecipeHandler::BulkCreate -> Failed to bulk create product recipes")
|
||||||
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductRecipeHandler::BulkCreate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(recipes))
|
||||||
|
}
|
||||||
@ -19,7 +19,6 @@ func NewReportHandler(reportService service.ReportService, userService UserServi
|
|||||||
return &ReportHandler{reportService: reportService, userService: userService}
|
return &ReportHandler{reportService: reportService, userService: userService}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/v1/outlets/:outlet_id/reports/daily-transaction.pdf?date=YYYY-MM-DD
|
|
||||||
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
ci := appcontext.FromGinContext(ctx)
|
ci := appcontext.FromGinContext(ctx)
|
||||||
@ -32,11 +31,9 @@ func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user name for "Dicetak Oleh"
|
|
||||||
user, err := h.userService.GetUserByID(ctx, ci.UserID)
|
user, err := h.userService.GetUserByID(ctx, ci.UserID)
|
||||||
var genBy string
|
var genBy string
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to user ID if name cannot be retrieved
|
|
||||||
genBy = ci.UserID.String()
|
genBy = ci.UserID.String()
|
||||||
} else {
|
} else {
|
||||||
genBy = user.Name
|
genBy = user.Name
|
||||||
|
|||||||
55
internal/models/product_recipe.go
Normal file
55
internal/models/product_recipe.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipe struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
VariantID *uuid.UUID `json:"variant_id"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Product *Product `json:"product,omitempty"`
|
||||||
|
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
||||||
|
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProductRecipeRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||||
|
VariantID *uuid.UUID `json:"variant_id"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductRecipeRequest struct {
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
VariantID *uuid.UUID `json:"variant_id"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductRecipeResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
|
OutletID *uuid.UUID `json:"outlet_id"`
|
||||||
|
ProductID uuid.UUID `json:"product_id"`
|
||||||
|
VariantID *uuid.UUID `json:"variant_id"`
|
||||||
|
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Product *Product `json:"product,omitempty"`
|
||||||
|
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
||||||
|
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||||
|
}
|
||||||
241
internal/processor/product_recipe_processor.go
Normal file
241
internal/processor/product_recipe_processor.go
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipeProcessor interface {
|
||||||
|
Create(ctx context.Context, req *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
Update(ctx context.Context, id uuid.UUID, req *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
BulkCreate(ctx context.Context, recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductRecipeProcessorImpl struct {
|
||||||
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
|
productRepo ProductRepository
|
||||||
|
ingredientRepo IngredientRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductRecipeProcessor(productRecipeRepo *repository.ProductRecipeRepository, productRepo ProductRepository, ingredientRepo IngredientRepository) *ProductRecipeProcessorImpl {
|
||||||
|
return &ProductRecipeProcessorImpl{
|
||||||
|
productRecipeRepo: productRecipeRepo,
|
||||||
|
productRepo: productRepo,
|
||||||
|
ingredientRepo: ingredientRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) Create(ctx context.Context, req *models.CreateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) {
|
||||||
|
_, err := p.productRepo.GetByID(ctx, req.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid product: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.ingredientRepo.GetByID(ctx, req.IngredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid ingredient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity := &entities.ProductRecipe{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OrganizationID: organizationID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
ProductID: req.ProductID,
|
||||||
|
VariantID: req.VariantID,
|
||||||
|
IngredientID: req.IngredientID,
|
||||||
|
Quantity: req.Quantity,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.productRecipeRepo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create product recipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdEntity, err := p.productRecipeRepo.GetByID(ctx, entity.ID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get created product recipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.entityToResponse(createdEntity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) {
|
||||||
|
entity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product recipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.entityToResponse(entity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
entities, err := p.productRecipeRepo.GetByProductID(ctx, productID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product recipes by product ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]*models.ProductRecipeResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
responses[i] = p.entityToResponse(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
entities, err := p.productRecipeRepo.GetByProductAndVariantID(ctx, productID, variantID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product recipes by product and variant ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]*models.ProductRecipeResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
responses[i] = p.entityToResponse(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
entities, err := p.productRecipeRepo.GetByIngredientID(ctx, ingredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product recipes by ingredient ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]*models.ProductRecipeResponse, len(entities))
|
||||||
|
for i, entity := range entities {
|
||||||
|
responses[i] = p.entityToResponse(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateProductRecipeRequest, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) {
|
||||||
|
// Get existing entity
|
||||||
|
existingEntity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("product recipe not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
existingEntity.OutletID = req.OutletID
|
||||||
|
existingEntity.VariantID = req.VariantID
|
||||||
|
existingEntity.Quantity = req.Quantity
|
||||||
|
existingEntity.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
|
if err := p.productRecipeRepo.Update(ctx, existingEntity); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update product recipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the updated entity with relations
|
||||||
|
updatedEntity, err := p.productRecipeRepo.GetByID(ctx, id, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get updated product recipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.entityToResponse(updatedEntity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error {
|
||||||
|
if err := p.productRecipeRepo.Delete(ctx, id, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete product recipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) BulkCreate(ctx context.Context, recipes []models.CreateProductRecipeRequest, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
responses := make([]*models.ProductRecipeResponse, 0, len(recipes))
|
||||||
|
|
||||||
|
for _, recipe := range recipes {
|
||||||
|
response, err := p.Create(ctx, &recipe, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create recipe for product %s and ingredient %s: %w", recipe.ProductID, recipe.IngredientID, err)
|
||||||
|
}
|
||||||
|
responses = append(responses, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProductRecipeProcessorImpl) entityToResponse(entity *entities.ProductRecipe) *models.ProductRecipeResponse {
|
||||||
|
response := &models.ProductRecipeResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
OrganizationID: entity.OrganizationID,
|
||||||
|
OutletID: entity.OutletID,
|
||||||
|
ProductID: entity.ProductID,
|
||||||
|
VariantID: entity.VariantID,
|
||||||
|
IngredientID: entity.IngredientID,
|
||||||
|
Quantity: entity.Quantity,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.Product != nil {
|
||||||
|
response.Product = &models.Product{
|
||||||
|
ID: entity.Product.ID,
|
||||||
|
OrganizationID: entity.Product.OrganizationID,
|
||||||
|
CategoryID: entity.Product.CategoryID,
|
||||||
|
SKU: entity.Product.SKU,
|
||||||
|
Name: entity.Product.Name,
|
||||||
|
Description: entity.Product.Description,
|
||||||
|
Price: entity.Product.Price,
|
||||||
|
Cost: entity.Product.Cost,
|
||||||
|
BusinessType: constants.BusinessType(entity.Product.BusinessType),
|
||||||
|
ImageURL: entity.Product.ImageURL,
|
||||||
|
PrinterType: entity.Product.PrinterType,
|
||||||
|
UnitID: entity.Product.UnitID,
|
||||||
|
HasIngredients: entity.Product.HasIngredients,
|
||||||
|
Metadata: entity.Product.Metadata,
|
||||||
|
IsActive: entity.Product.IsActive,
|
||||||
|
CreatedAt: entity.Product.CreatedAt,
|
||||||
|
UpdatedAt: entity.Product.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.ProductVariant != nil {
|
||||||
|
response.ProductVariant = &models.ProductVariant{
|
||||||
|
ID: entity.ProductVariant.ID,
|
||||||
|
ProductID: entity.ProductVariant.ProductID,
|
||||||
|
Name: entity.ProductVariant.Name,
|
||||||
|
PriceModifier: entity.ProductVariant.PriceModifier,
|
||||||
|
Cost: entity.ProductVariant.Cost,
|
||||||
|
Metadata: entity.ProductVariant.Metadata,
|
||||||
|
CreatedAt: entity.ProductVariant.CreatedAt,
|
||||||
|
UpdatedAt: entity.ProductVariant.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.Ingredient != nil {
|
||||||
|
response.Ingredient = &models.Ingredient{
|
||||||
|
ID: entity.Ingredient.ID,
|
||||||
|
OrganizationID: entity.Ingredient.OrganizationID,
|
||||||
|
OutletID: entity.Ingredient.OutletID,
|
||||||
|
Name: entity.Ingredient.Name,
|
||||||
|
UnitID: entity.Ingredient.UnitID,
|
||||||
|
Cost: entity.Ingredient.Cost,
|
||||||
|
Stock: entity.Ingredient.Stock,
|
||||||
|
IsSemiFinished: entity.Ingredient.IsSemiFinished,
|
||||||
|
IsActive: entity.Ingredient.IsActive,
|
||||||
|
Metadata: entity.Ingredient.Metadata,
|
||||||
|
CreatedAt: entity.Ingredient.CreatedAt,
|
||||||
|
UpdatedAt: entity.Ingredient.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
127
internal/repository/product_recipe_repository.go
Normal file
127
internal/repository/product_recipe_repository.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/entities"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipeRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductRecipeRepository(db *gorm.DB) *ProductRecipeRepository {
|
||||||
|
return &ProductRecipeRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) Create(ctx context.Context, productRecipe *entities.ProductRecipe) error {
|
||||||
|
return r.db.WithContext(ctx).Create(productRecipe).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductRecipe, error) {
|
||||||
|
var productRecipe entities.ProductRecipe
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("ProductVariant").
|
||||||
|
Preload("Ingredient").
|
||||||
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
|
First(&productRecipe).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &productRecipe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductRecipe, error) {
|
||||||
|
var productRecipes []*entities.ProductRecipe
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("ProductVariant").
|
||||||
|
Preload("Ingredient").
|
||||||
|
Where("product_id = ? AND organization_id = ?", productID, organizationID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&productRecipes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return productRecipes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*entities.ProductRecipe, error) {
|
||||||
|
var productRecipes []*entities.ProductRecipe
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("ProductVariant").
|
||||||
|
Preload("Ingredient").
|
||||||
|
Where("product_id = ? AND organization_id = ?", productID, organizationID)
|
||||||
|
|
||||||
|
if variantID != nil {
|
||||||
|
query = query.Where("variant_id = ?", *variantID)
|
||||||
|
} else {
|
||||||
|
query = query.Where("variant_id IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := query.Order("created_at DESC").Find(&productRecipes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return productRecipes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductRecipe, error) {
|
||||||
|
var productRecipes []*entities.ProductRecipe
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("ProductVariant").
|
||||||
|
Preload("Ingredient").
|
||||||
|
Where("ingredient_id = ? AND organization_id = ?", ingredientID, organizationID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&productRecipes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return productRecipes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) Update(ctx context.Context, productRecipe *entities.ProductRecipe) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("id = ? AND organization_id = ?", productRecipe.ID, productRecipe.OrganizationID).
|
||||||
|
Updates(productRecipe).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||||
|
result := r.db.WithContext(ctx).
|
||||||
|
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||||
|
Delete(&entities.ProductRecipe{})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) DeleteByProductID(ctx context.Context, productID, organizationID uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Where("product_id = ? AND organization_id = ?", productID, organizationID).
|
||||||
|
Delete(&entities.ProductRecipe{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRecipeRepository) DeleteByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) error {
|
||||||
|
query := r.db.WithContext(ctx).Where("product_id = ? AND organization_id = ?", productID, organizationID)
|
||||||
|
|
||||||
|
if variantID != nil {
|
||||||
|
query = query.Where("variant_id = ?", *variantID)
|
||||||
|
} else {
|
||||||
|
query = query.Where("variant_id IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.Delete(&entities.ProductRecipe{}).Error
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ type Router struct {
|
|||||||
tableHandler *handler.TableHandler
|
tableHandler *handler.TableHandler
|
||||||
unitHandler *handler.UnitHandler
|
unitHandler *handler.UnitHandler
|
||||||
ingredientHandler *handler.IngredientHandler
|
ingredientHandler *handler.IngredientHandler
|
||||||
|
productRecipeHandler *handler.ProductRecipeHandler
|
||||||
authMiddleware *middleware.AuthMiddleware
|
authMiddleware *middleware.AuthMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +68,8 @@ func NewRouter(cfg *config.Config,
|
|||||||
tableService *service.TableServiceImpl,
|
tableService *service.TableServiceImpl,
|
||||||
tableValidator *validator.TableValidator,
|
tableValidator *validator.TableValidator,
|
||||||
unitService handler.UnitService,
|
unitService handler.UnitService,
|
||||||
ingredientService handler.IngredientService) *Router {
|
ingredientService handler.IngredientService,
|
||||||
|
productRecipeService service.ProductRecipeService) *Router {
|
||||||
|
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -89,6 +91,7 @@ func NewRouter(cfg *config.Config,
|
|||||||
tableHandler: handler.NewTableHandler(tableService, tableValidator),
|
tableHandler: handler.NewTableHandler(tableService, tableValidator),
|
||||||
unitHandler: handler.NewUnitHandler(unitService),
|
unitHandler: handler.NewUnitHandler(unitService),
|
||||||
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
||||||
|
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,6 +292,18 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
ingredients.DELETE("/:id", r.ingredientHandler.Delete)
|
ingredients.DELETE("/:id", r.ingredientHandler.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
productRecipes := protected.Group("/product-recipes")
|
||||||
|
productRecipes.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
|
{
|
||||||
|
productRecipes.POST("", r.productRecipeHandler.Create)
|
||||||
|
productRecipes.POST("/bulk", r.productRecipeHandler.BulkCreate)
|
||||||
|
productRecipes.GET("/:id", r.productRecipeHandler.GetByID)
|
||||||
|
productRecipes.PUT("/:id", r.productRecipeHandler.Update)
|
||||||
|
productRecipes.DELETE("/:id", r.productRecipeHandler.Delete)
|
||||||
|
productRecipes.GET("/product/:product_id", r.productRecipeHandler.GetByProductID)
|
||||||
|
productRecipes.GET("/ingredient/:ingredient_id", r.productRecipeHandler.GetByIngredientID)
|
||||||
|
}
|
||||||
|
|
||||||
outlets := protected.Group("/outlets")
|
outlets := protected.Group("/outlets")
|
||||||
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
||||||
{
|
{
|
||||||
|
|||||||
62
internal/service/product_recipe_service.go
Normal file
62
internal/service/product_recipe_service.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/processor"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRecipeService interface {
|
||||||
|
Create(ctx context.Context, organizationID uuid.UUID, req *models.CreateProductRecipeRequest) (*models.ProductRecipeResponse, error)
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error)
|
||||||
|
GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error)
|
||||||
|
Update(ctx context.Context, id uuid.UUID, organizationID uuid.UUID, req *models.UpdateProductRecipeRequest) (*models.ProductRecipeResponse, error)
|
||||||
|
Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error
|
||||||
|
BulkCreate(ctx context.Context, organizationID uuid.UUID, recipes []models.CreateProductRecipeRequest) ([]*models.ProductRecipeResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductRecipeServiceImpl struct {
|
||||||
|
processor processor.ProductRecipeProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductRecipeService(processor processor.ProductRecipeProcessor) *ProductRecipeServiceImpl {
|
||||||
|
return &ProductRecipeServiceImpl{
|
||||||
|
processor: processor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) Create(ctx context.Context, organizationID uuid.UUID, req *models.CreateProductRecipeRequest) (*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.Create(ctx, req, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) GetByID(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) (*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.GetByID(ctx, id, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) GetByProductID(ctx context.Context, productID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.GetByProductID(ctx, productID, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) GetByProductAndVariantID(ctx context.Context, productID uuid.UUID, variantID *uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.GetByProductAndVariantID(ctx, productID, variantID, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) GetByIngredientID(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.GetByIngredientID(ctx, ingredientID, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) Update(ctx context.Context, id uuid.UUID, organizationID uuid.UUID, req *models.UpdateProductRecipeRequest) (*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.Update(ctx, id, req, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error {
|
||||||
|
return s.processor.Delete(ctx, id, organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductRecipeServiceImpl) BulkCreate(ctx context.Context, organizationID uuid.UUID, recipes []models.CreateProductRecipeRequest) ([]*models.ProductRecipeResponse, error) {
|
||||||
|
return s.processor.BulkCreate(ctx, recipes, organizationID)
|
||||||
|
}
|
||||||
@ -81,27 +81,26 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
return "", "", fmt.Errorf("invalid outlet id: %w", err)
|
return "", "", fmt.Errorf("invalid outlet id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve organization and outlet names
|
|
||||||
org, err := s.organizationRepo.GetByID(ctx, orgID)
|
org, err := s.organizationRepo.GetByID(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("organization not found: %w", err)
|
return "", "", fmt.Errorf("organization not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outlet, err := s.outletRepo.GetByID(ctx, outID)
|
outlet, err := s.outletRepo.GetByID(ctx, outID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("outlet not found: %w", err)
|
return "", "", fmt.Errorf("outlet not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine timezone (fallback to system local if not available)
|
|
||||||
tzName := "Asia/Jakarta"
|
tzName := "Asia/Jakarta"
|
||||||
if outlet.Timezone != nil && *outlet.Timezone != "" {
|
if outlet.Timezone != nil && *outlet.Timezone != "" {
|
||||||
tzName = *outlet.Timezone
|
tzName = *outlet.Timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
loc, locErr := time.LoadLocation(tzName)
|
loc, locErr := time.LoadLocation(tzName)
|
||||||
if locErr != nil || loc == nil {
|
if locErr != nil || loc == nil {
|
||||||
loc = time.Local
|
loc = time.Local
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute day range in the chosen location
|
|
||||||
var day time.Time
|
var day time.Time
|
||||||
if reportDate != nil {
|
if reportDate != nil {
|
||||||
t := reportDate.UTC()
|
t := reportDate.UTC()
|
||||||
@ -113,11 +112,9 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
start := day
|
start := day
|
||||||
end := day.Add(24*time.Hour - time.Nanosecond)
|
end := day.Add(24*time.Hour - time.Nanosecond)
|
||||||
|
|
||||||
// Build requests
|
|
||||||
salesReq := &models.SalesAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
salesReq := &models.SalesAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
||||||
plReq := &models.ProfitLossAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
plReq := &models.ProfitLossAnalyticsRequest{OrganizationID: orgID, OutletID: &outID, DateFrom: start, DateTo: end, GroupBy: "day"}
|
||||||
|
|
||||||
// Call services
|
|
||||||
sales, err := s.analyticsService.GetSalesAnalytics(ctx, salesReq)
|
sales, err := s.analyticsService.GetSalesAnalytics(ctx, salesReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", fmt.Errorf("get sales analytics: %w", err)
|
return "", "", fmt.Errorf("get sales analytics: %w", err)
|
||||||
@ -127,7 +124,6 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
return "", "", fmt.Errorf("get profit/loss analytics: %w", err)
|
return "", "", fmt.Errorf("get profit/loss analytics: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose template data
|
|
||||||
data := reportTemplateData{
|
data := reportTemplateData{
|
||||||
OrganizationName: org.Name,
|
OrganizationName: org.Name,
|
||||||
OutletName: outlet.Name,
|
OutletName: outlet.Name,
|
||||||
@ -149,7 +145,6 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Items by product
|
|
||||||
items := make([]reportItem, 0, len(pl.ProductData))
|
items := make([]reportItem, 0, len(pl.ProductData))
|
||||||
for _, p := range pl.ProductData {
|
for _, p := range pl.ProductData {
|
||||||
items = append(items, reportItem{
|
items = append(items, reportItem{
|
||||||
@ -164,7 +159,6 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
|||||||
}
|
}
|
||||||
data.Items = items
|
data.Items = items
|
||||||
|
|
||||||
// Render to PDF
|
|
||||||
templatePath := filepath.Join("templates", "daily_transaction.html")
|
templatePath := filepath.Join("templates", "daily_transaction.html")
|
||||||
pdfBytes, err := renderTemplateToPDF(templatePath, data)
|
pdfBytes, err := renderTemplateToPDF(templatePath, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
2
migrations/000038_create_product_recipes_table.down.sql
Normal file
2
migrations/000038_create_product_recipes_table.down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Drop the product_recipes table
|
||||||
|
DROP TABLE IF EXISTS product_recipes;
|
||||||
24
migrations/000038_create_product_recipes_table.up.sql
Normal file
24
migrations/000038_create_product_recipes_table.up.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-- Product recipes table
|
||||||
|
CREATE TABLE product_recipes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||||
|
outlet_id UUID REFERENCES outlets(id) ON DELETE CASCADE,
|
||||||
|
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
variant_id UUID REFERENCES product_variants(id) ON DELETE CASCADE,
|
||||||
|
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||||
|
quantity DECIMAL(12,3) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX idx_product_recipes_organization_id ON product_recipes(organization_id);
|
||||||
|
CREATE INDEX idx_product_recipes_outlet_id ON product_recipes(outlet_id);
|
||||||
|
CREATE INDEX idx_product_recipes_product_id ON product_recipes(product_id);
|
||||||
|
CREATE INDEX idx_product_recipes_variant_id ON product_recipes(variant_id);
|
||||||
|
CREATE INDEX idx_product_recipes_ingredient_id ON product_recipes(ingredient_id);
|
||||||
|
CREATE INDEX idx_product_recipes_created_at ON product_recipes(created_at);
|
||||||
|
|
||||||
|
-- Unique constraint to prevent duplicate recipe combinations
|
||||||
|
-- This allows multiple recipes for same product with different variants, or same product with no variant
|
||||||
|
CREATE UNIQUE INDEX idx_product_recipes_unique ON product_recipes(product_id, COALESCE(variant_id, '00000000-0000-0000-0000-000000000000'::UUID), ingredient_id);
|
||||||
469
product-recipe-postman-collection.json
Normal file
469
product-recipe-postman-collection.json
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "product-recipe-collection",
|
||||||
|
"name": "Product Recipe API",
|
||||||
|
"description": "Complete CRUD operations for Product Recipe management",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "12345678"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Authentication",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Login",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"email\": \"admin@example.com\",\n \"password\": \"password123\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/auth/login",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"auth",
|
||||||
|
"login"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Product Recipe Management",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Create Product Recipe",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"product_id\": \"{{product_id}}\",\n \"variant_id\": \"{{variant_id}}\",\n \"ingredient_id\": \"{{ingredient_id}}\",\n \"quantity\": 2.5,\n \"outlet_id\": \"{{outlet_id}}\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create Product Recipe (No Variant)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"product_id\": \"{{product_id}}\",\n \"ingredient_id\": \"{{ingredient_id}}\",\n \"quantity\": 1.0\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bulk Create Product Recipes",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"recipes\": [\n {\n \"product_id\": \"{{product_id}}\",\n \"variant_id\": \"{{variant_id}}\",\n \"ingredient_id\": \"{{ingredient_id_1}}\",\n \"quantity\": 2.0\n },\n {\n \"product_id\": \"{{product_id}}\",\n \"variant_id\": \"{{variant_id}}\",\n \"ingredient_id\": \"{{ingredient_id_2}}\",\n \"quantity\": 1.5\n },\n {\n \"product_id\": \"{{product_id}}\",\n \"ingredient_id\": \"{{ingredient_id_3}}\",\n \"quantity\": 0.5\n }\n ]\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/bulk",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"bulk"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Product Recipe by ID",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/{{recipe_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"{{recipe_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Product Recipes by Product ID",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/product/{{product_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"product",
|
||||||
|
"{{product_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Product Recipes by Product ID and Variant ID",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/product/{{product_id}}?variant_id={{variant_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"product",
|
||||||
|
"{{product_id}}"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "variant_id",
|
||||||
|
"value": "{{variant_id}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Product Recipes by Ingredient ID",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/ingredient/{{ingredient_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"ingredient",
|
||||||
|
"{{ingredient_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Product Recipe",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"quantity\": 3.0,\n \"variant_id\": \"{{new_variant_id}}\",\n \"outlet_id\": \"{{outlet_id}}\"\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/{{recipe_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"{{recipe_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Product Recipe",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/product-recipes/{{recipe_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"product-recipes",
|
||||||
|
"{{recipe_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Supporting Data",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Get Products",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/products",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"products"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Ingredients",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/ingredients",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"ingredients"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Product by ID (with variants)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{access_token}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/products/{{product_id}}",
|
||||||
|
"host": [
|
||||||
|
"{{base_url}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"products",
|
||||||
|
"{{product_id}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "base_url",
|
||||||
|
"value": "http://localhost:8080",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "access_token",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "product_id",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "variant_id",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ingredient_id",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ingredient_id_1",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ingredient_id_2",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ingredient_id_3",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "recipe_id",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "outlet_id",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "new_variant_id",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user