add product ingredients
This commit is contained in:
parent
efe09c21e4
commit
3a04990ec8
@ -92,6 +92,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.chartOfAccountValidator,
|
||||
services.accountService,
|
||||
validators.accountValidator,
|
||||
*services.orderIngredientTransactionService,
|
||||
validators.orderIngredientTransactionValidator,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -164,6 +166,8 @@ type repositories struct {
|
||||
chartOfAccountTypeRepo *repository.ChartOfAccountTypeRepositoryImpl
|
||||
chartOfAccountRepo *repository.ChartOfAccountRepositoryImpl
|
||||
accountRepo *repository.AccountRepositoryImpl
|
||||
orderIngredientTransactionRepo *repository.OrderIngredientTransactionRepositoryImpl
|
||||
productIngredientRepo *repository.ProductIngredientRepository
|
||||
txManager *repository.TxManager
|
||||
}
|
||||
|
||||
@ -196,6 +200,11 @@ func (a *App) initRepositories() *repositories {
|
||||
chartOfAccountTypeRepo: repository.NewChartOfAccountTypeRepositoryImpl(a.db),
|
||||
chartOfAccountRepo: repository.NewChartOfAccountRepositoryImpl(a.db),
|
||||
accountRepo: repository.NewAccountRepositoryImpl(a.db),
|
||||
orderIngredientTransactionRepo: repository.NewOrderIngredientTransactionRepositoryImpl(a.db).(*repository.OrderIngredientTransactionRepositoryImpl),
|
||||
productIngredientRepo: func() *repository.ProductIngredientRepository {
|
||||
db, _ := a.db.DB()
|
||||
return repository.NewProductIngredientRepository(db)
|
||||
}(),
|
||||
txManager: repository.NewTxManager(a.db),
|
||||
}
|
||||
}
|
||||
@ -224,6 +233,7 @@ type processors struct {
|
||||
chartOfAccountTypeProcessor *processor.ChartOfAccountTypeProcessorImpl
|
||||
chartOfAccountProcessor *processor.ChartOfAccountProcessorImpl
|
||||
accountProcessor *processor.AccountProcessorImpl
|
||||
orderIngredientTransactionProcessor *processor.OrderIngredientTransactionProcessorImpl
|
||||
fileClient processor.FileClient
|
||||
inventoryMovementService service.InventoryMovementService
|
||||
}
|
||||
@ -256,6 +266,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
chartOfAccountTypeProcessor: processor.NewChartOfAccountTypeProcessorImpl(repos.chartOfAccountTypeRepo),
|
||||
chartOfAccountProcessor: processor.NewChartOfAccountProcessorImpl(repos.chartOfAccountRepo, repos.chartOfAccountTypeRepo),
|
||||
accountProcessor: processor.NewAccountProcessorImpl(repos.accountRepo, repos.chartOfAccountRepo),
|
||||
orderIngredientTransactionProcessor: processor.NewOrderIngredientTransactionProcessorImpl(repos.orderIngredientTransactionRepo, repos.productIngredientRepo, repos.ingredientRepo, repos.unitRepo).(*processor.OrderIngredientTransactionProcessorImpl),
|
||||
fileClient: fileClient,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
}
|
||||
@ -287,6 +298,7 @@ type services struct {
|
||||
chartOfAccountTypeService service.ChartOfAccountTypeService
|
||||
chartOfAccountService service.ChartOfAccountService
|
||||
accountService service.AccountService
|
||||
orderIngredientTransactionService *service.OrderIngredientTransactionService
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||
@ -300,7 +312,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productService := service.NewProductService(processors.productProcessor)
|
||||
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
||||
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo)
|
||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productIngredientRepo, repos.txManager) // Will be updated after orderIngredientTransactionService is created
|
||||
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||
@ -316,6 +328,10 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
chartOfAccountTypeService := service.NewChartOfAccountTypeService(processors.chartOfAccountTypeProcessor)
|
||||
chartOfAccountService := service.NewChartOfAccountService(processors.chartOfAccountProcessor)
|
||||
accountService := service.NewAccountService(processors.accountProcessor)
|
||||
orderIngredientTransactionService := service.NewOrderIngredientTransactionService(processors.orderIngredientTransactionProcessor, repos.txManager)
|
||||
|
||||
// Update order service with order ingredient transaction service
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productIngredientRepo, repos.txManager)
|
||||
|
||||
return &services{
|
||||
userService: service.NewUserService(processors.userProcessor),
|
||||
@ -343,6 +359,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
chartOfAccountTypeService: chartOfAccountTypeService,
|
||||
chartOfAccountService: chartOfAccountService,
|
||||
accountService: accountService,
|
||||
orderIngredientTransactionService: orderIngredientTransactionService,
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,6 +392,7 @@ type validators struct {
|
||||
chartOfAccountTypeValidator *validator.ChartOfAccountTypeValidatorImpl
|
||||
chartOfAccountValidator *validator.ChartOfAccountValidatorImpl
|
||||
accountValidator *validator.AccountValidatorImpl
|
||||
orderIngredientTransactionValidator *validator.OrderIngredientTransactionValidatorImpl
|
||||
}
|
||||
|
||||
func (a *App) initValidators() *validators {
|
||||
@ -397,5 +415,6 @@ func (a *App) initValidators() *validators {
|
||||
chartOfAccountTypeValidator: validator.NewChartOfAccountTypeValidator().(*validator.ChartOfAccountTypeValidatorImpl),
|
||||
chartOfAccountValidator: validator.NewChartOfAccountValidator().(*validator.ChartOfAccountValidatorImpl),
|
||||
accountValidator: validator.NewAccountValidator().(*validator.AccountValidatorImpl),
|
||||
orderIngredientTransactionValidator: validator.NewOrderIngredientTransactionValidator().(*validator.OrderIngredientTransactionValidatorImpl),
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,3 +74,11 @@ type ListIngredientUnitConvertersResponse struct {
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type IngredientUnitsResponse struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
BaseUnitID uuid.UUID `json:"base_unit_id"`
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
20
internal/contract/order_ingredient_transaction_contract.go
Normal file
20
internal/contract/order_ingredient_transaction_contract.go
Normal file
@ -0,0 +1,20 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionContract interface {
|
||||
CreateOrderIngredientTransaction(ctx context.Context, req *CreateOrderIngredientTransactionRequest) (*OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionByID(ctx context.Context, id uuid.UUID) (*OrderIngredientTransactionResponse, error)
|
||||
UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *UpdateOrderIngredientTransactionRequest) (*OrderIngredientTransactionResponse, error)
|
||||
DeleteOrderIngredientTransaction(ctx context.Context, id uuid.UUID) error
|
||||
ListOrderIngredientTransactions(ctx context.Context, req *ListOrderIngredientTransactionsRequest) ([]*OrderIngredientTransactionResponse, int64, error)
|
||||
GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID uuid.UUID) ([]*OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID uuid.UUID) ([]*OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID uuid.UUID) ([]*OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionSummary(ctx context.Context, req *ListOrderIngredientTransactionsRequest) ([]*OrderIngredientTransactionSummary, error)
|
||||
BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*CreateOrderIngredientTransactionRequest) ([]*OrderIngredientTransactionResponse, error)
|
||||
}
|
||||
79
internal/contract/order_ingredient_transaction_request.go
Normal file
79
internal/contract/order_ingredient_transaction_request.go
Normal file
@ -0,0 +1,79 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateOrderIngredientTransactionRequest struct {
|
||||
OrderID uuid.UUID `json:"order_id" validate:"required"`
|
||||
OrderItemID *uuid.UUID `json:"order_item_id,omitempty"`
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
GrossQty float64 `json:"gross_qty" validate:"required,gt=0"`
|
||||
NetQty float64 `json:"net_qty" validate:"required,gt=0"`
|
||||
WasteQty float64 `json:"waste_qty" validate:"min=0"`
|
||||
Unit string `json:"unit" validate:"required,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateOrderIngredientTransactionRequest struct {
|
||||
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
||||
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
||||
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
OrderID uuid.UUID `json:"order_id"`
|
||||
OrderItemID *uuid.UUID `json:"order_item_id"`
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
GrossQty float64 `json:"gross_qty"`
|
||||
NetQty float64 `json:"net_qty"`
|
||||
WasteQty float64 `json:"waste_qty"`
|
||||
Unit string `json:"unit"`
|
||||
TransactionDate time.Time `json:"transaction_date"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Relations - these would be populated by the service layer
|
||||
Organization interface{} `json:"organization,omitempty"`
|
||||
Outlet interface{} `json:"outlet,omitempty"`
|
||||
Order interface{} `json:"order,omitempty"`
|
||||
OrderItem interface{} `json:"order_item,omitempty"`
|
||||
Product interface{} `json:"product,omitempty"`
|
||||
ProductVariant interface{} `json:"product_variant,omitempty"`
|
||||
Ingredient interface{} `json:"ingredient,omitempty"`
|
||||
CreatedByUser interface{} `json:"created_by_user,omitempty"`
|
||||
}
|
||||
|
||||
type ListOrderIngredientTransactionsRequest struct {
|
||||
OrderID *uuid.UUID `json:"order_id,omitempty"`
|
||||
OrderItemID *uuid.UUID `json:"order_item_id,omitempty"`
|
||||
ProductID *uuid.UUID `json:"product_id,omitempty"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
StartDate *time.Time `json:"start_date,omitempty"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
Page int `json:"page" validate:"min=1"`
|
||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionSummary struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||
TotalNetQty float64 `json:"total_net_qty"`
|
||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
48
internal/entities/order_ingredient_transaction.go
Normal file
48
internal/entities/order_ingredient_transaction.go
Normal file
@ -0,0 +1,48 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderIngredientTransaction 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" validate:"required"`
|
||||
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
||||
OrderID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_id" validate:"required"`
|
||||
OrderItemID *uuid.UUID `gorm:"type:uuid;index" json:"order_item_id"`
|
||||
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"`
|
||||
ProductVariantID *uuid.UUID `gorm:"type:uuid;index" json:"product_variant_id"`
|
||||
IngredientID uuid.UUID `gorm:"type:uuid;not null;index" json:"ingredient_id" validate:"required"`
|
||||
GrossQty float64 `gorm:"type:decimal(12,3);not null" json:"gross_qty" validate:"required,gt=0"`
|
||||
NetQty float64 `gorm:"type:decimal(12,3);not null" json:"net_qty" validate:"required,gt=0"`
|
||||
WasteQty float64 `gorm:"type:decimal(12,3);not null" json:"waste_qty" validate:"min=0"`
|
||||
Unit string `gorm:"size:50;not null" json:"unit" validate:"required,max=50"`
|
||||
TransactionDate time.Time `gorm:"not null;index" json:"transaction_date"`
|
||||
CreatedBy uuid.UUID `gorm:"type:uuid;not null;index" json:"created_by" validate:"required"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||
OrderItem *OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"`
|
||||
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
||||
Ingredient Ingredient `gorm:"foreignKey:IngredientID" json:"ingredient,omitempty"`
|
||||
CreatedByUser User `gorm:"foreignKey:CreatedBy" json:"created_by_user,omitempty"`
|
||||
}
|
||||
|
||||
func (oit *OrderIngredientTransaction) BeforeCreate(tx *gorm.DB) error {
|
||||
if oit.ID == uuid.Nil {
|
||||
oit.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (OrderIngredientTransaction) TableName() string {
|
||||
return "order_ingredients_transactions"
|
||||
}
|
||||
@ -13,6 +13,7 @@ type ProductIngredient struct {
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" db:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity" db:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage" db:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
|
||||
@ -254,3 +254,25 @@ func (h *IngredientUnitConverterHandler) ConvertUnit(c *gin.Context) {
|
||||
util.HandleResponse(c.Writer, c.Request, converterResponse, "IngredientUnitConverterHandler::ConvertUnit")
|
||||
}
|
||||
|
||||
func (h *IngredientUnitConverterHandler) GetUnitsByIngredientID(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("IngredientUnitConverterHandler::GetUnitsByIngredientID -> Invalid ingredient ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid ingredient ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||
return
|
||||
}
|
||||
|
||||
unitsResponse := h.converterService.GetUnitsByIngredientID(ctx, contextInfo, ingredientID)
|
||||
if unitsResponse.HasErrors() {
|
||||
errorResp := unitsResponse.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("IngredientUnitConverterHandler::GetUnitsByIngredientID -> Failed to get units for ingredient from service")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, unitsResponse, "IngredientUnitConverterHandler::GetUnitsByIngredientID")
|
||||
}
|
||||
|
||||
|
||||
201
internal/handler/order_ingredient_transaction_handler.go
Normal file
201
internal/handler/order_ingredient_transaction_handler.go
Normal file
@ -0,0 +1,201 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/util"
|
||||
"apskel-pos-be/internal/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionHandler struct {
|
||||
service OrderIngredientTransactionService
|
||||
validator validator.OrderIngredientTransactionValidator
|
||||
}
|
||||
|
||||
func NewOrderIngredientTransactionHandler(service OrderIngredientTransactionService, validator validator.OrderIngredientTransactionValidator) *OrderIngredientTransactionHandler {
|
||||
return &OrderIngredientTransactionHandler{
|
||||
service: service,
|
||||
validator: validator,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) CreateOrderIngredientTransaction(c *gin.Context) {
|
||||
var req contract.CreateOrderIngredientTransactionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.CreateOrderIngredientTransaction(c, &req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionByID(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ID format"}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetOrderIngredientTransactionByID(c, id)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) UpdateOrderIngredientTransaction(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ID format"}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdateOrderIngredientTransactionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.UpdateOrderIngredientTransaction(c, id, &req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) DeleteOrderIngredientTransaction(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ID format"}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.DeleteOrderIngredientTransaction(c, id)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Order ingredient transaction deleted successfully"}), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) ListOrderIngredientTransactions(c *gin.Context) {
|
||||
var req contract.ListOrderIngredientTransactionsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, total, err := h.service.ListOrderIngredientTransactions(c, &req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{
|
||||
"data": response,
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
"limit": req.Limit,
|
||||
}), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionsByOrder(c *gin.Context) {
|
||||
orderIDStr := c.Param("order_id")
|
||||
orderID, err := uuid.Parse(orderIDStr)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid order ID format"}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetOrderIngredientTransactionsByOrder(c, orderID)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionsByOrderItem(c *gin.Context) {
|
||||
orderItemIDStr := c.Param("order_item_id")
|
||||
orderItemID, err := uuid.Parse(orderItemIDStr)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid order item ID format"}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetOrderIngredientTransactionsByOrderItem(c, orderItemID)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionsByIngredient(c *gin.Context) {
|
||||
ingredientIDStr := c.Param("ingredient_id")
|
||||
ingredientID, err := uuid.Parse(ingredientIDStr)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: "Invalid ingredient ID format"}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetOrderIngredientTransactionsByIngredient(c, ingredientID)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) GetOrderIngredientTransactionSummary(c *gin.Context) {
|
||||
var req contract.ListOrderIngredientTransactionsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.GetOrderIngredientTransactionSummary(c, &req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
|
||||
func (h *OrderIngredientTransactionHandler) BulkCreateOrderIngredientTransactions(c *gin.Context) {
|
||||
var req struct {
|
||||
Transactions []*contract.CreateOrderIngredientTransactionRequest `json:"transactions" validate:"required,min=1"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.service.BulkCreateOrderIngredientTransactions(c, req.Transactions)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{{Cause: err.Error()}}), "OrderIngredientTransactionHandler")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "OrderIngredientTransactionHandler")
|
||||
}
|
||||
21
internal/handler/order_ingredient_transaction_interface.go
Normal file
21
internal/handler/order_ingredient_transaction_interface.go
Normal file
@ -0,0 +1,21 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionService interface {
|
||||
CreateOrderIngredientTransaction(ctx context.Context, req *contract.CreateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionByID(ctx context.Context, id uuid.UUID) (*contract.OrderIngredientTransactionResponse, error)
|
||||
UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *contract.UpdateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error)
|
||||
DeleteOrderIngredientTransaction(ctx context.Context, id uuid.UUID) error
|
||||
ListOrderIngredientTransactions(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionResponse, int64, error)
|
||||
GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionSummary(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionSummary, error)
|
||||
BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*contract.CreateOrderIngredientTransactionRequest) ([]*contract.OrderIngredientTransactionResponse, error)
|
||||
}
|
||||
@ -166,3 +166,110 @@ func ModelToContractAccountResponse(resp *models.AccountResponse) *contract.Acco
|
||||
|
||||
return contractResp
|
||||
}
|
||||
|
||||
// Order Ingredient Transaction mappers
|
||||
func ContractToModelCreateOrderIngredientTransactionRequest(req *contract.CreateOrderIngredientTransactionRequest) *models.CreateOrderIngredientTransactionRequest {
|
||||
return &models.CreateOrderIngredientTransactionRequest{
|
||||
OrderID: req.OrderID,
|
||||
OrderItemID: req.OrderItemID,
|
||||
ProductID: req.ProductID,
|
||||
ProductVariantID: req.ProductVariantID,
|
||||
IngredientID: req.IngredientID,
|
||||
GrossQty: req.GrossQty,
|
||||
NetQty: req.NetQty,
|
||||
WasteQty: req.WasteQty,
|
||||
Unit: req.Unit,
|
||||
TransactionDate: req.TransactionDate,
|
||||
}
|
||||
}
|
||||
|
||||
func ContractToModelUpdateOrderIngredientTransactionRequest(req *contract.UpdateOrderIngredientTransactionRequest) *models.UpdateOrderIngredientTransactionRequest {
|
||||
return &models.UpdateOrderIngredientTransactionRequest{
|
||||
GrossQty: req.GrossQty,
|
||||
NetQty: req.NetQty,
|
||||
WasteQty: req.WasteQty,
|
||||
Unit: req.Unit,
|
||||
TransactionDate: req.TransactionDate,
|
||||
}
|
||||
}
|
||||
|
||||
func ModelToContractOrderIngredientTransactionResponse(resp *models.OrderIngredientTransactionResponse) *contract.OrderIngredientTransactionResponse {
|
||||
return &contract.OrderIngredientTransactionResponse{
|
||||
ID: resp.ID,
|
||||
OrganizationID: resp.OrganizationID,
|
||||
OutletID: resp.OutletID,
|
||||
OrderID: resp.OrderID,
|
||||
OrderItemID: resp.OrderItemID,
|
||||
ProductID: resp.ProductID,
|
||||
ProductVariantID: resp.ProductVariantID,
|
||||
IngredientID: resp.IngredientID,
|
||||
GrossQty: resp.GrossQty,
|
||||
NetQty: resp.NetQty,
|
||||
WasteQty: resp.WasteQty,
|
||||
Unit: resp.Unit,
|
||||
TransactionDate: resp.TransactionDate,
|
||||
CreatedBy: resp.CreatedBy,
|
||||
CreatedAt: resp.CreatedAt,
|
||||
UpdatedAt: resp.UpdatedAt,
|
||||
Organization: resp.Organization,
|
||||
Outlet: resp.Outlet,
|
||||
Order: resp.Order,
|
||||
OrderItem: resp.OrderItem,
|
||||
Product: resp.Product,
|
||||
ProductVariant: resp.ProductVariant,
|
||||
Ingredient: resp.Ingredient,
|
||||
CreatedByUser: resp.CreatedByUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ModelToContractOrderIngredientTransactionResponses(responses []*models.OrderIngredientTransactionResponse) []*contract.OrderIngredientTransactionResponse {
|
||||
if responses == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contractResponses := make([]*contract.OrderIngredientTransactionResponse, len(responses))
|
||||
for i, resp := range responses {
|
||||
contractResponses[i] = ModelToContractOrderIngredientTransactionResponse(resp)
|
||||
}
|
||||
|
||||
return contractResponses
|
||||
}
|
||||
|
||||
func ContractToModelListOrderIngredientTransactionsRequest(req *contract.ListOrderIngredientTransactionsRequest) *models.ListOrderIngredientTransactionsRequest {
|
||||
return &models.ListOrderIngredientTransactionsRequest{
|
||||
OrderID: req.OrderID,
|
||||
OrderItemID: req.OrderItemID,
|
||||
ProductID: req.ProductID,
|
||||
ProductVariantID: req.ProductVariantID,
|
||||
IngredientID: req.IngredientID,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
}
|
||||
}
|
||||
|
||||
func ModelToContractOrderIngredientTransactionSummary(resp *models.OrderIngredientTransactionSummary) *contract.OrderIngredientTransactionSummary {
|
||||
return &contract.OrderIngredientTransactionSummary{
|
||||
IngredientID: resp.IngredientID,
|
||||
IngredientName: resp.IngredientName,
|
||||
TotalGrossQty: resp.TotalGrossQty,
|
||||
TotalNetQty: resp.TotalNetQty,
|
||||
TotalWasteQty: resp.TotalWasteQty,
|
||||
WastePercentage: resp.WastePercentage,
|
||||
Unit: resp.Unit,
|
||||
}
|
||||
}
|
||||
|
||||
func ModelToContractOrderIngredientTransactionSummaries(responses []*models.OrderIngredientTransactionSummary) []*contract.OrderIngredientTransactionSummary {
|
||||
if responses == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contractResponses := make([]*contract.OrderIngredientTransactionSummary, len(responses))
|
||||
for i, resp := range responses {
|
||||
contractResponses[i] = ModelToContractOrderIngredientTransactionSummary(resp)
|
||||
}
|
||||
|
||||
return contractResponses
|
||||
}
|
||||
|
||||
185
internal/mappers/order_ingredient_transaction_mapper.go
Normal file
185
internal/mappers/order_ingredient_transaction_mapper.go
Normal file
@ -0,0 +1,185 @@
|
||||
package mappers
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func MapOrderIngredientTransactionEntityToModel(entity *entities.OrderIngredientTransaction) *models.OrderIngredientTransaction {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.OrderIngredientTransaction{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
OrderID: entity.OrderID,
|
||||
OrderItemID: entity.OrderItemID,
|
||||
ProductID: entity.ProductID,
|
||||
ProductVariantID: entity.ProductVariantID,
|
||||
IngredientID: entity.IngredientID,
|
||||
GrossQty: entity.GrossQty,
|
||||
NetQty: entity.NetQty,
|
||||
WasteQty: entity.WasteQty,
|
||||
Unit: entity.Unit,
|
||||
TransactionDate: entity.TransactionDate,
|
||||
CreatedBy: entity.CreatedBy,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Organization: nil,
|
||||
Outlet: nil,
|
||||
Order: nil,
|
||||
OrderItem: nil,
|
||||
Product: nil,
|
||||
ProductVariant: nil,
|
||||
Ingredient: nil,
|
||||
CreatedByUser: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func MapOrderIngredientTransactionModelToEntity(model *models.OrderIngredientTransaction) *entities.OrderIngredientTransaction {
|
||||
if model == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &entities.OrderIngredientTransaction{
|
||||
ID: model.ID,
|
||||
OrganizationID: model.OrganizationID,
|
||||
OutletID: model.OutletID,
|
||||
OrderID: model.OrderID,
|
||||
OrderItemID: model.OrderItemID,
|
||||
ProductID: model.ProductID,
|
||||
ProductVariantID: model.ProductVariantID,
|
||||
IngredientID: model.IngredientID,
|
||||
GrossQty: model.GrossQty,
|
||||
NetQty: model.NetQty,
|
||||
WasteQty: model.WasteQty,
|
||||
Unit: model.Unit,
|
||||
TransactionDate: model.TransactionDate,
|
||||
CreatedBy: model.CreatedBy,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Organization: entities.Organization{},
|
||||
Outlet: nil,
|
||||
Order: entities.Order{},
|
||||
OrderItem: nil,
|
||||
Product: entities.Product{},
|
||||
ProductVariant: nil,
|
||||
Ingredient: entities.Ingredient{},
|
||||
CreatedByUser: entities.User{},
|
||||
}
|
||||
}
|
||||
|
||||
func MapOrderIngredientTransactionEntitiesToModels(entities []*entities.OrderIngredientTransaction) []*models.OrderIngredientTransaction {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
models := make([]*models.OrderIngredientTransaction, len(entities))
|
||||
for i, entity := range entities {
|
||||
models[i] = MapOrderIngredientTransactionEntityToModel(entity)
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
func MapOrderIngredientTransactionModelsToEntities(models []*models.OrderIngredientTransaction) []*entities.OrderIngredientTransaction {
|
||||
if models == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entities := make([]*entities.OrderIngredientTransaction, len(models))
|
||||
for i, model := range models {
|
||||
entities[i] = MapOrderIngredientTransactionModelToEntity(model)
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
func MapOrderIngredientTransactionEntityToResponse(entity *entities.OrderIngredientTransaction) *models.OrderIngredientTransactionResponse {
|
||||
if entity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.OrderIngredientTransactionResponse{
|
||||
ID: entity.ID,
|
||||
OrganizationID: entity.OrganizationID,
|
||||
OutletID: entity.OutletID,
|
||||
OrderID: entity.OrderID,
|
||||
OrderItemID: entity.OrderItemID,
|
||||
ProductID: entity.ProductID,
|
||||
ProductVariantID: entity.ProductVariantID,
|
||||
IngredientID: entity.IngredientID,
|
||||
GrossQty: entity.GrossQty,
|
||||
NetQty: entity.NetQty,
|
||||
WasteQty: entity.WasteQty,
|
||||
Unit: entity.Unit,
|
||||
TransactionDate: entity.TransactionDate,
|
||||
CreatedBy: entity.CreatedBy,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Organization: nil,
|
||||
Outlet: nil,
|
||||
Order: nil,
|
||||
OrderItem: nil,
|
||||
Product: nil,
|
||||
ProductVariant: nil,
|
||||
Ingredient: nil,
|
||||
CreatedByUser: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func MapOrderIngredientTransactionEntitiesToResponses(entities []*entities.OrderIngredientTransaction) []*models.OrderIngredientTransactionResponse {
|
||||
if entities == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
responses := make([]*models.OrderIngredientTransactionResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = MapOrderIngredientTransactionEntityToResponse(entity)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
|
||||
func MapOrderIngredientTransactionSummary(transactions []*entities.OrderIngredientTransaction) []*models.OrderIngredientTransactionSummary {
|
||||
if transactions == nil || len(transactions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Group by ingredient ID
|
||||
ingredientMap := make(map[uuid.UUID]*models.OrderIngredientTransactionSummary)
|
||||
|
||||
for _, transaction := range transactions {
|
||||
ingredientID := transaction.IngredientID
|
||||
|
||||
if summary, exists := ingredientMap[ingredientID]; exists {
|
||||
summary.TotalGrossQty += transaction.GrossQty
|
||||
summary.TotalNetQty += transaction.NetQty
|
||||
summary.TotalWasteQty += transaction.WasteQty
|
||||
} else {
|
||||
ingredientMap[ingredientID] = &models.OrderIngredientTransactionSummary{
|
||||
IngredientID: ingredientID,
|
||||
IngredientName: transaction.Ingredient.Name,
|
||||
TotalGrossQty: transaction.GrossQty,
|
||||
TotalNetQty: transaction.NetQty,
|
||||
TotalWasteQty: transaction.WasteQty,
|
||||
Unit: transaction.Unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to slice and calculate waste percentage
|
||||
var summaries []*models.OrderIngredientTransactionSummary
|
||||
for _, summary := range ingredientMap {
|
||||
if summary.TotalGrossQty > 0 {
|
||||
summary.WastePercentage = (summary.TotalWasteQty / summary.TotalGrossQty) * 100
|
||||
}
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
|
||||
return summaries
|
||||
}
|
||||
@ -17,6 +17,7 @@ func MapProductIngredientEntityToModel(entity *entities.ProductIngredient) *mode
|
||||
ProductID: entity.ProductID,
|
||||
IngredientID: entity.IngredientID,
|
||||
Quantity: entity.Quantity,
|
||||
WastePercentage: entity.WastePercentage,
|
||||
CreatedAt: entity.CreatedAt,
|
||||
UpdatedAt: entity.UpdatedAt,
|
||||
Product: ProductEntityToModel(entity.Product),
|
||||
@ -36,6 +37,7 @@ func MapProductIngredientModelToEntity(model *models.ProductIngredient) *entitie
|
||||
ProductID: model.ProductID,
|
||||
IngredientID: model.IngredientID,
|
||||
Quantity: model.Quantity,
|
||||
WastePercentage: model.WastePercentage,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
Product: ProductModelToEntity(model.Product),
|
||||
|
||||
@ -94,3 +94,11 @@ type ListIngredientUnitConvertersResponse struct {
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type IngredientUnitsResponse struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
BaseUnitID uuid.UUID `json:"base_unit_id"`
|
||||
BaseUnitName string `json:"base_unit_name"`
|
||||
Units []*UnitResponse `json:"units"`
|
||||
}
|
||||
|
||||
|
||||
108
internal/models/order_ingredient_transaction.go
Normal file
108
internal/models/order_ingredient_transaction.go
Normal file
@ -0,0 +1,108 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderIngredientTransaction struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID
|
||||
OrderID uuid.UUID
|
||||
OrderItemID *uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
ProductVariantID *uuid.UUID
|
||||
IngredientID uuid.UUID
|
||||
GrossQty float64
|
||||
NetQty float64
|
||||
WasteQty float64
|
||||
Unit string
|
||||
TransactionDate time.Time
|
||||
CreatedBy uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
// Relations
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
Outlet *Outlet `json:"outlet,omitempty"`
|
||||
Order *Order `json:"order,omitempty"`
|
||||
OrderItem *OrderItem `json:"order_item,omitempty"`
|
||||
Product *Product `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||
CreatedByUser *User `json:"created_by_user,omitempty"`
|
||||
}
|
||||
|
||||
type CreateOrderIngredientTransactionRequest struct {
|
||||
OrderID uuid.UUID `json:"order_id" validate:"required"`
|
||||
OrderItemID *uuid.UUID `json:"order_item_id,omitempty"`
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
GrossQty float64 `json:"gross_qty" validate:"required,gt=0"`
|
||||
NetQty float64 `json:"net_qty" validate:"required,gt=0"`
|
||||
WasteQty float64 `json:"waste_qty" validate:"min=0"`
|
||||
Unit string `json:"unit" validate:"required,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateOrderIngredientTransactionRequest struct {
|
||||
GrossQty *float64 `json:"gross_qty,omitempty" validate:"omitempty,gt=0"`
|
||||
NetQty *float64 `json:"net_qty,omitempty" validate:"omitempty,gt=0"`
|
||||
WasteQty *float64 `json:"waste_qty,omitempty" validate:"min=0"`
|
||||
Unit *string `json:"unit,omitempty" validate:"omitempty,max=50"`
|
||||
TransactionDate *time.Time `json:"transaction_date,omitempty"`
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionResponse struct {
|
||||
ID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID
|
||||
OrderID uuid.UUID
|
||||
OrderItemID *uuid.UUID
|
||||
ProductID uuid.UUID
|
||||
ProductVariantID *uuid.UUID
|
||||
IngredientID uuid.UUID
|
||||
GrossQty float64
|
||||
NetQty float64
|
||||
WasteQty float64
|
||||
Unit string
|
||||
TransactionDate time.Time
|
||||
CreatedBy uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
// Relations
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
Outlet *Outlet `json:"outlet,omitempty"`
|
||||
Order *Order `json:"order,omitempty"`
|
||||
OrderItem *OrderItem `json:"order_item,omitempty"`
|
||||
Product *Product `json:"product,omitempty"`
|
||||
ProductVariant *ProductVariant `json:"product_variant,omitempty"`
|
||||
Ingredient *Ingredient `json:"ingredient,omitempty"`
|
||||
CreatedByUser *User `json:"created_by_user,omitempty"`
|
||||
}
|
||||
|
||||
type ListOrderIngredientTransactionsRequest struct {
|
||||
OrderID *uuid.UUID `json:"order_id,omitempty"`
|
||||
OrderItemID *uuid.UUID `json:"order_item_id,omitempty"`
|
||||
ProductID *uuid.UUID `json:"product_id,omitempty"`
|
||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
||||
IngredientID *uuid.UUID `json:"ingredient_id,omitempty"`
|
||||
StartDate *time.Time `json:"start_date,omitempty"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
Page int `json:"page" validate:"min=1"`
|
||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionSummary struct {
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
IngredientName string `json:"ingredient_name"`
|
||||
TotalGrossQty float64 `json:"total_gross_qty"`
|
||||
TotalNetQty float64 `json:"total_net_qty"`
|
||||
TotalWasteQty float64 `json:"total_waste_qty"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
@ -13,6 +13,7 @@ type ProductIngredient struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@ -26,11 +27,13 @@ type CreateProductIngredientRequest struct {
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id" validate:"required"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"`
|
||||
}
|
||||
|
||||
type UpdateProductIngredientRequest struct {
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
WastePercentage float64 `json:"waste_percentage" validate:"min=0,max=100"`
|
||||
}
|
||||
|
||||
type ProductIngredientResponse struct {
|
||||
@ -40,6 +43,7 @@ type ProductIngredientResponse struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
IngredientID uuid.UUID `json:"ingredient_id"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
WastePercentage float64 `json:"waste_percentage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
|
||||
type AccountProcessor interface {
|
||||
CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error)
|
||||
GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error)
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
|
||||
type ChartOfAccountTypeProcessor interface {
|
||||
CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error)
|
||||
GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error)
|
||||
|
||||
@ -1,17 +1 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type IngredientRepository interface {
|
||||
Create(ctx context.Context, ingredient *entities.Ingredient) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error)
|
||||
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error)
|
||||
Update(ctx context.Context, ingredient *entities.Ingredient) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ type IngredientUnitConverterProcessor interface {
|
||||
ListIngredientUnitConverters(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.IngredientUnitConverterResponse, int, error)
|
||||
GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error)
|
||||
ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error)
|
||||
GetUnitsByIngredientID(ctx context.Context, organizationID, ingredientID uuid.UUID) (*models.IngredientUnitsResponse, error)
|
||||
}
|
||||
|
||||
type IngredientUnitConverterProcessorImpl struct {
|
||||
@ -257,3 +258,64 @@ func (p *IngredientUnitConverterProcessorImpl) ConvertUnit(ctx context.Context,
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *IngredientUnitConverterProcessorImpl) GetUnitsByIngredientID(ctx context.Context, organizationID, ingredientID uuid.UUID) (*models.IngredientUnitsResponse, error) {
|
||||
// Get the ingredient with its base unit
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
||||
}
|
||||
|
||||
// Get the base unit details
|
||||
baseUnit, err := p.unitRepo.GetByID(ctx, ingredient.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get base unit: %w", err)
|
||||
}
|
||||
|
||||
// Start with the base unit
|
||||
units := []*models.UnitResponse{
|
||||
mappers.MapUnitEntityToResponse(baseUnit),
|
||||
}
|
||||
|
||||
// Get all converters for this ingredient
|
||||
converters, err := p.converterRepo.GetConvertersForIngredient(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get converters: %w", err)
|
||||
}
|
||||
|
||||
// Add unique units from converters
|
||||
unitMap := make(map[uuid.UUID]bool)
|
||||
unitMap[baseUnit.ID] = true
|
||||
|
||||
for _, converter := range converters {
|
||||
if converter.IsActive {
|
||||
// Add FromUnit if not already added
|
||||
if !unitMap[converter.FromUnitID] {
|
||||
fromUnit, err := p.unitRepo.GetByID(ctx, converter.FromUnitID, organizationID)
|
||||
if err == nil {
|
||||
units = append(units, mappers.MapUnitEntityToResponse(fromUnit))
|
||||
unitMap[converter.FromUnitID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add ToUnit if not already added
|
||||
if !unitMap[converter.ToUnitID] {
|
||||
toUnit, err := p.unitRepo.GetByID(ctx, converter.ToUnitID, organizationID)
|
||||
if err == nil {
|
||||
units = append(units, mappers.MapUnitEntityToResponse(toUnit))
|
||||
unitMap[converter.ToUnitID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response := &models.IngredientUnitsResponse{
|
||||
IngredientID: ingredientID,
|
||||
IngredientName: ingredient.Name,
|
||||
BaseUnitID: baseUnit.ID,
|
||||
BaseUnitName: baseUnit.Name,
|
||||
Units: units,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
393
internal/processor/order_ingredient_transaction_processor.go
Normal file
393
internal/processor/order_ingredient_transaction_processor.go
Normal file
@ -0,0 +1,393 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/util"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionProcessor interface {
|
||||
CreateOrderIngredientTransaction(ctx context.Context, req *models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) (*models.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionByID(ctx context.Context, id, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error)
|
||||
UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *models.UpdateOrderIngredientTransactionRequest, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error)
|
||||
DeleteOrderIngredientTransaction(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
ListOrderIngredientTransactions(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, int64, error)
|
||||
GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error)
|
||||
GetOrderIngredientTransactionSummary(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionSummary, error)
|
||||
BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error)
|
||||
CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error)
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionProcessorImpl struct {
|
||||
orderIngredientTransactionRepo OrderIngredientTransactionRepository
|
||||
productIngredientRepo ProductIngredientRepository
|
||||
ingredientRepo IngredientRepository
|
||||
unitRepo UnitRepository
|
||||
}
|
||||
|
||||
func NewOrderIngredientTransactionProcessorImpl(
|
||||
orderIngredientTransactionRepo OrderIngredientTransactionRepository,
|
||||
productIngredientRepo ProductIngredientRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
unitRepo UnitRepository,
|
||||
) OrderIngredientTransactionProcessor {
|
||||
return &OrderIngredientTransactionProcessorImpl{
|
||||
orderIngredientTransactionRepo: orderIngredientTransactionRepo,
|
||||
productIngredientRepo: productIngredientRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
unitRepo: unitRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) CreateOrderIngredientTransaction(ctx context.Context, req *models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) (*models.OrderIngredientTransactionResponse, error) {
|
||||
// Validate that gross qty >= net qty
|
||||
if req.GrossQty < req.NetQty {
|
||||
return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity")
|
||||
}
|
||||
|
||||
// Validate that waste qty = gross qty - net qty
|
||||
expectedWasteQty := req.GrossQty - req.NetQty
|
||||
if req.WasteQty != expectedWasteQty {
|
||||
return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity")
|
||||
}
|
||||
|
||||
// Set transaction date if not provided
|
||||
transactionDate := time.Now()
|
||||
if req.TransactionDate != nil {
|
||||
transactionDate = *req.TransactionDate
|
||||
}
|
||||
|
||||
// Create entity
|
||||
entity := &entities.OrderIngredientTransaction{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: &outletID,
|
||||
OrderID: req.OrderID,
|
||||
OrderItemID: req.OrderItemID,
|
||||
ProductID: req.ProductID,
|
||||
ProductVariantID: req.ProductVariantID,
|
||||
IngredientID: req.IngredientID,
|
||||
GrossQty: req.GrossQty,
|
||||
NetQty: req.NetQty,
|
||||
WasteQty: req.WasteQty,
|
||||
Unit: req.Unit,
|
||||
TransactionDate: transactionDate,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
|
||||
// Create in database
|
||||
if err := p.orderIngredientTransactionRepo.Create(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Get created entity with relations
|
||||
createdEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, entity.ID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get created order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Convert to response
|
||||
response := mappers.MapOrderIngredientTransactionEntityToResponse(createdEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionByID(ctx context.Context, id, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) {
|
||||
entity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.MapOrderIngredientTransactionEntityToResponse(entity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *models.UpdateOrderIngredientTransactionRequest, organizationID uuid.UUID) (*models.OrderIngredientTransactionResponse, error) {
|
||||
// Get existing entity
|
||||
entity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if req.GrossQty != nil {
|
||||
entity.GrossQty = *req.GrossQty
|
||||
}
|
||||
if req.NetQty != nil {
|
||||
entity.NetQty = *req.NetQty
|
||||
}
|
||||
if req.WasteQty != nil {
|
||||
entity.WasteQty = *req.WasteQty
|
||||
}
|
||||
if req.Unit != nil {
|
||||
entity.Unit = *req.Unit
|
||||
}
|
||||
if req.TransactionDate != nil {
|
||||
entity.TransactionDate = *req.TransactionDate
|
||||
}
|
||||
|
||||
// Validate quantities
|
||||
if entity.GrossQty < entity.NetQty {
|
||||
return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity")
|
||||
}
|
||||
|
||||
expectedWasteQty := entity.GrossQty - entity.NetQty
|
||||
if entity.WasteQty != expectedWasteQty {
|
||||
return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity")
|
||||
}
|
||||
|
||||
// Update in database
|
||||
if err := p.orderIngredientTransactionRepo.Update(ctx, entity); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Get updated entity with relations
|
||||
updatedEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get updated order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.MapOrderIngredientTransactionEntityToResponse(updatedEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) DeleteOrderIngredientTransaction(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
if err := p.orderIngredientTransactionRepo.Delete(ctx, id, organizationID); err != nil {
|
||||
return fmt.Errorf("failed to delete order ingredient transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) ListOrderIngredientTransactions(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, int64, error) {
|
||||
// Convert filters
|
||||
filters := make(map[string]interface{})
|
||||
if req.OrderID != nil {
|
||||
filters["order_id"] = *req.OrderID
|
||||
}
|
||||
if req.OrderItemID != nil {
|
||||
filters["order_item_id"] = *req.OrderItemID
|
||||
}
|
||||
if req.ProductID != nil {
|
||||
filters["product_id"] = *req.ProductID
|
||||
}
|
||||
if req.ProductVariantID != nil {
|
||||
filters["product_variant_id"] = *req.ProductVariantID
|
||||
}
|
||||
if req.IngredientID != nil {
|
||||
filters["ingredient_id"] = *req.IngredientID
|
||||
}
|
||||
if req.StartDate != nil {
|
||||
filters["start_date"] = req.StartDate.Format(time.RFC3339)
|
||||
}
|
||||
if req.EndDate != nil {
|
||||
filters["end_date"] = req.EndDate.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Set default pagination
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
entities, total, err := p.orderIngredientTransactionRepo.List(ctx, organizationID, filters, page, limit)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list order ingredient transactions: %w", err)
|
||||
}
|
||||
|
||||
responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities)
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) {
|
||||
entities, err := p.orderIngredientTransactionRepo.GetByOrderID(ctx, orderID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transactions by order: %w", err)
|
||||
}
|
||||
|
||||
responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities)
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) {
|
||||
entities, err := p.orderIngredientTransactionRepo.GetByOrderItemID(ctx, orderItemID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transactions by order item: %w", err)
|
||||
}
|
||||
|
||||
responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities)
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) {
|
||||
entities, err := p.orderIngredientTransactionRepo.GetByIngredientID(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transactions by ingredient: %w", err)
|
||||
}
|
||||
|
||||
responses := mappers.MapOrderIngredientTransactionEntitiesToResponses(entities)
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) GetOrderIngredientTransactionSummary(ctx context.Context, req *models.ListOrderIngredientTransactionsRequest, organizationID uuid.UUID) ([]*models.OrderIngredientTransactionSummary, error) {
|
||||
// Convert filters
|
||||
filters := make(map[string]interface{})
|
||||
if req.OrderID != nil {
|
||||
filters["order_id"] = *req.OrderID
|
||||
}
|
||||
if req.OrderItemID != nil {
|
||||
filters["order_item_id"] = *req.OrderItemID
|
||||
}
|
||||
if req.ProductID != nil {
|
||||
filters["product_id"] = *req.ProductID
|
||||
}
|
||||
if req.ProductVariantID != nil {
|
||||
filters["product_variant_id"] = *req.ProductVariantID
|
||||
}
|
||||
if req.IngredientID != nil {
|
||||
filters["ingredient_id"] = *req.IngredientID
|
||||
}
|
||||
if req.StartDate != nil {
|
||||
filters["start_date"] = req.StartDate.Format(time.RFC3339)
|
||||
}
|
||||
if req.EndDate != nil {
|
||||
filters["end_date"] = req.EndDate.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
entities, err := p.orderIngredientTransactionRepo.GetSummary(ctx, organizationID, filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transaction summary: %w", err)
|
||||
}
|
||||
|
||||
summaries := mappers.MapOrderIngredientTransactionSummary(entities)
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*models.CreateOrderIngredientTransactionRequest, organizationID, outletID, createdBy uuid.UUID) ([]*models.OrderIngredientTransactionResponse, error) {
|
||||
if len(transactions) == 0 {
|
||||
return []*models.OrderIngredientTransactionResponse{}, nil
|
||||
}
|
||||
|
||||
// Convert to entities
|
||||
transactionEntities := make([]*entities.OrderIngredientTransaction, len(transactions))
|
||||
for i, req := range transactions {
|
||||
// Validate quantities
|
||||
if req.GrossQty < req.NetQty {
|
||||
return nil, fmt.Errorf("gross quantity must be greater than or equal to net quantity for transaction %d", i)
|
||||
}
|
||||
|
||||
expectedWasteQty := req.GrossQty - req.NetQty
|
||||
if req.WasteQty != expectedWasteQty {
|
||||
return nil, fmt.Errorf("waste quantity must equal gross quantity minus net quantity for transaction %d", i)
|
||||
}
|
||||
|
||||
// Set transaction date if not provided
|
||||
transactionDate := time.Now()
|
||||
if req.TransactionDate != nil {
|
||||
transactionDate = *req.TransactionDate
|
||||
}
|
||||
|
||||
transactionEntities[i] = &entities.OrderIngredientTransaction{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: organizationID,
|
||||
OutletID: &outletID,
|
||||
OrderID: req.OrderID,
|
||||
OrderItemID: req.OrderItemID,
|
||||
ProductID: req.ProductID,
|
||||
ProductVariantID: req.ProductVariantID,
|
||||
IngredientID: req.IngredientID,
|
||||
GrossQty: req.GrossQty,
|
||||
NetQty: req.NetQty,
|
||||
WasteQty: req.WasteQty,
|
||||
Unit: req.Unit,
|
||||
TransactionDate: transactionDate,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk create
|
||||
if err := p.orderIngredientTransactionRepo.BulkCreate(ctx, transactionEntities); err != nil {
|
||||
return nil, fmt.Errorf("failed to bulk create order ingredient transactions: %w", err)
|
||||
}
|
||||
|
||||
// Get created entities with relations
|
||||
responses := make([]*models.OrderIngredientTransactionResponse, len(transactionEntities))
|
||||
for i, entity := range transactionEntities {
|
||||
createdEntity, err := p.orderIngredientTransactionRepo.GetByID(ctx, entity.ID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get created order ingredient transaction %d: %w", i, err)
|
||||
}
|
||||
responses[i] = mappers.MapOrderIngredientTransactionEntityToResponse(createdEntity)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *OrderIngredientTransactionProcessorImpl) CalculateWasteQuantities(ctx context.Context, productID uuid.UUID, quantity float64, organizationID uuid.UUID) ([]*models.CreateOrderIngredientTransactionRequest, error) {
|
||||
// Get product ingredients
|
||||
productIngredients, err := p.productIngredientRepo.GetByProductID(ctx, productID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get product ingredients: %w", err)
|
||||
}
|
||||
|
||||
if len(productIngredients) == 0 {
|
||||
return []*models.CreateOrderIngredientTransactionRequest{}, nil
|
||||
}
|
||||
|
||||
// Get ingredient details for unit information
|
||||
ingredientMap := make(map[uuid.UUID]*entities.Ingredient)
|
||||
for _, pi := range productIngredients {
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, pi.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ingredient %s: %w", pi.IngredientID, err)
|
||||
}
|
||||
ingredientMap[pi.IngredientID] = ingredient
|
||||
}
|
||||
|
||||
// Calculate quantities for each ingredient
|
||||
transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productIngredients))
|
||||
for _, pi := range productIngredients {
|
||||
ingredient := ingredientMap[pi.IngredientID]
|
||||
|
||||
// Calculate net quantity (actual quantity needed for the product)
|
||||
netQty := pi.Quantity * quantity
|
||||
|
||||
// Calculate gross quantity (including waste)
|
||||
wasteMultiplier := 1 + (pi.WastePercentage / 100)
|
||||
grossQty := netQty * wasteMultiplier
|
||||
|
||||
// Calculate waste quantity
|
||||
wasteQty := grossQty - netQty
|
||||
|
||||
// Get unit name
|
||||
unitName := "unit" // default
|
||||
if ingredient.UnitID != uuid.Nil {
|
||||
unit, err := p.unitRepo.GetByID(ctx, ingredient.UnitID, organizationID)
|
||||
if err == nil {
|
||||
unitName = unit.Name
|
||||
}
|
||||
}
|
||||
|
||||
transaction := &models.CreateOrderIngredientTransactionRequest{
|
||||
IngredientID: pi.IngredientID,
|
||||
GrossQty: util.RoundToDecimalPlaces(grossQty, 3),
|
||||
NetQty: util.RoundToDecimalPlaces(netQty, 3),
|
||||
WasteQty: util.RoundToDecimalPlaces(wasteQty, 3),
|
||||
Unit: unitName,
|
||||
}
|
||||
|
||||
transactions = append(transactions, transaction)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
@ -42,3 +42,43 @@ type AccountRepository interface {
|
||||
UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error
|
||||
GetBalance(ctx context.Context, id uuid.UUID) (float64, error)
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionRepository interface {
|
||||
Create(ctx context.Context, transaction *entities.OrderIngredientTransaction) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.OrderIngredientTransaction, error)
|
||||
Update(ctx context.Context, transaction *entities.OrderIngredientTransaction) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.OrderIngredientTransaction, int64, error)
|
||||
GetByOrderID(ctx context.Context, orderID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error)
|
||||
GetByOrderItemID(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error)
|
||||
GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error)
|
||||
GetSummary(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) ([]*entities.OrderIngredientTransaction, error)
|
||||
BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error
|
||||
}
|
||||
|
||||
type ProductIngredientRepository interface {
|
||||
Create(ctx context.Context, productIngredient *entities.ProductIngredient) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.ProductIngredient, error)
|
||||
GetByProductID(ctx context.Context, productID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error)
|
||||
GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.ProductIngredient, error)
|
||||
Update(ctx context.Context, productIngredient *entities.ProductIngredient) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
DeleteByProductID(ctx context.Context, productID, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
type IngredientRepository interface {
|
||||
Create(ctx context.Context, ingredient *entities.Ingredient) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Ingredient, error)
|
||||
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string, isSemiFinished *bool) ([]*entities.Ingredient, int, error)
|
||||
Update(ctx context.Context, ingredient *entities.Ingredient) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
UpdateStock(ctx context.Context, id uuid.UUID, newStock float64, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
type UnitRepository interface {
|
||||
Create(ctx context.Context, unit *entities.Unit) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error)
|
||||
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error)
|
||||
Update(ctx context.Context, unit *entities.Unit) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
@ -1,16 +1 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UnitRepository interface {
|
||||
Create(ctx context.Context, unit *entities.Unit) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Unit, error)
|
||||
GetAll(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, page, limit int, search string) ([]*entities.Unit, int, error)
|
||||
Update(ctx context.Context, unit *entities.Unit) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
}
|
||||
|
||||
234
internal/repository/order_ingredient_transaction_repository.go
Normal file
234
internal/repository/order_ingredient_transaction_repository.go
Normal file
@ -0,0 +1,234 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionRepository interface {
|
||||
Create(ctx context.Context, transaction *entities.OrderIngredientTransaction) error
|
||||
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.OrderIngredientTransaction, error)
|
||||
Update(ctx context.Context, transaction *entities.OrderIngredientTransaction) error
|
||||
Delete(ctx context.Context, id, organizationID uuid.UUID) error
|
||||
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.OrderIngredientTransaction, int64, error)
|
||||
GetByOrderID(ctx context.Context, orderID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error)
|
||||
GetByOrderItemID(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error)
|
||||
GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error)
|
||||
GetSummary(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) ([]*entities.OrderIngredientTransaction, error)
|
||||
BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOrderIngredientTransactionRepositoryImpl(db *gorm.DB) OrderIngredientTransactionRepository {
|
||||
return &OrderIngredientTransactionRepositoryImpl{db: db}
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) Create(ctx context.Context, transaction *entities.OrderIngredientTransaction) error {
|
||||
return r.db.WithContext(ctx).Create(transaction).Error
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.OrderIngredientTransaction, error) {
|
||||
var transaction entities.OrderIngredientTransaction
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||
Preload("Organization").
|
||||
Preload("Outlet").
|
||||
Preload("Order").
|
||||
Preload("OrderItem").
|
||||
Preload("Product").
|
||||
Preload("ProductVariant").
|
||||
Preload("Ingredient").
|
||||
Preload("CreatedByUser").
|
||||
First(&transaction).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &transaction, nil
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) Update(ctx context.Context, transaction *entities.OrderIngredientTransaction) error {
|
||||
return r.db.WithContext(ctx).Save(transaction).Error
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) Delete(ctx context.Context, id, organizationID uuid.UUID) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("id = ? AND organization_id = ?", id, organizationID).
|
||||
Delete(&entities.OrderIngredientTransaction{}).Error
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.OrderIngredientTransaction, int64, error) {
|
||||
var transactions []*entities.OrderIngredientTransaction
|
||||
var total int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.OrderIngredientTransaction{}).
|
||||
Where("organization_id = ?", organizationID)
|
||||
|
||||
// Apply filters
|
||||
for key, value := range filters {
|
||||
switch key {
|
||||
case "order_id":
|
||||
if orderID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("order_id = ?", orderID)
|
||||
}
|
||||
case "order_item_id":
|
||||
if orderItemID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("order_item_id = ?", orderItemID)
|
||||
}
|
||||
case "product_id":
|
||||
if productID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("product_id = ?", productID)
|
||||
}
|
||||
case "product_variant_id":
|
||||
if productVariantID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("product_variant_id = ?", productVariantID)
|
||||
}
|
||||
case "ingredient_id":
|
||||
if ingredientID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("ingredient_id = ?", ingredientID)
|
||||
}
|
||||
case "start_date":
|
||||
if startDate, ok := value.(string); ok {
|
||||
query = query.Where("transaction_date >= ?", startDate)
|
||||
}
|
||||
case "end_date":
|
||||
if endDate, ok := value.(string); ok {
|
||||
query = query.Where("transaction_date <= ?", endDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get total count
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Apply pagination and get results
|
||||
offset := (page - 1) * limit
|
||||
err := query.
|
||||
Preload("Organization").
|
||||
Preload("Outlet").
|
||||
Preload("Order").
|
||||
Preload("OrderItem").
|
||||
Preload("Product").
|
||||
Preload("ProductVariant").
|
||||
Preload("Ingredient").
|
||||
Preload("CreatedByUser").
|
||||
Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Find(&transactions).Error
|
||||
|
||||
return transactions, total, err
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) GetByOrderID(ctx context.Context, orderID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) {
|
||||
var transactions []*entities.OrderIngredientTransaction
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("order_id = ? AND organization_id = ?", orderID, organizationID).
|
||||
Preload("Organization").
|
||||
Preload("Outlet").
|
||||
Preload("Order").
|
||||
Preload("OrderItem").
|
||||
Preload("Product").
|
||||
Preload("ProductVariant").
|
||||
Preload("Ingredient").
|
||||
Preload("CreatedByUser").
|
||||
Order("created_at ASC").
|
||||
Find(&transactions).Error
|
||||
return transactions, err
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) GetByOrderItemID(ctx context.Context, orderItemID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) {
|
||||
var transactions []*entities.OrderIngredientTransaction
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("order_item_id = ? AND organization_id = ?", orderItemID, organizationID).
|
||||
Preload("Organization").
|
||||
Preload("Outlet").
|
||||
Preload("Order").
|
||||
Preload("OrderItem").
|
||||
Preload("Product").
|
||||
Preload("ProductVariant").
|
||||
Preload("Ingredient").
|
||||
Preload("CreatedByUser").
|
||||
Order("created_at ASC").
|
||||
Find(&transactions).Error
|
||||
return transactions, err
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) GetByIngredientID(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.OrderIngredientTransaction, error) {
|
||||
var transactions []*entities.OrderIngredientTransaction
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("ingredient_id = ? AND organization_id = ?", ingredientID, organizationID).
|
||||
Preload("Organization").
|
||||
Preload("Outlet").
|
||||
Preload("Order").
|
||||
Preload("OrderItem").
|
||||
Preload("Product").
|
||||
Preload("ProductVariant").
|
||||
Preload("Ingredient").
|
||||
Preload("CreatedByUser").
|
||||
Order("created_at DESC").
|
||||
Find(&transactions).Error
|
||||
return transactions, err
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) GetSummary(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) ([]*entities.OrderIngredientTransaction, error) {
|
||||
var transactions []*entities.OrderIngredientTransaction
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&entities.OrderIngredientTransaction{}).
|
||||
Where("organization_id = ?", organizationID)
|
||||
|
||||
// Apply filters
|
||||
for key, value := range filters {
|
||||
switch key {
|
||||
case "order_id":
|
||||
if orderID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("order_id = ?", orderID)
|
||||
}
|
||||
case "order_item_id":
|
||||
if orderItemID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("order_item_id = ?", orderItemID)
|
||||
}
|
||||
case "product_id":
|
||||
if productID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("product_id = ?", productID)
|
||||
}
|
||||
case "product_variant_id":
|
||||
if productVariantID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("product_variant_id = ?", productVariantID)
|
||||
}
|
||||
case "ingredient_id":
|
||||
if ingredientID, ok := value.(uuid.UUID); ok {
|
||||
query = query.Where("ingredient_id = ?", ingredientID)
|
||||
}
|
||||
case "start_date":
|
||||
if startDate, ok := value.(string); ok {
|
||||
query = query.Where("transaction_date >= ?", startDate)
|
||||
}
|
||||
case "end_date":
|
||||
if endDate, ok := value.(string); ok {
|
||||
query = query.Where("transaction_date <= ?", endDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := query.
|
||||
Preload("Ingredient").
|
||||
Order("ingredient_id, created_at ASC").
|
||||
Find(&transactions).Error
|
||||
|
||||
return transactions, err
|
||||
}
|
||||
|
||||
func (r *OrderIngredientTransactionRepositoryImpl) BulkCreate(ctx context.Context, transactions []*entities.OrderIngredientTransaction) error {
|
||||
if len(transactions) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.WithContext(ctx).CreateInBatches(transactions, 100).Error
|
||||
}
|
||||
@ -39,6 +39,7 @@ type Router struct {
|
||||
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
|
||||
chartOfAccountHandler *handler.ChartOfAccountHandler
|
||||
accountHandler *handler.AccountHandler
|
||||
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
|
||||
authMiddleware *middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
@ -87,7 +88,9 @@ func NewRouter(cfg *config.Config,
|
||||
chartOfAccountService service.ChartOfAccountService,
|
||||
chartOfAccountValidator validator.ChartOfAccountValidator,
|
||||
accountService service.AccountService,
|
||||
accountValidator validator.AccountValidator) *Router {
|
||||
accountValidator validator.AccountValidator,
|
||||
orderIngredientTransactionService service.OrderIngredientTransactionService,
|
||||
orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator) *Router {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -116,6 +119,7 @@ func NewRouter(cfg *config.Config,
|
||||
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
|
||||
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
|
||||
accountHandler: handler.NewAccountHandler(accountService, accountValidator),
|
||||
orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
|
||||
authMiddleware: authMiddleware,
|
||||
}
|
||||
}
|
||||
@ -351,6 +355,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
unitConverters.POST("", r.unitConverterHandler.CreateIngredientUnitConverter)
|
||||
unitConverters.GET("", r.unitConverterHandler.ListIngredientUnitConverters)
|
||||
unitConverters.GET("/ingredient/:ingredient_id", r.unitConverterHandler.GetConvertersForIngredient)
|
||||
unitConverters.GET("/ingredient/:ingredient_id/units", r.unitConverterHandler.GetUnitsByIngredientID)
|
||||
unitConverters.POST("/convert", r.unitConverterHandler.ConvertUnit)
|
||||
unitConverters.GET("/:id", r.unitConverterHandler.GetIngredientUnitConverter)
|
||||
unitConverters.PUT("/:id", r.unitConverterHandler.UpdateIngredientUnitConverter)
|
||||
@ -406,6 +411,21 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
accounts.GET("/:id/balance", r.accountHandler.GetAccountBalance)
|
||||
}
|
||||
|
||||
orderIngredientTransactions := protected.Group("/order-ingredient-transactions")
|
||||
orderIngredientTransactions.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
orderIngredientTransactions.POST("", r.orderIngredientTransactionHandler.CreateOrderIngredientTransaction)
|
||||
orderIngredientTransactions.GET("", r.orderIngredientTransactionHandler.ListOrderIngredientTransactions)
|
||||
orderIngredientTransactions.GET("/:id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionByID)
|
||||
orderIngredientTransactions.PUT("/:id", r.orderIngredientTransactionHandler.UpdateOrderIngredientTransaction)
|
||||
orderIngredientTransactions.DELETE("/:id", r.orderIngredientTransactionHandler.DeleteOrderIngredientTransaction)
|
||||
orderIngredientTransactions.GET("/order/:order_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByOrder)
|
||||
orderIngredientTransactions.GET("/order-item/:order_item_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByOrderItem)
|
||||
orderIngredientTransactions.GET("/ingredient/:ingredient_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByIngredient)
|
||||
orderIngredientTransactions.GET("/summary", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionSummary)
|
||||
orderIngredientTransactions.POST("/bulk", r.orderIngredientTransactionHandler.BulkCreateOrderIngredientTransactions)
|
||||
}
|
||||
|
||||
outlets := protected.Group("/outlets")
|
||||
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
|
||||
@ -19,6 +19,7 @@ type IngredientUnitConverterService interface {
|
||||
ListIngredientUnitConverters(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListIngredientUnitConvertersRequest) *contract.Response
|
||||
GetConvertersForIngredient(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response
|
||||
ConvertUnit(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ConvertUnitRequest) *contract.Response
|
||||
GetUnitsByIngredientID(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response
|
||||
}
|
||||
|
||||
type IngredientUnitConverterServiceImpl struct {
|
||||
@ -149,3 +150,14 @@ func (s *IngredientUnitConverterServiceImpl) ConvertUnit(ctx context.Context, ap
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *IngredientUnitConverterServiceImpl) GetUnitsByIngredientID(ctx context.Context, apctx *appcontext.ContextInfo, ingredientID uuid.UUID) *contract.Response {
|
||||
unitsResponse, err := s.converterProcessor.GetUnitsByIngredientID(ctx, apctx.OrganizationID, ingredientID)
|
||||
if err != nil {
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.IngredientUnitConverterServiceEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
contractResponse := transformer.IngredientUnitsModelResponseToResponse(unitsResponse)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
|
||||
205
internal/service/order_ingredient_transaction_service.go
Normal file
205
internal/service/order_ingredient_transaction_service.go
Normal file
@ -0,0 +1,205 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionService struct {
|
||||
processor processor.OrderIngredientTransactionProcessor
|
||||
txManager *repository.TxManager
|
||||
}
|
||||
|
||||
func NewOrderIngredientTransactionService(processor processor.OrderIngredientTransactionProcessor, txManager *repository.TxManager) *OrderIngredientTransactionService {
|
||||
return &OrderIngredientTransactionService{
|
||||
processor: processor,
|
||||
txManager: txManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) CreateOrderIngredientTransaction(ctx context.Context, req *contract.CreateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization and outlet from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
outletID := appCtx.OutletID
|
||||
createdBy := appCtx.UserID
|
||||
|
||||
// Convert contract to model
|
||||
modelReq := mappers.ContractToModelCreateOrderIngredientTransactionRequest(req)
|
||||
|
||||
// Create transaction
|
||||
response, err := s.processor.CreateOrderIngredientTransaction(ctx, modelReq, organizationID, outletID, createdBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Convert model to contract
|
||||
contractResp := mappers.ModelToContractOrderIngredientTransactionResponse(response)
|
||||
return contractResp, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionByID(ctx context.Context, id uuid.UUID) (*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Get transaction
|
||||
response, err := s.processor.GetOrderIngredientTransactionByID(ctx, id, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Convert model to contract
|
||||
contractResp := mappers.ModelToContractOrderIngredientTransactionResponse(response)
|
||||
return contractResp, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) UpdateOrderIngredientTransaction(ctx context.Context, id uuid.UUID, req *contract.UpdateOrderIngredientTransactionRequest) (*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Convert contract to model
|
||||
modelReq := mappers.ContractToModelUpdateOrderIngredientTransactionRequest(req)
|
||||
|
||||
// Update transaction
|
||||
response, err := s.processor.UpdateOrderIngredientTransaction(ctx, id, modelReq, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
// Convert model to contract
|
||||
contractResp := mappers.ModelToContractOrderIngredientTransactionResponse(response)
|
||||
return contractResp, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) DeleteOrderIngredientTransaction(ctx context.Context, id uuid.UUID) error {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Delete transaction
|
||||
if err := s.processor.DeleteOrderIngredientTransaction(ctx, id, organizationID); err != nil {
|
||||
return fmt.Errorf("failed to delete order ingredient transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) ListOrderIngredientTransactions(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionResponse, int64, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Convert contract to model
|
||||
modelReq := mappers.ContractToModelListOrderIngredientTransactionsRequest(req)
|
||||
|
||||
// List transactions
|
||||
responses, total, err := s.processor.ListOrderIngredientTransactions(ctx, modelReq, organizationID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list order ingredient transactions: %w", err)
|
||||
}
|
||||
|
||||
// Convert models to contracts
|
||||
contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses)
|
||||
return contractResponses, total, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionsByOrder(ctx context.Context, orderID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Get transactions by order
|
||||
responses, err := s.processor.GetOrderIngredientTransactionsByOrder(ctx, orderID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transactions by order: %w", err)
|
||||
}
|
||||
|
||||
// Convert models to contracts
|
||||
contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses)
|
||||
return contractResponses, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionsByOrderItem(ctx context.Context, orderItemID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Get transactions by order item
|
||||
responses, err := s.processor.GetOrderIngredientTransactionsByOrderItem(ctx, orderItemID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transactions by order item: %w", err)
|
||||
}
|
||||
|
||||
// Convert models to contracts
|
||||
contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses)
|
||||
return contractResponses, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionsByIngredient(ctx context.Context, ingredientID uuid.UUID) ([]*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Get transactions by ingredient
|
||||
responses, err := s.processor.GetOrderIngredientTransactionsByIngredient(ctx, ingredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transactions by ingredient: %w", err)
|
||||
}
|
||||
|
||||
// Convert models to contracts
|
||||
contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses)
|
||||
return contractResponses, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) GetOrderIngredientTransactionSummary(ctx context.Context, req *contract.ListOrderIngredientTransactionsRequest) ([]*contract.OrderIngredientTransactionSummary, error) {
|
||||
// Get organization from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
// Convert contract to model
|
||||
modelReq := mappers.ContractToModelListOrderIngredientTransactionsRequest(req)
|
||||
|
||||
// Get summary
|
||||
summaries, err := s.processor.GetOrderIngredientTransactionSummary(ctx, modelReq, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order ingredient transaction summary: %w", err)
|
||||
}
|
||||
|
||||
// Convert models to contracts
|
||||
contractSummaries := mappers.ModelToContractOrderIngredientTransactionSummaries(summaries)
|
||||
return contractSummaries, nil
|
||||
}
|
||||
|
||||
func (s *OrderIngredientTransactionService) BulkCreateOrderIngredientTransactions(ctx context.Context, transactions []*contract.CreateOrderIngredientTransactionRequest) ([]*contract.OrderIngredientTransactionResponse, error) {
|
||||
// Get organization and outlet from context
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
outletID := appCtx.OutletID
|
||||
createdBy := appCtx.UserID
|
||||
|
||||
// Convert contracts to models
|
||||
modelReqs := make([]*models.CreateOrderIngredientTransactionRequest, len(transactions))
|
||||
for i, req := range transactions {
|
||||
modelReqs[i] = mappers.ContractToModelCreateOrderIngredientTransactionRequest(req)
|
||||
}
|
||||
|
||||
// Bulk create transactions
|
||||
responses, err := s.processor.BulkCreateOrderIngredientTransactions(ctx, modelReqs, organizationID, outletID, createdBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to bulk create order ingredient transactions: %w", err)
|
||||
}
|
||||
|
||||
// Convert models to contracts
|
||||
contractResponses := mappers.ModelToContractOrderIngredientTransactionResponses(responses)
|
||||
return contractResponses, nil
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"apskel-pos-be/internal/util"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@ -29,12 +33,20 @@ type OrderService interface {
|
||||
type OrderServiceImpl struct {
|
||||
orderProcessor processor.OrderProcessor
|
||||
tableRepo repository.TableRepositoryInterface
|
||||
orderIngredientTransactionService *OrderIngredientTransactionService
|
||||
orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor
|
||||
productIngredientRepo repository.ProductIngredientRepository
|
||||
txManager *repository.TxManager
|
||||
}
|
||||
|
||||
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface) *OrderServiceImpl {
|
||||
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productIngredientRepo repository.ProductIngredientRepository, txManager *repository.TxManager) *OrderServiceImpl {
|
||||
return &OrderServiceImpl{
|
||||
orderProcessor: orderProcessor,
|
||||
tableRepo: tableRepo,
|
||||
orderIngredientTransactionService: orderIngredientTransactionService,
|
||||
orderIngredientTransactionProcessor: orderIngredientTransactionProcessor,
|
||||
productIngredientRepo: productIngredientRepo,
|
||||
txManager: txManager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,47 +61,133 @@ func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOr
|
||||
}
|
||||
}
|
||||
|
||||
response, err := s.orderProcessor.CreateOrder(ctx, req, organizationID)
|
||||
var response *models.OrderResponse
|
||||
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
// Create the order
|
||||
orderResp, err := s.orderProcessor.CreateOrder(txCtx, req, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create order: %w", err)
|
||||
return fmt.Errorf("failed to create order: %w", err)
|
||||
}
|
||||
response = orderResp
|
||||
|
||||
// Create ingredient transactions for each order item
|
||||
ingredientTransactions, err = s.createIngredientTransactions(txCtx, response.ID, response.OrderItems)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ingredient transactions: %w", err)
|
||||
}
|
||||
|
||||
// Bulk create ingredient transactions
|
||||
if len(ingredientTransactions) > 0 {
|
||||
_, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bulk create ingredient transactions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Occupy table if specified
|
||||
if req.TableID != nil {
|
||||
if err := s.occupyTableWithOrder(ctx, *req.TableID, response.ID); err != nil {
|
||||
if err := s.occupyTableWithOrder(txCtx, *req.TableID, response.ID); err != nil {
|
||||
// Log warning but don't fail the transaction
|
||||
fmt.Printf("Warning: failed to occupy table %s with order %s: %v\n", *req.TableID, response.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// createIngredientTransactions creates ingredient transactions for order items efficiently
|
||||
func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, orderID uuid.UUID, orderItems []models.OrderItemResponse) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
var allTransactions []*contract.CreateOrderIngredientTransactionRequest
|
||||
|
||||
for _, orderItem := range orderItems {
|
||||
// Get product ingredients for this product
|
||||
productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err)
|
||||
}
|
||||
|
||||
if len(productIngredients) == 0 {
|
||||
continue // Skip if no ingredients
|
||||
}
|
||||
|
||||
// Calculate waste quantities
|
||||
transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate waste quantities for product %s: %w", err)
|
||||
}
|
||||
|
||||
// Set common fields for all transactions
|
||||
for _, transaction := range transactions {
|
||||
transaction.OrderID = orderID
|
||||
transaction.OrderItemID = &orderItem.ID
|
||||
transaction.ProductID = orderItem.ProductID
|
||||
transaction.ProductVariantID = orderItem.ProductVariantID
|
||||
}
|
||||
|
||||
allTransactions = append(allTransactions, transactions...)
|
||||
}
|
||||
|
||||
return allTransactions, nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
|
||||
// Validate inputs
|
||||
if orderID == uuid.Nil {
|
||||
return nil, fmt.Errorf("invalid order ID")
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := s.validateAddToOrderRequest(req); err != nil {
|
||||
return nil, fmt.Errorf("validation error: %w", err)
|
||||
}
|
||||
|
||||
// Process adding items to order
|
||||
response, err := s.orderProcessor.AddToOrder(ctx, orderID, req)
|
||||
var response *models.AddToOrderResponse
|
||||
var ingredientTransactions []*contract.CreateOrderIngredientTransactionRequest
|
||||
|
||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
addResp, err := s.orderProcessor.AddToOrder(txCtx, orderID, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add items to order: %w", err)
|
||||
return fmt.Errorf("failed to add items to order: %w", err)
|
||||
}
|
||||
response = addResp
|
||||
|
||||
ingredientTransactions, err = s.createIngredientTransactions(txCtx, orderID, response.AddedItems)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ingredient transactions: %w", err)
|
||||
}
|
||||
|
||||
if len(ingredientTransactions) > 0 {
|
||||
_, err = s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(txCtx, ingredientTransactions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to bulk create ingredient transactions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
|
||||
// Validate request
|
||||
if err := s.validateUpdateOrderRequest(req); err != nil {
|
||||
return nil, fmt.Errorf("validation error: %w", err)
|
||||
}
|
||||
|
||||
// Process order update
|
||||
response, err := s.orderProcessor.UpdateOrder(ctx, id, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
@ -137,9 +235,7 @@ func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderR
|
||||
return fmt.Errorf("failed to void order: %w", err)
|
||||
}
|
||||
|
||||
// Release table if order is voided
|
||||
if err := s.handleTableReleaseOnVoid(ctx, req.OrderID); err != nil {
|
||||
// Log the error but don't fail the void operation
|
||||
fmt.Printf("Warning: failed to handle table release for voided order %s: %v\n", req.OrderID, err)
|
||||
}
|
||||
|
||||
@ -547,3 +643,79 @@ func (s *OrderServiceImpl) handleTableReleaseOnVoid(ctx context.Context, orderID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OrderServiceImpl) createOrderIngredientTransactions(ctx context.Context, order *models.Order, orderItems []*models.OrderItem) error {
|
||||
for _, orderItem := range orderItems {
|
||||
productIngredients, err := s.productIngredientRepo.GetByProductID(ctx, orderItem.ProductID, order.OrganizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get product ingredients for product %s: %w", orderItem.ProductID, err)
|
||||
}
|
||||
|
||||
if len(productIngredients) == 0 {
|
||||
continue // Skip if no ingredients
|
||||
}
|
||||
|
||||
// Calculate waste quantities using the utility function
|
||||
transactions, err := s.calculateWasteQuantities(productIngredients, float64(orderItem.Quantity))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate waste quantities for product %s: %w", orderItem.ProductID, err)
|
||||
}
|
||||
|
||||
// Set common fields for all transactions
|
||||
for _, transaction := range transactions {
|
||||
transaction.OrderID = order.ID
|
||||
transaction.OrderItemID = &orderItem.ID
|
||||
transaction.ProductID = orderItem.ProductID
|
||||
transaction.ProductVariantID = orderItem.ProductVariantID
|
||||
}
|
||||
|
||||
// Bulk create transactions
|
||||
if len(transactions) > 0 {
|
||||
_, err := s.orderIngredientTransactionService.BulkCreateOrderIngredientTransactions(ctx, transactions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create order ingredient transactions for product %s: %w", orderItem.ProductID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateWasteQuantities calculates gross, net, and waste quantities for product ingredients
|
||||
func (s *OrderServiceImpl) calculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
|
||||
if len(productIngredients) == 0 {
|
||||
return []*contract.CreateOrderIngredientTransactionRequest{}, nil
|
||||
}
|
||||
|
||||
transactions := make([]*contract.CreateOrderIngredientTransactionRequest, 0, len(productIngredients))
|
||||
|
||||
for _, pi := range productIngredients {
|
||||
// Calculate net quantity (actual quantity needed for the product)
|
||||
netQty := pi.Quantity * quantity
|
||||
|
||||
// Calculate gross quantity (including waste)
|
||||
wasteMultiplier := 1 + (pi.WastePercentage / 100)
|
||||
grossQty := netQty * wasteMultiplier
|
||||
|
||||
// Calculate waste quantity
|
||||
wasteQty := grossQty - netQty
|
||||
|
||||
// Get unit name from ingredient
|
||||
unitName := "unit" // default
|
||||
if pi.Ingredient != nil && pi.Ingredient.Unit != nil {
|
||||
unitName = pi.Ingredient.Unit.Name
|
||||
}
|
||||
|
||||
transaction := &contract.CreateOrderIngredientTransactionRequest{
|
||||
IngredientID: pi.IngredientID,
|
||||
GrossQty: util.RoundToDecimalPlaces(grossQty, 3),
|
||||
NetQty: util.RoundToDecimalPlaces(netQty, 3),
|
||||
WasteQty: util.RoundToDecimalPlaces(wasteQty, 3),
|
||||
Unit: unitName,
|
||||
}
|
||||
|
||||
transactions = append(transactions, transaction)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
@ -152,3 +152,27 @@ func UnitModelResponseToResponse(model *models.UnitResponse) contract.UnitRespon
|
||||
}
|
||||
}
|
||||
|
||||
func IngredientUnitsModelResponseToResponse(model *models.IngredientUnitsResponse) contract.IngredientUnitsResponse {
|
||||
if model == nil {
|
||||
return contract.IngredientUnitsResponse{}
|
||||
}
|
||||
|
||||
response := contract.IngredientUnitsResponse{
|
||||
IngredientID: model.IngredientID,
|
||||
IngredientName: model.IngredientName,
|
||||
BaseUnitID: model.BaseUnitID,
|
||||
BaseUnitName: model.BaseUnitName,
|
||||
Units: make([]*contract.UnitResponse, 0),
|
||||
}
|
||||
|
||||
// Map units
|
||||
for _, unit := range model.Units {
|
||||
if unit != nil {
|
||||
unitResp := UnitModelResponseToResponse(unit)
|
||||
response.Units = append(response.Units, &unitResp)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/processor"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@ -35,8 +34,8 @@ type AccountTemplate struct {
|
||||
}
|
||||
|
||||
func CreateDefaultChartOfAccounts(ctx context.Context,
|
||||
chartOfAccountProcessor processor.ChartOfAccountProcessor,
|
||||
accountProcessor processor.AccountProcessor,
|
||||
chartOfAccountProcessor interface{}, // Use interface{} to avoid import cycle
|
||||
accountProcessor interface{}, // Use interface{} to avoid import cycle
|
||||
organizationID uuid.UUID,
|
||||
outletID *uuid.UUID) error {
|
||||
|
||||
@ -100,7 +99,7 @@ func loadDefaultChartOfAccountsTemplate() (*DefaultChartOfAccount, error) {
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
func getChartOfAccountTypeMap(ctx context.Context, chartOfAccountProcessor processor.ChartOfAccountProcessor) (map[string]uuid.UUID, error) {
|
||||
func getChartOfAccountTypeMap(ctx context.Context, chartOfAccountProcessor interface{}) (map[string]uuid.UUID, error) {
|
||||
// This is a placeholder - in a real implementation, you would call the processor
|
||||
// to get all chart of account types and create a map of code -> ID
|
||||
return map[string]uuid.UUID{
|
||||
|
||||
87
internal/util/waste_util.go
Normal file
87
internal/util/waste_util.go
Normal file
@ -0,0 +1,87 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CalculateWasteQuantities calculates gross, net, and waste quantities for product ingredients
|
||||
func CalculateWasteQuantities(productIngredients []*entities.ProductIngredient, quantity float64) ([]*models.CreateOrderIngredientTransactionRequest, error) {
|
||||
if len(productIngredients) == 0 {
|
||||
return []*models.CreateOrderIngredientTransactionRequest{}, nil
|
||||
}
|
||||
|
||||
transactions := make([]*models.CreateOrderIngredientTransactionRequest, 0, len(productIngredients))
|
||||
|
||||
for _, pi := range productIngredients {
|
||||
// Calculate net quantity (actual quantity needed for the product)
|
||||
netQty := pi.Quantity * quantity
|
||||
|
||||
// Calculate gross quantity (including waste)
|
||||
wasteMultiplier := 1 + (pi.WastePercentage / 100)
|
||||
grossQty := netQty * wasteMultiplier
|
||||
|
||||
// Calculate waste quantity
|
||||
wasteQty := grossQty - netQty
|
||||
|
||||
// Get unit name from ingredient
|
||||
unitName := "unit" // default
|
||||
if pi.Ingredient != nil && pi.Ingredient.Unit != nil {
|
||||
unitName = pi.Ingredient.Unit.Name
|
||||
}
|
||||
|
||||
transaction := &models.CreateOrderIngredientTransactionRequest{
|
||||
IngredientID: pi.IngredientID,
|
||||
GrossQty: RoundToDecimalPlaces(grossQty, 3),
|
||||
NetQty: RoundToDecimalPlaces(netQty, 3),
|
||||
WasteQty: RoundToDecimalPlaces(wasteQty, 3),
|
||||
Unit: unitName,
|
||||
}
|
||||
|
||||
transactions = append(transactions, transaction)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
// CreateOrderIngredientTransactionsFromProduct creates order ingredient transactions for a product
|
||||
func CreateOrderIngredientTransactionsFromProduct(
|
||||
productID uuid.UUID,
|
||||
productVariantID *uuid.UUID,
|
||||
orderID uuid.UUID,
|
||||
orderItemID *uuid.UUID,
|
||||
quantity float64,
|
||||
productIngredients []*entities.ProductIngredient,
|
||||
organizationID, outletID, createdBy uuid.UUID,
|
||||
) ([]*models.CreateOrderIngredientTransactionRequest, error) {
|
||||
// Calculate waste quantities
|
||||
wasteTransactions, err := CalculateWasteQuantities(productIngredients, quantity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate waste quantities: %w", err)
|
||||
}
|
||||
|
||||
// Set common fields for all transactions
|
||||
for _, transaction := range wasteTransactions {
|
||||
transaction.OrderID = orderID
|
||||
transaction.OrderItemID = orderItemID
|
||||
transaction.ProductID = productID
|
||||
transaction.ProductVariantID = productVariantID
|
||||
transaction.TransactionDate = &time.Time{}
|
||||
*transaction.TransactionDate = time.Now()
|
||||
}
|
||||
|
||||
return wasteTransactions, nil
|
||||
}
|
||||
|
||||
// RoundToDecimalPlaces rounds a float64 to the specified number of decimal places
|
||||
func RoundToDecimalPlaces(value float64, places int) float64 {
|
||||
multiplier := 1.0
|
||||
for i := 0; i < places; i++ {
|
||||
multiplier *= 10
|
||||
}
|
||||
return float64(int(value*multiplier+0.5)) / multiplier
|
||||
}
|
||||
120
internal/validator/order_ingredient_transaction_validator.go
Normal file
120
internal/validator/order_ingredient_transaction_validator.go
Normal file
@ -0,0 +1,120 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/models"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type OrderIngredientTransactionValidator interface {
|
||||
ValidateCreateOrderIngredientTransactionRequest(req *models.CreateOrderIngredientTransactionRequest) error
|
||||
ValidateUpdateOrderIngredientTransactionRequest(req *models.UpdateOrderIngredientTransactionRequest) error
|
||||
ValidateListOrderIngredientTransactionsRequest(req *models.ListOrderIngredientTransactionsRequest) error
|
||||
}
|
||||
|
||||
type OrderIngredientTransactionValidatorImpl struct {
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
func NewOrderIngredientTransactionValidator() OrderIngredientTransactionValidator {
|
||||
v := validator.New()
|
||||
return &OrderIngredientTransactionValidatorImpl{
|
||||
validator: v,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *OrderIngredientTransactionValidatorImpl) ValidateCreateOrderIngredientTransactionRequest(req *models.CreateOrderIngredientTransactionRequest) error {
|
||||
if err := v.validator.Struct(req); err != nil {
|
||||
var validationErrors []string
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("%s: %s", err.Field(), getValidationMessage(err)))
|
||||
}
|
||||
return fmt.Errorf("validation failed: %s", strings.Join(validationErrors, ", "))
|
||||
}
|
||||
|
||||
// Custom validations
|
||||
if req.GrossQty < req.NetQty {
|
||||
return fmt.Errorf("gross quantity must be greater than or equal to net quantity")
|
||||
}
|
||||
|
||||
expectedWasteQty := req.GrossQty - req.NetQty
|
||||
if req.WasteQty != expectedWasteQty {
|
||||
return fmt.Errorf("waste quantity must equal gross quantity minus net quantity")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *OrderIngredientTransactionValidatorImpl) ValidateUpdateOrderIngredientTransactionRequest(req *models.UpdateOrderIngredientTransactionRequest) error {
|
||||
if err := v.validator.Struct(req); err != nil {
|
||||
var validationErrors []string
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("%s: %s", err.Field(), getValidationMessage(err)))
|
||||
}
|
||||
return fmt.Errorf("validation failed: %s", strings.Join(validationErrors, ", "))
|
||||
}
|
||||
|
||||
// Custom validations for partial updates
|
||||
if req.GrossQty != nil && req.NetQty != nil {
|
||||
if *req.GrossQty < *req.NetQty {
|
||||
return fmt.Errorf("gross quantity must be greater than or equal to net quantity")
|
||||
}
|
||||
}
|
||||
|
||||
if req.GrossQty != nil && req.NetQty != nil && req.WasteQty != nil {
|
||||
expectedWasteQty := *req.GrossQty - *req.NetQty
|
||||
if *req.WasteQty != expectedWasteQty {
|
||||
return fmt.Errorf("waste quantity must equal gross quantity minus net quantity")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *OrderIngredientTransactionValidatorImpl) ValidateListOrderIngredientTransactionsRequest(req *models.ListOrderIngredientTransactionsRequest) error {
|
||||
if err := v.validator.Struct(req); err != nil {
|
||||
var validationErrors []string
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
validationErrors = append(validationErrors, fmt.Sprintf("%s: %s", err.Field(), getValidationMessage(err)))
|
||||
}
|
||||
return fmt.Errorf("validation failed: %s", strings.Join(validationErrors, ", "))
|
||||
}
|
||||
|
||||
// Custom validations
|
||||
if req.StartDate != nil && req.EndDate != nil {
|
||||
if req.StartDate.After(*req.EndDate) {
|
||||
return fmt.Errorf("start date must be before end date")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getValidationMessage(err validator.FieldError) string {
|
||||
switch err.Tag() {
|
||||
case "required":
|
||||
return "is required"
|
||||
case "min":
|
||||
return fmt.Sprintf("must be at least %s", err.Param())
|
||||
case "max":
|
||||
return fmt.Sprintf("must be at most %s", err.Param())
|
||||
case "gt":
|
||||
return fmt.Sprintf("must be greater than %s", err.Param())
|
||||
case "gte":
|
||||
return fmt.Sprintf("must be greater than or equal to %s", err.Param())
|
||||
case "lt":
|
||||
return fmt.Sprintf("must be less than %s", err.Param())
|
||||
case "lte":
|
||||
return fmt.Sprintf("must be less than or equal to %s", err.Param())
|
||||
case "email":
|
||||
return "must be a valid email address"
|
||||
case "uuid":
|
||||
return "must be a valid UUID"
|
||||
case "len":
|
||||
return fmt.Sprintf("must be exactly %s characters long", err.Param())
|
||||
default:
|
||||
return "is invalid"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
-- Remove waste_percentage column from product_ingredients table
|
||||
ALTER TABLE product_ingredients DROP COLUMN waste_percentage;
|
||||
@ -0,0 +1,6 @@
|
||||
-- Add waste_percentage column to product_ingredients table
|
||||
ALTER TABLE product_ingredients
|
||||
ADD COLUMN waste_percentage DECIMAL(5,2) DEFAULT 0.00 CHECK (waste_percentage >= 0 AND waste_percentage <= 100);
|
||||
|
||||
-- Add comment to explain the column
|
||||
COMMENT ON COLUMN product_ingredients.waste_percentage IS 'Waste percentage for this ingredient (0-100). Used to calculate gross quantity needed including waste.';
|
||||
@ -0,0 +1,2 @@
|
||||
-- Drop order ingredients transactions table
|
||||
DROP TABLE IF EXISTS order_ingredients_transactions;
|
||||
@ -0,0 +1,37 @@
|
||||
-- Order ingredients transactions table
|
||||
CREATE TABLE order_ingredients_transactions (
|
||||
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,
|
||||
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
||||
order_item_id UUID REFERENCES order_items(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
product_variant_id UUID REFERENCES product_variants(id) ON DELETE CASCADE,
|
||||
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
|
||||
gross_qty DECIMAL(12,3) NOT NULL CHECK (gross_qty > 0),
|
||||
net_qty DECIMAL(12,3) NOT NULL CHECK (net_qty > 0),
|
||||
waste_qty DECIMAL(12,3) NOT NULL CHECK (waste_qty >= 0),
|
||||
unit VARCHAR(50) NOT NULL,
|
||||
transaction_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_order_ingredients_transactions_organization_id ON order_ingredients_transactions(organization_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_outlet_id ON order_ingredients_transactions(outlet_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_order_id ON order_ingredients_transactions(order_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_order_item_id ON order_ingredients_transactions(order_item_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_product_id ON order_ingredients_transactions(product_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_product_variant_id ON order_ingredients_transactions(product_variant_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_ingredient_id ON order_ingredients_transactions(ingredient_id);
|
||||
CREATE INDEX idx_order_ingredients_transactions_transaction_date ON order_ingredients_transactions(transaction_date);
|
||||
CREATE INDEX idx_order_ingredients_transactions_created_by ON order_ingredients_transactions(created_by);
|
||||
CREATE INDEX idx_order_ingredients_transactions_created_at ON order_ingredients_transactions(created_at);
|
||||
|
||||
-- Add comment to explain the table
|
||||
COMMENT ON TABLE order_ingredients_transactions IS 'Tracks ingredient usage for orders including gross, net, and waste quantities';
|
||||
COMMENT ON COLUMN order_ingredients_transactions.gross_qty IS 'Total quantity needed including waste';
|
||||
COMMENT ON COLUMN order_ingredients_transactions.net_qty IS 'Actual quantity used in the product';
|
||||
COMMENT ON COLUMN order_ingredients_transactions.waste_qty IS 'Waste quantity (gross_qty - net_qty)';
|
||||
Loading…
x
Reference in New Issue
Block a user