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,
|
||||
services.unitService,
|
||||
services.ingredientService,
|
||||
services.productRecipeService,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -144,6 +145,7 @@ type repositories struct {
|
||||
tableRepo repository.TableRepositoryInterface
|
||||
unitRepo *repository.UnitRepository
|
||||
ingredientRepo *repository.IngredientRepository
|
||||
productRecipeRepo *repository.ProductRecipeRepository
|
||||
}
|
||||
|
||||
func (a *App) initRepositories() *repositories {
|
||||
@ -168,6 +170,7 @@ func (a *App) initRepositories() *repositories {
|
||||
tableRepo: repository.NewTableRepository(a.db),
|
||||
unitRepo: repository.NewUnitRepository(a.db),
|
||||
ingredientRepo: repository.NewIngredientRepository(a.db),
|
||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,6 +191,7 @@ type processors struct {
|
||||
tableProcessor *processor.TableProcessor
|
||||
unitProcessor *processor.UnitProcessorImpl
|
||||
ingredientProcessor *processor.IngredientProcessorImpl
|
||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||
fileClient processor.FileClient
|
||||
}
|
||||
|
||||
@ -211,6 +215,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
|
||||
unitProcessor: processor.NewUnitProcessor(repos.unitRepo),
|
||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
|
||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||
fileClient: fileClient,
|
||||
}
|
||||
}
|
||||
@ -234,6 +239,7 @@ type services struct {
|
||||
tableService *service.TableServiceImpl
|
||||
unitService *service.UnitServiceImpl
|
||||
ingredientService *service.IngredientServiceImpl
|
||||
productRecipeService *service.ProductRecipeServiceImpl
|
||||
}
|
||||
|
||||
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())
|
||||
unitService := service.NewUnitService(processors.unitProcessor)
|
||||
ingredientService := service.NewIngredientService(processors.ingredientProcessor)
|
||||
productRecipeService := service.NewProductRecipeService(processors.productRecipeProcessor)
|
||||
|
||||
return &services{
|
||||
userService: service.NewUserService(processors.userProcessor),
|
||||
@ -276,6 +283,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
tableService: tableService,
|
||||
unitService: unitService,
|
||||
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}
|
||||
}
|
||||
|
||||
// GET /api/v1/outlets/:outlet_id/reports/daily-transaction.pdf?date=YYYY-MM-DD
|
||||
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
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)
|
||||
var genBy string
|
||||
if err != nil {
|
||||
// Fallback to user ID if name cannot be retrieved
|
||||
genBy = ci.UserID.String()
|
||||
} else {
|
||||
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
|
||||
unitHandler *handler.UnitHandler
|
||||
ingredientHandler *handler.IngredientHandler
|
||||
productRecipeHandler *handler.ProductRecipeHandler
|
||||
authMiddleware *middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
@ -67,7 +68,8 @@ func NewRouter(cfg *config.Config,
|
||||
tableService *service.TableServiceImpl,
|
||||
tableValidator *validator.TableValidator,
|
||||
unitService handler.UnitService,
|
||||
ingredientService handler.IngredientService) *Router {
|
||||
ingredientService handler.IngredientService,
|
||||
productRecipeService service.ProductRecipeService) *Router {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -89,6 +91,7 @@ func NewRouter(cfg *config.Config,
|
||||
tableHandler: handler.NewTableHandler(tableService, tableValidator),
|
||||
unitHandler: handler.NewUnitHandler(unitService),
|
||||
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
||||
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
||||
authMiddleware: authMiddleware,
|
||||
}
|
||||
}
|
||||
@ -289,6 +292,18 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
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.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)
|
||||
}
|
||||
|
||||
// Resolve organization and outlet names
|
||||
org, err := s.organizationRepo.GetByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
|
||||
outlet, err := s.outletRepo.GetByID(ctx, outID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Determine timezone (fallback to system local if not available)
|
||||
tzName := "Asia/Jakarta"
|
||||
if outlet.Timezone != nil && *outlet.Timezone != "" {
|
||||
tzName = *outlet.Timezone
|
||||
}
|
||||
|
||||
loc, locErr := time.LoadLocation(tzName)
|
||||
if locErr != nil || loc == nil {
|
||||
loc = time.Local
|
||||
}
|
||||
|
||||
// Compute day range in the chosen location
|
||||
var day time.Time
|
||||
if reportDate != nil {
|
||||
t := reportDate.UTC()
|
||||
@ -113,11 +112,9 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
||||
start := day
|
||||
end := day.Add(24*time.Hour - time.Nanosecond)
|
||||
|
||||
// Build requests
|
||||
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"}
|
||||
|
||||
// Call services
|
||||
sales, err := s.analyticsService.GetSalesAnalytics(ctx, salesReq)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// Compose template data
|
||||
data := reportTemplateData{
|
||||
OrganizationName: org.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))
|
||||
for _, p := range pl.ProductData {
|
||||
items = append(items, reportItem{
|
||||
@ -164,7 +159,6 @@ func (s *ReportServiceImpl) GenerateDailyTransactionPDF(ctx context.Context, org
|
||||
}
|
||||
data.Items = items
|
||||
|
||||
// Render to PDF
|
||||
templatePath := filepath.Join("templates", "daily_transaction.html")
|
||||
pdfBytes, err := renderTemplateToPDF(templatePath, data)
|
||||
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